diff --git a/.gitignore b/.gitignore index db7ffb6..c2d45db 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,14 @@ doc/build/ python/xnd/xnd.h *.egg-info __pycache__ + +# Ignore ruby generated files +ruby/Gemfile.lock +*.gem +ruby/tmp/ + +# tag files +GPATH +GRTAGS +GTAGS + diff --git a/python/test_xnd.py b/python/test_xnd.py index 8d6ac5f..def93a2 100644 --- a/python/test_xnd.py +++ b/python/test_xnd.py @@ -2319,7 +2319,7 @@ def test_float64(self): class TestComplexKind(XndTestCase): def test_complex_kind(self): - self.assertRaises(ValueError, xnd.empty, "Complex") +n self.assertRaises(ValueError, xnd.empty, "Complex") class TestComplex(XndTestCase): diff --git a/ruby/CONTRIBUTING.md b/ruby/CONTRIBUTING.md new file mode 100644 index 0000000..327ca63 --- /dev/null +++ b/ruby/CONTRIBUTING.md @@ -0,0 +1,42 @@ +# Developer notes + +## Using C APIs from other Ruby libraries + +The xnd Ruby wrapper uses type definitions, macros and functions from the +libndtypes Ruby wrapper. For this purpose, it is important to make sure that +xnd can find the correct headers and shared object files during compile and +linking time. + +This requires some modifications in both the ndtypes and xnd repos. ndtypes +must ensure that the relevant header files and shared object are exposed to +other Ruby gems. + +## structs in XND + +The primary struct that contains data for the XND type is the following: +``` +typedef struct XndObject { + VALUE mblock; /* owner of the primary type and memory block */ + VALUE type; /* owner of the current type. lives and dies with this obj. */ + xnd_t xnd; /* typed view, does not own anything */ +} XndObject; +``` +As the comments say, the `mblock` is an object of type `MemoryBlockObject` that +is never revealed to the user. It is shared between multiple instances of XND objects +and contains the primary type (i.e the type of the root object). + +The `type` attribute is of type `NDT` and exists only on a per-object basis. It is specific +to the particular instance of `XndObject`. Therefore, whenever making a view, it is important +to store a reference to the `mblock` in the GC guard so that the memory that the view needs +to access for its data needs does not get GC'd in case the root object needs to be GC'd. + +## Infinite ranges + +Ruby 2.6 will introduce infinite ranges using a prettier and less verbose syntax, but for now +we're stuck with using `Float::INFINITY` every time we want to specify an infinite range. This +can quickly get tedious to type. Therefore XND introduces the following syntax for infinite +ranges for references arrays: + +* Full range (`0..Float::INFINITY`) : `INF`. +* Part range (`4..Float::INFINITY`) : `4..INF`. + diff --git a/ruby/Gemfile b/ruby/Gemfile new file mode 100644 index 0000000..06618ce --- /dev/null +++ b/ruby/Gemfile @@ -0,0 +1,3 @@ +source 'https://rubygems.org' +gemspec + diff --git a/ruby/History.md b/ruby/History.md new file mode 100644 index 0000000..e69de29 diff --git a/ruby/README.md b/ruby/README.md new file mode 100644 index 0000000..6e233cd --- /dev/null +++ b/ruby/README.md @@ -0,0 +1,14 @@ +# Introduction + +XND is a library for storing typed data defined by ndtypes. It is a wrapper +over the libxnd C library. + +# Installation + +For direct installation, run: `gem install xnd --pre`. + +# Usage + +# Acknowledgments + +Sponsored by Quansight Inc. diff --git a/ruby/Rakefile b/ruby/Rakefile new file mode 100644 index 0000000..c388aad --- /dev/null +++ b/ruby/Rakefile @@ -0,0 +1,105 @@ +require 'rake' +require 'rake/extensiontask' +require 'bundler/gem_tasks' +require 'rake/testtask' +require 'fileutils' +require 'xnd/version.rb' + +gemspec = eval(IO.read("xnd.gemspec")) + +ext_name = "ruby_xnd" +Rake::ExtensionTask.new(ext_name, gemspec) do |ext| + ext.ext_dir = "ext/#{ext_name}" + ext.source_pattern = "**/*.{c,h}" +end + +Rake::TestTask.new(:test) do |t| + t.libs << "test" + t.libs << "lib" + t.test_files = FileList['test/**/test_*.rb'] +end + +def run *cmd + sh(cmd.join(" ")) +end + +task :console do + cmd = ['irb', "-r './lib/xnd.rb'"] + run(*cmd) +end + +task :pry do + cmd = ['pry', "-r './lib/xnd.rb'"] + run(*cmd) +end + +BASEDIR = Pathname( __FILE__ ).dirname.relative_path_from( Pathname.pwd ) +SPECDIR = BASEDIR + 'spec' + +VALGRIND_OPTIONS = [ + "--tool=memcheck", + #"--leak-check=yes", + "--num-callers=15", + #"--error-limit=no", + "--partial-loads-ok=yes", + "--undef-value-errors=no" #, + #"--dsymutil=yes" +] + +CALLGRIND_OPTIONS = [ + "--tool=callgrind", + "--dump-instr=yes", + "--simulate-cache=yes", + "--collect-jumps=yes" +] + +VALGRIND_MEMORYFILL_OPTIONS = [ + "--freelist-vol=100000000", + "--malloc-fill=6D", + "--free-fill=66 ", +] + +# partial-loads-ok and undef-value-errors necessary to ignore +# spurious (and eminently ignorable) warnings from the ruby +# interpreter + +desc "Run specs under Valgrind." +task :valgrind => [ :compile ] do |task| + cmd = [ 'valgrind' ] + VALGRIND_OPTIONS + cmd += [" rake test "] + run( *cmd ) +end + +LEAKCHECK_CMD = [ 'ruby', '-Ilib:ext', "#{SPECDIR}/leakcheck.rb" ] + +task :clobber do |task| + [ + "ext/#{ext_name}/include", + "ext/#{ext_name}/share", + "ext/#{ext_name}/lib", + ].each do |f| + puts "deleting folder #{f}..." + FileUtils.rm_rf(f) + end + + Dir.chdir("ext/#{ext_name}/xnd/libxnd/") do + system("make clean") + end +end + +task :develop do + ext_xnd = "ext/ruby_xnd/xnd" + puts "deleting previously created #{ext_xnd} directory..." + FileUtils.rm_rf(ext_xnd) + Dir.mkdir(ext_xnd) + + puts "cloning xnd repo into ext/ folder..." + system("git clone https://github.com/plures/xnd #{ext_xnd}") + + Dir.chdir(ext_xnd) do + system("git checkout #{RubyXND::COMMIT}") + end + + puts "building gem with rake build..." + system("rake build") +end diff --git a/ruby/ext/ruby_xnd/.gitignore b/ruby/ext/ruby_xnd/.gitignore new file mode 100644 index 0000000..2f545e8 --- /dev/null +++ b/ruby/ext/ruby_xnd/.gitignore @@ -0,0 +1,2 @@ +/include +/xnd diff --git a/ruby/ext/ruby_xnd/extconf.rb b/ruby/ext/ruby_xnd/extconf.rb new file mode 100644 index 0000000..994cf93 --- /dev/null +++ b/ruby/ext/ruby_xnd/extconf.rb @@ -0,0 +1,74 @@ +require 'mkmf' + +def windows? + (/cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM) != nil +end + +def mac? + (/darwin/ =~ RUBY_PLATFORM) != nil +end + +def unix? + !windows? +end + +# libndtypes config + +ndtypes_version = ">= 0.2.0dev5" +ndtypes_spec = Gem::Specification.find_by_name("ndtypes", ndtypes_version) +ndtypes_extdir = File.join(ndtypes_spec.gem_dir, 'ext', 'ruby_ndtypes') +ndtypes_includedir = File.join(ndtypes_extdir, 'include') +ndtypes_libdir = File.join(ndtypes_extdir, 'lib') + +find_header("ruby_ndtypes.h", ndtypes_includedir) +raise "cannot find ruby_ndtypes.h in path #{ndtypes_includedir}." unless have_header("ruby_ndtypes.h") + +find_header("ndtypes.h", ndtypes_includedir) +find_library("ndtypes", nil, ndtypes_libdir) + +dir_config("ndtypes", [ndtypes_includedir], [ndtypes_libdir]) + +# libxnd config + +puts "compiling libxnd for your machine..." +Dir.chdir(File.join(File.dirname(__FILE__) + "/xnd")) do + if unix? + Dir.chdir("libxnd") do + Dir.mkdir(".objs") unless Dir.exists? ".objs" + end + + system("./configure --prefix=#{File.expand_path("../")} --with-docs=no " + + "--with-includes=#{ndtypes_includedir}") + system("make") + system("make install") + elsif windows? + raise NotImplementedError, "need to specify build instructions for windows." + end +end + +binaries = File.expand_path(File.join(File.dirname(__FILE__) + "/lib/")) +headers = File.expand_path(File.join(File.dirname(__FILE__) + "/include/")) + +find_library("xnd", nil, binaries) +find_header("xnd.h", headers) + +FileUtils.copy_file File.expand_path(File.join(File.dirname(__FILE__) + + "/ruby_xnd.h")), + "#{headers}/ruby_xnd.h" + +dir_config("xnd", [headers], [binaries]) + +$INSTALLFILES = [ + ["ruby_xnd.h", "$(archdir)"], + ["xnd.h", "$(archdir)"] +] + +# for macOS +append_ldflags("-Wl,-rpath #{binaries}") + +basenames = %w{util float_pack_unpack gc_guard ruby_xnd} +$objs = basenames.map { |b| "#{b}.o" } +$srcs = basenames.map { |b| "#{b}.c" } + +$CFLAGS += " -fPIC -g " +create_makefile("ruby_xnd/ruby_xnd") diff --git a/ruby/ext/ruby_xnd/float_pack_unpack.c b/ruby/ext/ruby_xnd/float_pack_unpack.c new file mode 100644 index 0000000..5747b8e --- /dev/null +++ b/ruby/ext/ruby_xnd/float_pack_unpack.c @@ -0,0 +1,277 @@ +/* BSD 3-Clause License + * + * Copyright (c) 2018, Quansight and Sameer Deshmukh + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* Functions for packing and unpacking floats from char * arrays. + + Author: Sameer Deshmukh (@v0dro) +*/ + +#include "ruby_xnd_internal.h" + +/* Pack a 32-bit float into a contiguous unsigned char* buffer. + + Reference: + https://github.com/python/cpython/blob/master/Include/floatobject.h#L77 + + @param num The number to be packed. + @param ptr + @param le Le is a boolean argument. True if you want the string in + litte-endian format, false if you want it in big-endian format. + + @return 0 if success. Raise ruby error if failure. +*/ +int +rb_xnd_pack_float32(double num, unsigned char* ptr, int le) +{ + float y = (float)num; + int i, incr = 1; + + if (isinf(y) && !isinf(num)) { + rb_raise(rb_eRangeError, "cannot fit value in 32-bit floating point number."); + } + + unsigned char s[sizeof(float)]; + memcpy(s, &y, sizeof(float)); + +#ifdef XND_DEBUG + if (!le) { // choose big-endian + ptr += 3; + incr = -1; + } +#else + if ((IEEE_LITTLE_ENDIAN_P && !le) || (IEEE_BIG_ENDIAN_P && le)) { // choose big-endian + ptr += 3; + incr = -1; + } +#endif + + for (i = 0; i < sizeof(float); ++i) { + *ptr = s[i]; + ptr += incr; + } + + return 0; +} + +/* Unpack a 32-bit float from a contiguos unsigned char* buffer. + + @param ptr : + @param le Le is a boolean argument. True if you want the string in + litte-endian format, false if you want it in big-endian format. + + @return unpacked number as a double. +*/ +int +rb_xnd_unpack_float32(float *x, unsigned char* ptr, int le) +{ +#ifdef XND_DEBUG + if (!le) // big-endian +#else + if ((IEEE_LITTLE_ENDIAN_P && !le) || (IEEE_BIG_ENDIAN_P && le))// big-endian +#endif + { + char buf[4]; + char *d = &buf[3]; + int i; + + for (i = 0; i < sizeof(float); ++i) { + *d-- = *ptr++; + } + memcpy(x, buf, sizeof(float)); + } + else { + memcpy(x, ptr, sizeof(float)); + } + + return 0; +} + +/* Pack 64 bit floating point number into an unsigned char array. + + @param num + @param ptr + @param le + */ +int +rb_xnd_pack_float64(double num, unsigned char *ptr, int le) +{ + int i, incr = 1; + + unsigned char s[sizeof(double)]; + memcpy(s, &num, sizeof(double)); + +#ifdef XND_DEBUG + if (!le) { // choose big-endian + ptr += 7; + incr = -1; + } +#else + if ((IEEE_LITTLE_ENDIAN_P && !le) || (IEEE_BIG_ENDIAN_P && le)) { // choose big-endian + ptr += 7; + incr = -1; + } +#endif + + for (i = 0; i < sizeof(double); ++i) { + *ptr = s[i]; + ptr += incr; + } + + return 0; +} + +/* Unpack a 64-bit floating point number from an unsigned char array and return + the resulting number as a type double. + */ +int +rb_xnd_unpack_float64(double *x, unsigned char *ptr, int le) +{ +#ifdef XND_DEBUG + if (!le) { // big-endian +#else + if ((IEEE_LITTLE_ENDIAN_P && !le) || (IEEE_BIG_ENDIAN_P && le)) { // big-endian +#endif + char buf[sizeof(double)]; + char *d = &buf[sizeof(double)-1]; + int i; + + for (i = 0; i < sizeof(double); ++i) { + *d-- = *ptr++; + } + memcpy(x, buf, sizeof(double)); + } + else { + memcpy(x, ptr, sizeof(double)); + } + + return 0; +} + +#ifdef XND_DEBUG +/* Functions for testing packing/unpacking functions. + + In order to avoid the need for injecting the dependency of a C testing framework, + these are tests that work with the pack/unpack functions and are called in the + Init_ function if XND_DEBUG is defined. +*/ +void +test_pack_float32(void) +{ + double num = 16448.0; + int i; + unsigned char ptr[4]; + + /* test big endian */ + unsigned char ans_bige[4] = {0x46, 0x80, 0x80, 0x00}; + rb_xnd_pack_float32(num, ptr, 0); + for (i = 0; i < 4; i++) { + assert(ans_bige[i] == ptr[i]); + } + + /* test little endian */ + unsigned char ans_lite[4] = {0, 0X80, 0X80, 0X46}; + rb_xnd_pack_float32(num, ptr, 1); + for (i = 0; i < 4; i++) { + assert(ans_lite[i] == ptr[i]); + } +} + +void test_unpack_float32(void) +{ + float answer = 16448.0, result = 0.0; + + /* test big endian */ + unsigned char ptr_bige[4] = {0x46, 0x80, 0x80, 0x00}; + rb_xnd_unpack_float32(&result, ptr_bige, 0); + assert(answer == result); + + /* test little endian */ + unsigned char ptr_lite[4] = {0, 0X80, 0X80, 0X46}; + rb_xnd_unpack_float32(&result, ptr_lite, 1); + assert(answer == result); +} + +void test_pack_float64(void) +{ + double num = 16448.0; + int i; + unsigned char ptr[8]; + + /* test big endian. */ + unsigned char ans_bige[8] = {0x40, 0xD0, 0x10, 0, 0, 0, 0, 0}; + rb_xnd_pack_float64(num, ptr, 0); + for (i = 0; i < 8; i++) { + assert(ans_bige[i] == ptr[i]); + } + + /* test little endian. */ + unsigned char ans_lite[8] = {0, 0, 0, 0, 0, 0X10, 0XD0, 0X40}; + rb_xnd_pack_float64(num, ptr, 1); + for (i = 0; i < 8; i++) { + assert(ans_lite[i] == ptr[i]); + } + + double a = 1.0; + unsigned char ans_lite_a[8] = {0, 0, 0, 0, 0, 0, 0xF0, 0x3F}; + rb_xnd_pack_float64(a, ptr, 1); + for (i = 0; i < 8; i++) { + assert(ans_lite_a[i] == ptr[i]); + } +} + +void test_unpack_float64(void) +{ + double answer = 16448.0, result = 0.0; + + /* test big-endian */ + unsigned char ptr_bige[8] = {0x40, 0xD0, 0x10, 0, 0, 0, 0, 0}; + rb_xnd_unpack_float64(&result, ptr_bige, 0); + assert(answer == result); + + /* test little-endian */ + unsigned char ptr_lite[8] = {0, 0, 0, 0, 0, 0X10, 0XD0, 0X40}; + rb_xnd_unpack_float64(&result, ptr_lite, 1); + assert(answer == result); + + double a = 1.0; + unsigned char ans_lite_a[8] = {0, 0, 0, 0, 0, 0, 0xF0, 0x3F}; + rb_xnd_unpack_float64(&result, ans_lite_a, 1); + assert(a == result); +} + +void run_float_pack_unpack_tests(void) +{ + test_pack_float32(); + test_unpack_float32(); + test_pack_float64(); + test_unpack_float64(); +} +#endif diff --git a/ruby/ext/ruby_xnd/float_pack_unpack.h b/ruby/ext/ruby_xnd/float_pack_unpack.h new file mode 100644 index 0000000..dcd464c --- /dev/null +++ b/ruby/ext/ruby_xnd/float_pack_unpack.h @@ -0,0 +1,47 @@ +/* BSD 3-Clause License + * + * Copyright (c) 2018, Quansight and Sameer Deshmukh + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* Functions for packing and unpacking floats from char * arrays. + + Author: Sameer Deshmukh (@v0dro) +*/ + +int rb_xnd_pack_float32(double num, unsigned char* ptr, int le); + +int rb_xnd_unpack_float32(float *x, unsigned char* ptr, int le); + +int rb_xnd_pack_float64(double num, unsigned char *ptr, int le); + +int rb_xnd_unpack_float64(double *x, unsigned char *ptr, int le); + +#ifdef XND_DEBUG +void run_float_pack_unpack_tests(void); +#endif diff --git a/ruby/ext/ruby_xnd/gc_guard.c b/ruby/ext/ruby_xnd/gc_guard.c new file mode 100644 index 0000000..f0fbd47 --- /dev/null +++ b/ruby/ext/ruby_xnd/gc_guard.c @@ -0,0 +1,36 @@ +/* Functions useful for interfacing shared rbuf objects with the Ruby GC. */ +/* Author: Sameer Deshmukh (@v0dro) */ +#include "ruby_xnd_internal.h" + +#define GC_GUARD_TABLE_NAME "@__gc_guard_table" + +static ID id_gc_guard_table; + +/* Unregister an NDT object-rbuf pair from the GC guard. */ +void +rb_xnd_gc_guard_unregister(XndObject *xnd) +{ + VALUE table = rb_ivar_get(mRubyXND_GCGuard, id_gc_guard_table); + rb_hash_delete(table, PTR2NUM(xnd)); +} + +/* Register a XND-mblock pair in the GC guard. */ +void +rb_xnd_gc_guard_register(XndObject *xnd, VALUE mblock) +{ + VALUE table = rb_ivar_get(mRubyXND_GCGuard, id_gc_guard_table); + if (table == Qnil) { + rb_raise(rb_eLoadError, "GC guard not initialized."); + } + + rb_hash_aset(table, PTR2NUM(xnd), mblock); +} + +/* Initialize the global GC guard table. klass is a VALUE reprensenting NDTypes class. */ +void +rb_xnd_init_gc_guard(void) +{ + id_gc_guard_table = rb_intern(GC_GUARD_TABLE_NAME); + rb_ivar_set(mRubyXND_GCGuard, id_gc_guard_table, rb_hash_new()); +} + diff --git a/ruby/ext/ruby_xnd/gc_guard.h b/ruby/ext/ruby_xnd/gc_guard.h new file mode 100644 index 0000000..3a4942e --- /dev/null +++ b/ruby/ext/ruby_xnd/gc_guard.h @@ -0,0 +1,12 @@ +/* Header file containing various functions for GC guard table. */ + +#ifndef GC_GUARD_H +#define GC_GUARD_H + +#include "ruby_xnd_internal.h" + +void rb_xnd_gc_guard_unregister(XndObject *); +void rb_xnd_gc_guard_register(XndObject *, VALUE); +void rb_xnd_init_gc_guard(void); + +#endif /* GC_GUARD_H */ diff --git a/ruby/ext/ruby_xnd/memory_block_object.c b/ruby/ext/ruby_xnd/memory_block_object.c new file mode 100644 index 0000000..b09ec71 --- /dev/null +++ b/ruby/ext/ruby_xnd/memory_block_object.c @@ -0,0 +1,32 @@ +/* BSD 3-Clause License + * + * Copyright (c) 2018, Quansight and Sameer Deshmukh + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* File for various operations on the MemoryBlockObject struct. */ diff --git a/ruby/ext/ruby_xnd/memory_block_object.h b/ruby/ext/ruby_xnd/memory_block_object.h new file mode 100644 index 0000000..6d58319 --- /dev/null +++ b/ruby/ext/ruby_xnd/memory_block_object.h @@ -0,0 +1,33 @@ +/* BSD 3-Clause License + * + * Copyright (c) 2018, Quansight and Sameer Deshmukh + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* Headers for MemoryBlockObject */ + diff --git a/ruby/ext/ruby_xnd/ruby_xnd.c b/ruby/ext/ruby_xnd/ruby_xnd.c new file mode 100644 index 0000000..7dced22 --- /dev/null +++ b/ruby/ext/ruby_xnd/ruby_xnd.c @@ -0,0 +1,1987 @@ +/* BSD 3-Clause License + * + * Copyright (c) 2018, Quansight and Sameer Deshmukh + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* File containing the majority implementation of the Ruby XND wrapper. + * + * Author: Sameer Deshmukh (@v0dro) + */ + +#include "ruby_xnd_internal.h" +#include "xnd.h" + +VALUE cRubyXND; +VALUE cXND; +static VALUE cRubyXND_MBlock; +static const rb_data_type_t MemoryBlockObject_type; +static const rb_data_type_t XndObject_type; + +static VALUE rb_eValueError; + +VALUE mRubyXND_GCGuard; + +/****************************************************************************/ +/* Error handling */ +/****************************************************************************/ +static VALUE +seterr(ndt_context_t *ctx) +{ + return rb_ndtypes_set_error(ctx); +} + +#ifdef XND_DEBUG +void +obj_inspect(const char* msg, VALUE obj) +{ + VALUE insp = rb_funcall(obj, rb_intern("inspect"), 0, NULL); + printf("%s %s\n.", msg, StringValuePtr(insp)); +} +#endif + +/****************************************************************************/ +/* Singletons */ +/****************************************************************************/ +static VALUE +xnd_ellipsis(void) +{ + return rb_funcall(rb_const_get_at(cXND, rb_intern("Ellipsis")), + rb_intern("new"), 0, NULL); +} + +/****************************************************************************/ +/* MemoryBlock Object */ +/****************************************************************************/ + +/* The MemoryBlockObject is shared among several XND views/objects. */ +typedef struct MemoryBlockObject { + VALUE type; /* type owner (ndtype) */ + xnd_master_t *xnd; /* memblock owner */ +} MemoryBlockObject; + +#define GET_MBLOCK(obj, mblock_p) do { \ + TypedData_Get_Struct((obj), MemoryBlockObject, \ + &MemoryBlockObject_type, (mblock_p)); \ + } while (0) +#define MAKE_MBLOCK(self, mblock_p) TypedData_Make_Struct(self, MemoryBlockObject, \ + &MemoryBlockObject_type, mblock_p) +#define WRAP_MBLOCK(self, mblock_p) TypedData_Wrap_Struct(self, \ + &MemoryBlockObject_type, mblock_p) + +/* Mark Ruby objects within MemoryBlockObject. */ +static void +MemoryBlockObject_dmark(void *self) +{ + MemoryBlockObject *mblock = (MemoryBlockObject*)self; + + rb_gc_mark(mblock->type); +} + +static void +MemoryBlockObject_dfree(void *self) +{ + MemoryBlockObject *mblock = (MemoryBlockObject*)self; + + xnd_del(mblock->xnd); + mblock->xnd = NULL; + xfree(mblock); +} + +static size_t +MemoryBlockObject_dsize(const void *self) +{ + return sizeof(MemoryBlockObject); +} + +static const rb_data_type_t MemoryBlockObject_type = { + .wrap_struct_name = "MemoryBlockObject", + .function = { + .dmark = MemoryBlockObject_dmark, + .dfree = MemoryBlockObject_dfree, + .dsize = MemoryBlockObject_dsize, + .reserved = {0,0}, + }, + .parent = 0, + .flags = RUBY_TYPED_FREE_IMMEDIATELY, +}; + +/* Allocate a MemoryBlockObject and return a pointer to allocated memory. */ +static MemoryBlockObject * +mblock_alloc(void) +{ + MemoryBlockObject *self; + + self = ALLOC(MemoryBlockObject); + if (self == NULL) { + + } + + self->type = NULL; + self->xnd = NULL; + + return self; +} + +/* Allocate a MemoryBlockObject and wrap it in a Ruby object. */ +static VALUE +mblock_allocate(void) +{ + MemoryBlockObject *self = mblock_alloc(); + + return WRAP_MBLOCK(cRubyXND_MBlock, self); +} + +/* Create empty mblock with no data. */ +static VALUE +mblock_empty(VALUE type) +{ + NDT_STATIC_CONTEXT(ctx); + MemoryBlockObject *mblock_p; + + if (!rb_ndtypes_check_type(type)) { + rb_raise(rb_eArgError, "require NDT object to create mblock in mblock_empty."); + } + + mblock_p = mblock_alloc(); + mblock_p->xnd = xnd_empty_from_type( + rb_ndtypes_const_ndt(type), + XND_OWN_EMBEDDED, &ctx); + if (mblock_p->xnd == NULL) { + rb_raise(rb_eValueError, "cannot create mblock object from given type."); + } + mblock_p->type = type; + + return WRAP_MBLOCK(cRubyXND_MBlock, mblock_p); +} + +static VALUE +mblock_from_xnd(xnd_t *src) +{ + NDT_STATIC_CONTEXT(ctx); + MemoryBlockObject *mblock_p; + VALUE type, mblock; + xnd_master_t *x; + + x = xnd_from_xnd(src, XND_OWN_EMBEDDED, &ctx); + if (x == NULL) { + seterr(&ctx); + raise_error(); + } + + type = rb_ndtypes_from_type((ndt_t *)x->master.type); + mblock = mblock_allocate(); + + GET_MBLOCK(mblock, mblock_p); + + mblock_p->type = type; + mblock_p->xnd = x; + + return mblock; +} + +static void +_strncpy(char *dest, const void *src, size_t len, size_t size) +{ + assert (len <= size); + memcpy(dest, src, len); + memset(dest+len, '\0', size-len); +} + +static int +string_is_ascii(VALUE str) +{ + return RB_ENCODING_IS_ASCII8BIT(str); +} + +/* FIXME: Below functions make Ruby function calls. Find more efficient way. */ + +/* Return true if String is UTF-8 encoding. */ +static int +string_is_u8(VALUE str) +{ + return RTEST( + rb_funcall( + rb_funcall(str, rb_intern("encoding"), 0, NULL), + rb_intern("=="), 1, + rb_const_get(rb_cEncoding, rb_intern("UTF_8")) + ) + ); +} + +static int64_t +u8_skip_trailing_zero(const uint8_t *ptr, int64_t codepoints) +{ + int64_t i; + + for (i=codepoints-1; i >= 0; i--) + if (ptr[i] != 0) + return i+1; + + return 0; +} + +static int64_t +u16_skip_trailing_zero(const uint16_t *ptr, int64_t codepoints) +{ + int64_t i; + + for (i=codepoints-1; i >= 0; i--) + if (ptr[i] != 0) + return i+1; + + return 0; +} + +static int64_t +u32_skip_trailing_zero(const uint32_t *ptr, int64_t codepoints) +{ + int64_t i; + + for (i=codepoints-1; i >= 0; i--) + if (ptr[i] != 0) + return i+1; + + return 0; +} + +static int64_t +get_int(VALUE data, int64_t min, int64_t max) +{ + int64_t x; + + x = NUM2LL(data); + if (x < min || x > max) { + rb_raise(rb_eRangeError, "Number out of range of int64 range."); + } + + return x; +} + +static uint64_t +get_uint(VALUE data, uint64_t max) +{ + unsigned long long x = NUM2ULL(data); + + if (x == (unsigned long long)-1) { + rb_raise(rb_eRangeError, "cannot assigned negative number to unsigned type."); + } + + if (x > max) { + rb_raise(rb_eRangeError, "number out of range: %ulld", x); + } + + return x; +} + +/* Initialize an mblock object with data. */ +static int +mblock_init(xnd_t * const x, VALUE data) +{ + NDT_STATIC_CONTEXT(ctx); + const ndt_t * const t = x->type; + + if (!check_invariants(t)) { + rb_raise(rb_eArgError, "invariants in type."); + } + + if (ndt_is_abstract(t)) { + rb_raise(rb_eTypeError, "specified NDT has abstract type."); + } + + /* set missing value. */ + if (ndt_is_optional(t)) { + if (t->ndim > 0) { + rb_raise(rb_eNotImpError, + "optional dimensions are not implemented."); + } + + if (data == Qnil) { + xnd_set_na(x); + return 0; + } + + xnd_set_valid(x); + } + + switch (t->tag) { + case FixedDim: { + const int64_t shape = t->FixedDim.shape; + int64_t i; + + Check_Type(data, T_ARRAY); + + if (RARRAY_LEN(data) != shape) { + rb_raise(rb_eArgError, + "Input length (%ld) and type length (%ld) mismatch.", + RARRAY_LEN(data), shape); + } + + for (i = 0; i < shape; i++) { + xnd_t next = xnd_fixed_dim_next(x, i); + VALUE rb_index[1] = { LL2NUM(i) }; + + mblock_init(&next, rb_ary_aref(1, rb_index, data)); + } + return 0; + } + + case VarDim: { + int64_t start, step, shape; + int64_t i; + + Check_Type(data, T_ARRAY); + + shape = ndt_var_indices(&start, &step, t, x->index, &ctx); + if (shape < 0) { + seterr(&ctx); + raise_error(); + } + + if (RARRAY_LEN(data) != shape) { + rb_raise(rb_eValueError, "expected Array with size %ld not %ld.", + RARRAY_LEN(data), shape); + } + + for (i = 0; i < shape; i++) { + xnd_t next = xnd_var_dim_next(x, start, step, i); + VALUE rb_index[1] = { LL2NUM(i) }; + + mblock_init(&next, rb_ary_aref(1, rb_index, data)); + } + + return 0; + } + + case Tuple: { + const int64_t shape = t->Tuple.shape; + int64_t i; + + /* since ruby does not have immutable tuple-type, we use Array instead. */ + Check_Type(data, T_ARRAY); + + if (RARRAY_LEN(data) != shape) { + rb_raise(rb_eArgError, + "expected Array with size %ld, not %ld.", + shape, RARRAY_LEN(data)); + } + + for (i = 0; i < shape; i++) { + xnd_t next = xnd_tuple_next(x, i, &ctx); + if (next.ptr == NULL) { + seterr(&ctx); + raise_error(); + } + VALUE rb_index[1] = { LL2NUM(i) }; + + mblock_init(&next, rb_ary_aref(1, rb_index, data)); + } + + return 0; + } + + case Record: { + const int64_t shape = t->Record.shape; + VALUE temp; + int64_t i; + + Check_Type(data, T_HASH); + + if (rb_xnd_hash_size(data) != shape) { + rb_raise(rb_eArgError, "expected Hash size does not match with shape size."); + } + + for (i = 0; i < shape; i++) { + xnd_t next = xnd_record_next(x, i, &ctx); + if (next.ptr == NULL) { + seterr(&ctx); + raise_error(); + } + + temp = rb_hash_aref(data, rb_str_new2(t->Record.names[i])); + mblock_init(&next, temp); + } + + return 0; + } + + case Ref: { + xnd_t next = xnd_ref_next(x, &ctx); + if (next.ptr == NULL) { + seterr(&ctx); + raise_error(); + } + + return mblock_init(&next, data); + } + + case Constr: { + xnd_t next = xnd_constr_next(x, &ctx); + if (next.ptr == NULL) { + seterr(&ctx); + raise_error(); + } + + return mblock_init(&next, data); + } + + case Nominal: { + xnd_t next = xnd_nominal_next(x, &ctx); + if (next.ptr == NULL) { + seterr(&ctx); + raise_error(); + } + + if (t->Nominal.meth->init != NULL) { + if (!t->Nominal.meth->init(&next, x, &ctx)) { + rb_raise(rb_eTypeError, "could not init Nominal type in mblock_init."); + } + return 0; + } + + mblock_init(&next, data); + + if (t->Nominal.meth->constraint != NULL && + !t->Nominal.meth->constraint(&next, &ctx)) { + seterr(&ctx); + raise_error(); + } + + return 0; + } + + case Bool: { + bool b; + + if (data == Qnil) { + rb_raise(rb_eTypeError, + "assigning nil to memory block with non-optional type."); + } + + if (RTEST(data)) { + if (FIXNUM_P(data) || RB_FLOAT_TYPE_P(data)) { + if (NUM2INT(data) == 0) { + b = 0; + } + } + else { + b = 1; + } + } + else { + b = 0; + } + + PACK_SINGLE(x->ptr, b, bool, t->flags); + return 0; + } + + case Int8: { + int8_t temp = (int8_t)get_int(data, INT8_MIN, INT8_MAX); + + PACK_SINGLE(x->ptr, temp, int8_t, t->flags); + return 0; + } + + case Int16: { + int16_t temp = (int16_t)get_int(data, INT16_MIN, INT16_MAX); + + PACK_SINGLE(x->ptr, temp, int16_t, t->flags); + return 0; + } + + case Int32: { + int32_t temp = (int32_t)get_int(data, INT32_MIN, INT32_MAX); + + PACK_SINGLE(x->ptr, temp, int32_t, t->flags); + return 0; + } + + case Int64: { + int64_t temp = get_int(data, INT64_MIN, INT64_MAX); + + PACK_SINGLE(x->ptr, temp, int64_t, t->flags); + return 0; + } + + case Uint8: { + uint8_t temp = (uint8_t)get_uint(data, UINT8_MAX); + PACK_SINGLE(x->ptr, temp, uint8_t, t->flags); + return 0; + } + + case Uint16: { + uint16_t temp = (uint16_t)get_uint(data, UINT16_MAX); + + PACK_SINGLE(x->ptr, temp, uint16_t, t->flags); + return 0; + } + + case Uint32: { + uint32_t temp = (uint32_t)get_uint(data, UINT32_MAX); + + PACK_SINGLE(x->ptr, temp, uint32_t, t->flags); + return 0; + } + + case Uint64: { + uint64_t temp = get_uint(data, UINT64_MAX); + + PACK_SINGLE(x->ptr, temp, uint64_t, t->flags); + return 0; + } + + case Float16: { + rb_raise(rb_eNotImpError, "float16 not implemented."); + } + + case Float32: { + double temp = NUM2DBL(data); + return rb_xnd_pack_float32(temp, (unsigned char*)x->ptr, le(t->flags)); + } + + case Float64: { + double temp = NUM2DBL(data); + return rb_xnd_pack_float64(temp, (unsigned char*)x->ptr, le(t->flags)); + } + + case Complex32: { + rb_raise(rb_eNotImpError, "complex32 not implemented."); + } + + case Complex64: { + double real, imag; + + rb_xnd_get_complex_values(data, &real, &imag); + + rb_xnd_pack_float32(real, (unsigned char*)x->ptr, le(t->flags)); + rb_xnd_pack_float32(imag, (unsigned char*)x->ptr + 4, le(t->flags)); + + return 0; + } + + case Complex128: { + double real, imag; + + rb_xnd_get_complex_values(data, &real, &imag); + + rb_xnd_pack_float64(real, (unsigned char*)x->ptr, le(t->flags)); + rb_xnd_pack_float64(imag, (unsigned char*)x->ptr + 8, le(t->flags)); + + return 0; + } + + case FixedString: { + int64_t codepoints = t->FixedString.size; + int64_t len; + + Check_Type(data, T_STRING); + + /* FIXME: check for unicode string. */ + + switch (t->FixedString.encoding) { + case Ascii: { + if (!string_is_ascii(data)) { + rb_raise(rb_eValueError, "string must be ascii"); + } + + len = RSTRING_LEN(data); + if (len > t->datasize) { + rb_raise(rb_eValueError, + "maxmimum string size in bytes is %" PRIi64, codepoints); + } + + _strncpy(x->ptr, StringValuePtr(data), (size_t)len, (size_t)t->datasize); + return 0; + } + + case Utf8: { + if (!string_is_u8(data)) { + rb_raise(rb_eValueError, "string must be utf-8"); + } + + len = RSTRING_LEN(data); + if (len > t->datasize) { + rb_raise(rb_eValueError, + "maximum string size (in UTF-8 code points) is %" PRIi64, codepoints); + } + + _strncpy(x->ptr, StringValuePtr(data), (size_t)len, (size_t)t->datasize); + return 0; + } + + case Utf16: { + rb_encoding *utf16 = rb_enc_find("UTF-16"); + VALUE b = rb_str_export_to_enc(data, utf16); + + len = RSTRING_LEN(b); + if (len-2 > t->datasize) { + rb_raise(rb_eValueError, + "maximum string size (in UTF-16 code points) is %" PRIi64, codepoints); + } + +#ifdef XND_DEBUG + /* skip byte order mark. */ + assert(len >= 2); +#endif + _strncpy(x->ptr, StringValuePtr(b) + 2, (size_t)(len-2), (size_t)t->datasize); + return 0; + } + + case Utf32: { + VALUE b = rb_str_export_to_enc(data, rb_enc_find("UTF-32")); + + len = RSTRING_LEN(b); + if (len-4 > t->datasize) { + rb_raise(rb_eValueError, + "maximum string size (in UTF-32 code points) is %" PRIi64, codepoints); + } + +#ifdef XND_DEBUG + /* skip byte order mark. */ + assert(len >= 4); +#endif + _strncpy(x->ptr, StringValuePtr(b)+4, (size_t)(len-4), (size_t)t->datasize); + return 0; + } + + case Ucs2: { + rb_raise(rb_eNotImpError, "UCS2 encoding not implemented."); + } + + default: { + rb_raise(rb_eRuntimeError, "invaling string encoding."); + } + } + } + + case FixedBytes: { + int64_t size = t->FixedBytes.size; + int64_t len; + + Check_Type(data, T_STRING); + + if (!string_is_ascii(data)) { + rb_raise(rb_eTypeError, "String must be ASCII encoded for FixedBytes."); + } + + len = RSTRING_LEN(data); + + if (len > size) { + rb_raise(rb_eValueError, "maximum number of bytes in string is %", PRIi64, size); + } + + _strncpy(x->ptr, StringValuePtr(data), (size_t)len, (size_t)size); + + return 0; + } + + case String: { + size_t size; + const char *cp; + char *s; + + Check_Type(data, T_STRING); + + cp = StringValuePtr(data); + size = RSTRING_LEN(data); + s = ndt_strdup(cp, &ctx); + if (s == NULL) { + seterr(&ctx); + raise_error(); + } + + if (XND_POINTER_DATA(x->ptr)) { + ndt_free(XND_POINTER_DATA(x->ptr)); + } + + XND_POINTER_DATA(x->ptr) = s; + return 0; + } + + case Bytes: { + size_t size; + char *cp, *s; + + Check_Type(data, T_STRING); + + size = RSTRING_LEN(data); + cp = StringValuePtr(data); + + s = ndt_aligned_calloc(t->Bytes.target_align, size); + if (s == NULL) { + rb_raise(rb_eNoMemError, "no memory for allocating bytes."); + } + + memcpy(s, cp, size); + + if (XND_BYTES_DATA(x->ptr)) { + ndt_aligned_free(XND_BYTES_DATA(x->ptr)); + } + + XND_BYTES_SIZE(x->ptr) = size; + XND_BYTES_DATA(x->ptr) = (uint8_t *)s; + + return 0; + } + + case Categorical: { + int64_t k; + + if (RB_TYPE_P(data, T_TRUE) || RB_TYPE_P(data, T_FALSE)) { + int temp = RTEST(data); + + for (k = 0; k < t->Categorical.ntypes; k++) { + if (t->Categorical.types[k].tag == ValBool && + temp == t->Categorical.types[k].ValBool) { + PACK_SINGLE(x->ptr, k, int64_t, t->flags); + return 0; + } + } + } + else if (RB_TYPE_P(data, T_FIXNUM)) { + int64_t temp = get_int(data, INT64_MIN, INT64_MAX); + + for (k = 0; k < t->Categorical.ntypes; k++) { + if (t->Categorical.types[k].tag == ValInt64 && + temp == t->Categorical.types[k].ValInt64) { + PACK_SINGLE(x->ptr, k, int64_t, t->flags); + return 0; + } + } + } + else if (RB_TYPE_P(data, T_FLOAT)) { + double temp = NUM2DBL(data); + + for (k = 0; k < t->Categorical.ntypes; k++) { + if (t->Categorical.types[k].tag == ValFloat64 && + temp == t->Categorical.types[k].ValFloat64) { + PACK_SINGLE(x->ptr, k, int64_t, t->flags); + return 0; + } + } + } + else if (RB_TYPE_P(data, T_STRING)) { + const char *temp = StringValuePtr(data); + + for (k = 0; k < t->Categorical.ntypes; k++) { + if (t->Categorical.types[k].tag == ValString && + strcmp(temp, t->Categorical.types[k].ValString) == 0) { + PACK_SINGLE(x->ptr, k, int64_t, t->flags); + return 0; + } + } + } + + for (k = 0; k < t->Categorical.ntypes; k++) { + if (t->Categorical.types[k].tag == ValNA) { + PACK_SINGLE(x->ptr, k, int64_t, t->flags); + return 0; + } + } + + rb_raise(rb_eValueError, "category not found."); + } + + case Char: { + rb_raise(rb_eNotImpError, "'Char' type semantics need to be defined."); + } + + case Module: { + rb_raise(rb_eNotImpError, "'Module' type not implemented."); + } + + /* NOT REACHED: intercepted by ndt_is_abstract(). */ + case AnyKind: case SymbolicDim: case EllipsisDim: case Typevar: + case ScalarKind: case SignedKind: case UnsignedKind: case FloatKind: + case ComplexKind: case FixedStringKind: case FixedBytesKind: + case Function: + rb_raise(rb_eArgError, "unexpected abstract type."); + } +} + +/* Create mblock from NDT type. + * + * @param type - NDT Ruby object. + * @param data - Data as a Ruby object. + */ +static VALUE +mblock_from_typed_value(VALUE type, VALUE data) +{ + VALUE mblock; + MemoryBlockObject *mblock_p; + + mblock = mblock_empty(type); + GET_MBLOCK(mblock, mblock_p); + mblock_init(&mblock_p->xnd->master, data); + + return mblock; +} + +/****************************************************************************/ +/* xnd object */ +/****************************************************************************/ + +typedef struct XndObject { + VALUE mblock; /* owner of the primary type and memory block */ + VALUE type; /* owner of the current type. lives and dies with this obj. */ + xnd_t xnd; /* typed view, does not own anything */ +} XndObject; + +#define XND(xnd_p) (&(((XndObject *)xnd_p)->xnd)) +#define XND_CHECK_TYPE(xnd) (CLASS_OF(xnd) == cXND) +#define GET_XND(obj, xnd_p) do { \ + TypedData_Get_Struct((obj), XndObject, \ + &XndObject_type, (xnd_p)); \ + } while (0) +#define MAKE_XND(klass, xnd_p) TypedData_Make_Struct(klass, XndObject, \ + &XndObject_type, xnd_p) +#define WRAP_XND(klass, xnd_p) TypedData_Wrap_Struct(klass, &XndObject_type, xnd_p) + +static VALUE XND_size(VALUE self); + +/* Allocate an XndObject and return wrapped in a Ruby object. */ +static VALUE +XndObject_alloc(void) +{ + XndObject *xnd; + + xnd = ZALLOC(XndObject); + + xnd->mblock = 0; + xnd->type = 0; + xnd->xnd.bitmap.data = NULL; + xnd->xnd.bitmap.size = 0; + xnd->xnd.bitmap.next = NULL; + xnd->xnd.index = 0; + xnd->xnd.type = NULL; + xnd->xnd.ptr = NULL; + + return WRAP_XND(cXND, xnd); +} + +/* Mark Ruby objects within XndObject. */ +static void +XndObject_dmark(void *self) +{ + XndObject *xnd = (XndObject*)self; + + rb_gc_mark(xnd->type); + rb_gc_mark(xnd->mblock); +} + +static void +XndObject_dfree(void *self) +{ + XndObject *xnd = (XndObject*)self; + + rb_xnd_gc_guard_unregister(xnd); + xfree(xnd); +} + +static size_t +XndObject_dsize(const void *self) +{ + return sizeof(XndObject); +} + +static const rb_data_type_t XndObject_type = { + .wrap_struct_name = "XndObject", + .function = { + .dmark = XndObject_dmark, + .dfree = XndObject_dfree, + .dsize = XndObject_dsize, + .reserved = {0,0}, + }, + .parent = 0, + .flags = RUBY_TYPED_FREE_IMMEDIATELY, +}; + +static void +XND_from_mblock(XndObject *xnd_p, VALUE mblock) +{ + MemoryBlockObject *mblock_p; + + GET_MBLOCK(mblock, mblock_p); + + xnd_p->mblock = mblock; + xnd_p->type = mblock_p->type; + xnd_p->xnd = mblock_p->xnd->master; +} + +/* Allocator for RubyXND object. Called by Ruby before initialize. */ +static VALUE +RubyXND_allocate(VALUE klass) +{ + XndObject *xnd; + + xnd = ZALLOC(XndObject); + + xnd->mblock = 0; + xnd->type = 0; + xnd->xnd.bitmap.data = NULL; + xnd->xnd.bitmap.size = 0; + xnd->xnd.bitmap.next = NULL; + xnd->xnd.index = 0; + xnd->xnd.type = NULL; + xnd->xnd.ptr = NULL; + + return WRAP_XND(klass, xnd); +} + +/* Initialize a RubyXND object. */ +static VALUE +RubyXND_initialize(VALUE self, VALUE type, VALUE data) +{ + VALUE mblock; + XndObject *xnd_p; + + mblock = mblock_from_typed_value(type, data); + GET_XND(self, xnd_p); + + XND_from_mblock(xnd_p, mblock); + rb_xnd_gc_guard_register(xnd_p, mblock); + +#ifdef XND_DEBUG + assert(XND(xnd_p)->type); + assert(XND(xnd_p)->ptr); +#endif + + return self; +} + +static size_t +XND_get_size(VALUE xnd) +{ + size_t size; + int state; + VALUE rb_size; + + rb_size = rb_protect(XND_size, xnd, &state); + size = state ? 0 : NUM2LL(rb_size); + + return size; +} + +/*************************** object properties ********************************/ + +/* Return the ndtypes object of this xnd object. */ +static VALUE +XND_type(VALUE self) +{ + XndObject *xnd_p; + + GET_XND(self, xnd_p); + + + return xnd_p->type; +} + +static VALUE +_XND_value(const xnd_t * const x, const int64_t maxshape) +{ + NDT_STATIC_CONTEXT(ctx); + const ndt_t * const t = x->type; + +#ifdef XND_DEBUG + assert(t); + assert(x); +#endif + + if (!ndt_is_concrete(t)) { + rb_raise(rb_eTypeError, "type must be concrete for returning value."); + } + + /* bitmap access needs linear index. */ + if (xnd_is_na(x)) { + return Qnil; + } + + switch (t->tag) { + case FixedDim: { + VALUE array, v; + int64_t shape, i; + + shape = t->FixedDim.shape; + if (shape > maxshape) { + shape = maxshape; + } + + array = array_new(shape); + + for (i = 0; i < shape; i++) { + if (i == maxshape-1) { + rb_ary_store(array, i, xnd_ellipsis()); + break; + } + + const xnd_t next = xnd_fixed_dim_next(x, i); + v = _XND_value(&next, maxshape); + rb_ary_store(array, i, v); + } + + return array; + } + + case VarDim: { + VALUE array, v; + int64_t start, step, shape; + int64_t i; + + shape = ndt_var_indices(&start, &step, t, x->index, &ctx); + if (shape < 0) { + seterr(&ctx); + raise_error(); + } + if (shape > maxshape) { + shape = maxshape; + } + + array = array_new(shape); + + for (i = 0; i < shape; i++) { + if (i == maxshape-1) { + rb_ary_store(array, i, xnd_ellipsis()); + break; + } + + xnd_t next = xnd_var_dim_next(x, start, step, i); + v = _XND_value(&next, maxshape); + rb_ary_store(array, i, v); + } + + return array; + } + + case Tuple: { + VALUE tuple, v; + int64_t shape, i; + + shape = t->Tuple.shape; + if (shape > maxshape) { + shape = maxshape; + } + + tuple = array_new(shape); + + for (i = 0; i < shape; i++) { + if (i == maxshape) { + rb_ary_store(tuple, i, xnd_ellipsis()); + break; + } + + const xnd_t next = xnd_tuple_next(x, i, &ctx); + if (next.ptr == NULL) { + seterr(&ctx); + raise_error(); + } + + v = _XND_value(&next, maxshape); + rb_ary_store(tuple, i, v); + } + + return tuple; + } + + case Record: { + VALUE hash, v; + int64_t shape, i; + + shape = t->Record.shape; + if (shape > maxshape) { + shape = maxshape; + } + + hash = rb_hash_new(); + + for (i = 0; i < shape; ++i) { + if (i == maxshape - 1) { + rb_hash_aset(hash, xnd_ellipsis(), xnd_ellipsis()); + break; + } + + xnd_t next = xnd_record_next(x, i, &ctx); + if (next.ptr == NULL) { + seterr(&ctx); + raise_error(); + } + + v = _XND_value(&next, maxshape); + rb_hash_aset(hash, rb_str_new2(t->Record.names[i]), v); + } + + return hash; + } + + case Ref: { + xnd_t next = xnd_ref_next(x, &ctx); + if (next.ptr == NULL) { + seterr(&ctx); + raise_error(); + } + + return _XND_value(&next, maxshape); + } + + case Constr: { + xnd_t next = xnd_constr_next(x, &ctx); + if (next.ptr == NULL) { + seterr(&ctx); + raise_error(); + } + + return _XND_value(&next, maxshape); + } + + case Nominal: { + xnd_t next = xnd_nominal_next(x, &ctx); + if (next.ptr == NULL) { + seterr(&ctx); + raise_error(); + } + + if (t->Nominal.meth->repr != NULL) { + return t->Nominal.meth->repr(&next, &ctx); + } + + return _XND_value(&next, maxshape); + } + + case Bool: { + bool temp; + UNPACK_SINGLE(temp, x->ptr, bool, t->flags); + return INT2BOOL(temp); + } + + case Int8: { + int8_t temp; + UNPACK_SINGLE(temp, x->ptr, int8_t, t->flags); + return INT2NUM(temp); + } + + case Int16: { + int16_t temp; + UNPACK_SINGLE(temp, x->ptr, int16_t, t->flags); + return INT2NUM(temp); + } + + case Int32: { + int32_t temp; + UNPACK_SINGLE(temp, x->ptr, int32_t, t->flags); + return INT2NUM(temp); + } + + case Int64: { + int64_t temp; + UNPACK_SINGLE(temp, x->ptr, int64_t, t->flags); + return LL2NUM(temp); + } + + case Uint8: { + uint8_t temp; + UNPACK_SINGLE(temp, x->ptr, uint8_t, t->flags); + return UINT2NUM(temp); + } + + case Uint16: { + uint16_t temp; + UNPACK_SINGLE(temp, x->ptr, uint16_t, t->flags); + return UINT2NUM(temp); + } + + case Uint32: { + uint32_t temp; + UNPACK_SINGLE(temp, x->ptr, uint32_t, t->flags); + return ULL2NUM(temp); + } + + case Uint64: { + uint64_t temp; + UNPACK_SINGLE(temp, x->ptr, uint64_t, t->flags); + return ULL2NUM(temp); + } + + case Float16: { + rb_raise(rb_eNotImpError, "float16 is not implemented."); + } + + case Float32: { + float temp = 0.0; + + rb_xnd_unpack_float32(&temp, (unsigned char*)x->ptr, le(t->flags)); + return DBL2NUM(temp); + } + + case Float64: { + double temp = 0.0; + + rb_xnd_unpack_float64(&temp, (unsigned char*)x->ptr, le(t->flags)); + return DBL2NUM(temp); + } + + case Complex32: { + rb_raise(rb_eNotImpError, "complex32 not implemented."); + } + + case Complex64: { + float real = 0.0, imag = 0.0; + + rb_xnd_unpack_float32(&real, (unsigned char*)x->ptr, le(t->flags)); + rb_xnd_unpack_float32(&imag, (unsigned char*)x->ptr+4, le(t->flags)); + + return rb_complex_new(DBL2NUM(real), DBL2NUM(imag)); + } + + case Complex128: { + double real = 0.0, imag = 0.0; + + rb_xnd_unpack_float64(&real, (unsigned char*)x->ptr, le(t->flags)); + rb_xnd_unpack_float64(&imag, (unsigned char*)x->ptr+8, le(t->flags)); + + return rb_complex_new(DBL2NUM(real), DBL2NUM(imag)); + } + + case FixedString: { + int64_t codepoints = t->FixedString.size; + + switch (t->FixedString.encoding) { + case Ascii: { + codepoints = u8_skip_trailing_zero((uint8_t *)x->ptr, codepoints); + return rb_usascii_str_new(x->ptr, codepoints); + } + + case Utf8: { + codepoints = u8_skip_trailing_zero((uint8_t *)x->ptr, codepoints); + return rb_utf8_str_new(x->ptr, codepoints); + } + + case Utf16: { + rb_encoding *utf16 = rb_enc_find("UTF-16"); + codepoints = u16_skip_trailing_zero((uint16_t *)x->ptr, codepoints); + + return rb_enc_str_new(x->ptr, codepoints*2, utf16); + } + + case Utf32: { + rb_encoding *utf32 = rb_enc_find("UTF-32"); + codepoints = u32_skip_trailing_zero((uint32_t *)x->ptr, codepoints); + + return rb_enc_str_new(x->ptr, codepoints*4, utf32); + } + + case Ucs2: { + rb_raise(rb_eNotImpError, "UCS2 encoding not implemented."); + } + + default: { + rb_raise(rb_eRuntimeError, "invalid string encoding."); + } + } + } + + case FixedBytes: { + return bytes_from_string_and_size(x->ptr, t->FixedBytes.size); + } + + case String: { + const char *s = XND_POINTER_DATA(x->ptr); + size_t size = s ? strlen(s) : 0; + + return rb_utf8_str_new(s, size); + } + + case Bytes: { + char *s = (char *)XND_BYTES_DATA(x->ptr); + size_t size = s ? strlen(s) : 0; + + return bytes_from_string_and_size(s, size); + } + + case Categorical: { + int64_t k; + + UNPACK_SINGLE(k, x->ptr, int64_t, t->flags); + + switch(t->Categorical.types[k].tag) { + case ValBool: { + bool temp = t->Categorical.types[k].ValBool; + return INT2BOOL(temp); + } + + case ValInt64: { + int64_t temp = t->Categorical.types[k].ValInt64; + return LL2NUM(temp); + } + + case ValFloat64: { + double temp = t->Categorical.types[k].ValFloat64; + return DBL2NUM(temp); + } + + case ValString: { + const char *temp = t->Categorical.types[k].ValString; + return rb_str_new2(temp); + } + + case ValNA: { + return Qnil; + } + + default: { + rb_raise(rb_eRuntimeError, "unexpected category tag."); + } + } + } + + case Char: { + rb_raise(rb_eNotImpError, "char semantics need to be defined."); + } + + case Module: { + rb_raise(rb_eNotImpError, "'Module' type not implemented yet."); + } + + /* NOT REACHED: intercepted by ndt_is_abstract(). */ + case AnyKind: case SymbolicDim: case EllipsisDim: case Typevar: + case ScalarKind: case SignedKind: case UnsignedKind: case FloatKind: + case ComplexKind: case FixedStringKind: case FixedBytesKind: + case Function: + rb_raise(rb_eArgError, "unexpected abstract type."); + } + + rb_raise(rb_eRuntimeError, "invalid type tag %d.", t->tag); +} + +/* Return the value of this xnd object. Aliased to to_a. */ +static VALUE +XND_value(VALUE self) +{ + XndObject *xnd_p; + + GET_XND(self, xnd_p); + + return _XND_value(XND(xnd_p), INT64_MAX); +} + +/*************************** slicing functions ********************************/ + +#define KEY_INDEX 1 +#define KEY_FIELD 2 +#define KEY_SLICE 4 +#define KEY_ERROR 128 + +/* + @param src_p Pointer to the source XND object from which view is being created. + @param x Metadata for creating the view. + */ +static VALUE +RubyXND_view_move_type(XndObject *src_p, xnd_t *x) +{ + XndObject *view_p; + VALUE type, view; + + type = rb_ndtypes_move_subtree(src_p->type, (ndt_t *)x->type); + view = XndObject_alloc(); + GET_XND(view, view_p); + + view_p->mblock = src_p->mblock; + view_p->type = type; + view_p->xnd = *x; + + rb_xnd_gc_guard_register(view_p, view_p->mblock); + + return view; +} + +/* Convert a single Ruby object index into a form that XND can understand. + + @param *key + @param obj + @param size Size of object. 1 for all scalars and the actual size otherwise. +*/ +static uint8_t +convert_single(xnd_index_t *key, VALUE obj, size_t size) +{ + if (RB_TYPE_P(obj, T_FIXNUM)) { + int64_t i = NUM2LL(obj); + + key->tag = Index; + key->Index = i; + + return KEY_INDEX; + } + else if (RB_TYPE_P(obj, T_STRING)) { + const char *s = StringValuePtr(obj); + + key->tag = FieldName; + key->FieldName = s; + + return KEY_FIELD; + } + else if (CLASS_OF(obj) == rb_cRange) { + if (size == 0) { + rb_raise(rb_eIndexError, "Cannot use Range on this type."); + }; + + long long begin, end, step; + + rb_range_unpack(obj, &begin, &end, &step, size); + key->tag = Slice; + key->Slice.start = begin; + key->Slice.stop = end; + key->Slice.step = step; + + return KEY_SLICE; + } + // case of INF (infinite range syntax sugar) + else if (RB_TYPE_P(obj, T_FLOAT)) { + double value = RFLOAT_VALUE(obj); + + if (isinf(value)) { + key->tag = Slice; + key->Slice.start = 0; + key->Slice.stop = INT64_MAX; + key->Slice.step = 1; + + return KEY_SLICE; + } + else { + rb_raise(rb_eArgError, "wrong object specified in index."); + } + } + else { + rb_raise(rb_eArgError, "wrong object specified in index."); + } +} + +static uint8_t +convert_key(xnd_index_t *indices, int *len, int argc, VALUE *argv, size_t size) +{ + uint8_t flags = 0; + VALUE x; + + if (argc > 1) { + if (argc > NDT_MAX_DIM) { + rb_raise(rb_eArgError, "too many indices %d.", argc); + } + + for (unsigned int i = 0; i < argc; i++) { + x = argv[i]; + flags |= convert_single(indices+i, x, size); + if (flags & KEY_ERROR) { + return KEY_ERROR; + } + } + + *len = argc; + return flags; + } + else if (argc == 1 && RB_TYPE_P(argv[0], T_ARRAY)) { // args as an array + *len = RARRAY_LEN(argv[0]); + + for (int i = 0; i < *len; i++) { + VALUE args[1] = { INT2NUM(i) }; + flags |= convert_single(indices+i, rb_ary_aref(1, args, argv[0]), size); + if (flags & KEY_ERROR) { + return KEY_ERROR; + } + } + return flags; + } + + *len = 1; + return convert_single(indices, argv[0], size); +} + +/* Implement the #[] Ruby method. */ +static VALUE +XND_array_aref(int argc, VALUE *argv, VALUE self) +{ + NDT_STATIC_CONTEXT(ctx); + xnd_index_t indices[NDT_MAX_DIM]; + xnd_t x; + int len; + uint8_t flags; + XndObject *xnd_p; + size_t size; + + if (argc == 0) { + rb_raise(rb_eArgError, "expected atleast one argument for #[]."); + } + + GET_XND(self, xnd_p); + size = XND_get_size(self); + + flags = convert_key(indices, &len, argc, argv, size); + if (flags & KEY_ERROR) { + rb_raise(rb_eArgError, "something is wrong with the array key."); + } + + x = xnd_subscript(&xnd_p->xnd, indices, len, &ctx); + if (x.ptr == NULL) { + seterr(&ctx); + raise_error(); + } + + return RubyXND_view_move_type(xnd_p, &x); +} + +/* Compare between data of an XND object and Ruby object. */ +static VALUE +convert_compare(VALUE self, VALUE other) +{ + VALUE vcmp = XND_value(self); + + return rb_funcall(vcmp, rb_intern("=="), 1, other); +} + +/* Implementation for #== method. + + @param other Other Ruby object to compare with. + @return VALUE [TrueClass|FalseClass] +*/ +static VALUE +XND_eqeq(VALUE self, VALUE other) +{ + NDT_STATIC_CONTEXT(ctx); + XndObject *left_p, *right_p; + int r; + + if (!XND_CHECK_TYPE(other)) { + return convert_compare(self, other); + } + + GET_XND(self, left_p); + GET_XND(other, right_p); + + r = xnd_equal(XND(left_p), XND(right_p), &ctx); + + if (r == 1) { + return Qtrue; + } + else { + return Qfalse; + } +} + +/* Implement Ruby spaceship operator. */ +static VALUE +XND_spaceship(VALUE self, VALUE other) +{ + rb_raise(rb_eNotImpError, "spaceship not implemented yet."); + + return Qnil; +} + +/* XND#strict_equal */ +static VALUE +XND_strict_equal(VALUE self, VALUE other) +{ + NDT_STATIC_CONTEXT(ctx); + XndObject *left_p, *right_p; + int r; + + if (!XND_CHECK_TYPE(other)) { + rb_raise(rb_eArgError, "argument type has to be XND."); + } + + GET_XND(self, left_p); + GET_XND(other, right_p); + + r = xnd_strict_equal(XND(left_p), XND(right_p), &ctx); + if (r < 0) { + seterr(&ctx); + raise_error(); + } + + if (r) { + return Qtrue; + } + else { + return Qfalse; + } +} + +static size_t +_XND_size(const xnd_t *x) +{ + NDT_STATIC_CONTEXT(ctx); + const ndt_t *t = x->type; + + if (!ndt_is_concrete(t)) { + rb_raise(rb_eTypeError, "NDT must be concrete to get size."); + } + + if (t->ndim > 0 && ndt_is_optional(t)) { + rb_raise(rb_eNotImpError, "optional dimensions are not supported."); + } + + if (xnd_is_na(x)) { + return 0; + } + + switch (t->tag) { + case FixedDim: { + return safe_downcast(t->FixedDim.shape); + } + + case VarDim: { + int64_t start, step, shape; + + shape = ndt_var_indices(&start, &step, t, x->index, &ctx); + if (shape < 0) { + seterr(&ctx); + raise_error(); + } + + return safe_downcast(shape); + } + + case Tuple: { + return safe_downcast(t->Tuple.shape); + } + + case Record: { + return safe_downcast(t->Record.shape); + } + + case Ref: { + const xnd_t next = xnd_ref_next(x, &ctx); + if (next.ptr == NULL) { + seterr(&ctx); + raise_error(); + } + + return _XND_size(&next); + } + + case Constr: { + const xnd_t next = xnd_constr_next(x, &ctx); + if (next.ptr == NULL) { + seterr(&ctx); + raise_error(); + } + + return _XND_size(&next); + } + + case Nominal: { + const xnd_t next = xnd_nominal_next(x, &ctx); + if (next.ptr == NULL) { + seterr(&ctx); + raise_error(); + } + + return _XND_size(&next); + } + + default: { + rb_raise(rb_eNoMethodError, "This type has no size method."); + } + } +} + +/* Implement XND#size. */ +static VALUE +XND_size(VALUE self) +{ + XndObject *self_p; + + GET_XND(self, self_p); + return ULL2NUM(_XND_size(XND(self_p))); +} + +/* Implement #[]= */ +static VALUE +XND_array_store(int argc, VALUE *argv, VALUE self) +{ + NDT_STATIC_CONTEXT(ctx); + xnd_index_t indices[NDT_MAX_DIM]; + xnd_t x; + int free_type = 0, ret, len; + uint8_t flags; + XndObject *self_p, *value_p; + MemoryBlockObject *self_mblock_p; + size_t size; + VALUE value; + + if (argc < 2) { + rb_raise(rb_eArgError, "wrong number of arguments (given %d expected atleast 2)." + , argc); + } + + GET_XND(self, self_p); + + size = XND_get_size(self); + flags = convert_key(indices, &len, argc-1, argv, size); + if (flags & KEY_ERROR) { + rb_raise(rb_eIndexError, "wrong kind of key in []="); + } + + if (flags & KEY_SLICE) { + x = xnd_multikey(&self_p->xnd, indices, len, &ctx); + free_type = 1; + } + else { + x = xnd_subtree(&self_p->xnd, indices, len, &ctx); + } + + if (x.ptr == NULL) { + seterr(&ctx); + raise_error(); + } + + value = argv[argc-1]; + + if (XND_CHECK_TYPE(value)) { + GET_XND(value, value_p); + GET_MBLOCK(self_p->mblock, self_mblock_p); + + ret = xnd_copy(&x, XND(value_p), self_mblock_p->xnd->flags, &ctx); + if (ret < 0) { + seterr(&ctx); + raise_error(); + } + } + else { + ret = mblock_init(&x, value); + } + + if (free_type) { + ndt_del((ndt_t *)x.type); + } + + return value; +} + +/* Implement XND#each */ +static VALUE +XND_each(VALUE self) +{ + +} + +/* Implement XND#short_value */ +static VALUE +XND_short_value(VALUE self, VALUE maxshape) +{ + if (maxshape == Qnil) { + return XND_value(self); + } + else { + long max = NUM2LONG(maxshape); + XndObject *self_p; + + GET_XND(self, self_p); + + if (max < 0) { + rb_raise(rb_eArgError, "maxshape must be positive."); + } + + return _XND_value(XND(self_p), max); + } +} + +/*************************** Singleton methods ********************************/ + +static VALUE +XND_s_empty(VALUE klass, VALUE type) +{ + XndObject *self_p; + VALUE self, mblock; + + self = XndObject_alloc(); + GET_XND(self, self_p); + + type = rb_ndtypes_from_object(type); + mblock = mblock_empty(type); + + XND_from_mblock(self_p, mblock); + rb_xnd_gc_guard_register(self_p, mblock); + + return self; +} + +/*************************** C-API ********************************/ + +size_t +rb_xnd_hash_size(VALUE hash) +{ + Check_Type(hash, T_HASH); + + return NUM2ULL(rb_funcall(hash, rb_intern("size"), 0, NULL)); +} + +/* FIXME: Find a better way to access real/imag parts of complex number. + This is too slow. +*/ +int +rb_xnd_get_complex_values(VALUE comp, double *real, double *imag) +{ + Check_Type(comp, T_COMPLEX); + + *real = NUM2DBL(rb_funcall(comp, rb_intern("real"), 0, NULL)); + *imag = NUM2DBL(rb_funcall(comp, rb_intern("imag"), 0, NULL)); + + return 0; +} + +/* Return true if obj is of type XND. */ +int +rb_xnd_check_type(VALUE obj) +{ + return XND_CHECK_TYPE(obj); +} + +/* Return the xnd_t internal object within this Ruby object. */ +const xnd_t * +rb_xnd_const_xnd(VALUE xnd) +{ + XndObject *xnd_p; + + GET_XND(xnd, xnd_p); + + return &((XndObject *)xnd_p)->xnd; +} + +/* Creae a new XND object from xnd_t type. */ +VALUE +rb_xnd_from_xnd(xnd_t *x) +{ + VALUE mblock, xnd; + XndObject *xnd_p; + + mblock = mblock_from_xnd(x); + xnd = XndObject_alloc(); + GET_XND(xnd, xnd_p); + + XND_from_mblock(xnd_p, mblock); + rb_xnd_gc_guard_register(xnd_p, mblock); + + return xnd; +} + +/* Create an XND object of type ndt_t */ +VALUE +rb_xnd_empty_from_type(ndt_t *t) +{ + MemoryBlockObject *mblock_p; + XndObject *xnd_p; + VALUE type, mblock, xnd; + + type = rb_ndtypes_from_type(t); + mblock = mblock_empty(type); + xnd = XndObject_alloc(); + + GET_XND(xnd, xnd_p); + rb_xnd_gc_guard_register(xnd_p, mblock); + + XND_from_mblock(xnd_p, mblock); + + return xnd; +} + +VALUE +rb_xnd_get_type(void) +{ + +} + +/* + * This function handles two common view cases: + * + * a) A pristine view that owns everything, including new memory. + * b) A view that owns its type after xnd_subscript(). + */ +/* VALUE */ +/* rb_xnd_from_xnd_view(xnd_view_t *x) */ +/* { */ +/* if (x->obj == NULL && (x->flags & XND_OWN_ALL) == XND_OWN_ALL) { */ +/* VALUE type = rb_xnd_get_type(); */ +/* } */ +/* } */ + +void Init_ruby_xnd(void) +{ + NDT_STATIC_CONTEXT(ctx); + static int initialized = 0; + + if (!initialized) { + if (xnd_init_float(&ctx) < 0) { + seterr(&ctx); + raise_error(); + } + + if (!ndt_exists()) { + rb_raise(rb_eLoadError, "NDT does not exist."); + } + + initialized = 1; + } + + /* init classes */ + cRubyXND = rb_define_class("RubyXND", rb_cObject); + cXND = rb_define_class("XND", cRubyXND); + cRubyXND_MBlock = rb_define_class_under(cRubyXND, "MBlock", rb_cObject); + mRubyXND_GCGuard = rb_define_module_under(cRubyXND, "GCGuard"); + + /* errors */ + rb_eValueError = rb_define_class("ValueError", rb_eRuntimeError); + + /* initializers */ + rb_define_alloc_func(cRubyXND, RubyXND_allocate); + rb_define_method(cRubyXND, "initialize", RubyXND_initialize, 2); + + /* instance methods */ + rb_define_method(cXND, "type", XND_type, 0); + rb_define_method(cXND, "value", XND_value, 0); + rb_define_method(cXND, "[]", XND_array_aref, -1); + rb_define_method(cXND, "[]=", XND_array_store, -1); + rb_define_method(cXND, "==", XND_eqeq, 1); + // rb_define_method(cXND, "!=", XND_neq, 1); + rb_define_method(cXND, "<=>", XND_spaceship, 1); + rb_define_method(cXND, "strict_equal", XND_strict_equal, 1); + rb_define_method(cXND, "size", XND_size, 0); + rb_define_method(cXND, "short_value", XND_short_value, 1); + + /* iterators */ + rb_define_method(cXND, "each", XND_each, 0); + + /* singleton methods */ + rb_define_singleton_method(cXND, "empty", XND_s_empty, 1); + + /* GC guard */ + rb_xnd_init_gc_guard(); + +#ifdef XND_DEBUG + run_float_pack_unpack_tests(); + rb_define_const(cRubyXND, "XND_DEBUG", Qtrue); +#else + rb_define_const(cRubyXND, "XND_DEBUG", Qnil); +#endif +} diff --git a/ruby/ext/ruby_xnd/ruby_xnd.h b/ruby/ext/ruby_xnd/ruby_xnd.h new file mode 100644 index 0000000..a56f05b --- /dev/null +++ b/ruby/ext/ruby_xnd/ruby_xnd.h @@ -0,0 +1,61 @@ +/* BSD 3-Clause License + * + * Copyright (c) 2018, Quansight and Sameer Deshmukh + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* File containing headers for Ruby XND wrapper. + * + * Author: Sameer Deshmukh (@v0dro) +*/ +#ifndef RUBY_XND_H +#define RUBY_XND_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "ruby.h" +#include "ndtypes.h" +#include "xnd.h" + + size_t rb_xnd_hash_size(VALUE hash); + int rb_xnd_get_complex_values(VALUE comp, double *real, double *imag); + /* Return true if obj is of type XND. */ + int rb_xnd_check_type(VALUE obj); + const xnd_t * rb_xnd_const_xnd(VALUE xnd); + VALUE rb_xnd_empty_from_type(ndt_t *t); + VALUE rb_xnd_from_xnd(xnd_t *x); + + typedef struct XndObject XndObject; + +#ifdef __cplusplus +} +#endif + +#endif /* RUBY_XND_H */ diff --git a/ruby/ext/ruby_xnd/ruby_xnd_internal.h b/ruby/ext/ruby_xnd/ruby_xnd_internal.h new file mode 100644 index 0000000..e5814a2 --- /dev/null +++ b/ruby/ext/ruby_xnd/ruby_xnd_internal.h @@ -0,0 +1,85 @@ +/* BSD 3-Clause License + * + * Copyright (c) 2018, Quansight and Sameer Deshmukh + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + +/* File containing internal declarations for Ruby XND wrapper. + * + * Author: Sameer Deshmukh (@v0dro) +*/ + +#ifndef RUBY_XND_INTERNAL_H +#define RUBY_XND_INTERNAL_H + +//#define XND_DEBUG 1 + +#ifdef XND_DEBUG +#include +#endif + +#include +#include "ruby.h" +#include "ruby/encoding.h" +#include "ruby_ndtypes.h" +#include "ruby_xnd.h" +#include "util.h" +#include "float_pack_unpack.h" + +extern VALUE mRubyXND_GCGuard; + +/* typedefs */ +typedef struct XndObject XndObject; +typedef struct MemoryBlockObject MemoryBlockObject; + +#include "gc_guard.h" + +/* macros */ +#if SIZEOF_LONG == SIZEOF_VOIDP +# define PTR2NUM(x) (LONG2NUM((long)(x))) +# define NUM2PTR(x) ((void*)(NUM2ULONG(x))) +#elif SIZEOF_LONG_LONG == SIZEOF_VOIDP +# define PTR2NUM(x) (LL2NUM((LONG_LONG)(x))) +# define NUM2PTR(x) ((void*)(NUM2ULL(x))) +#else +# error ---->> ruby requires sizeof(void*) == sizeof(long) or sizeof(LONG_LONG) to be compiled. <<---- +#endif + +/* Convert C int 't' to Ruby 'true' or 'false'. */ +#define INT2BOOL(t) (t ? Qtrue : Qfalse) + +#ifdef WORDS_BIGENDIAN +#define IEEE_BIG_ENDIAN_P 1 +#define IEEE_LITTLE_ENDIAN_P NULL +#else +#define IEEE_LITTLE_ENDIAN_P 1 +#define IEEE_BIG_ENDIAN_P NULL +#endif + +#endif /* RUBY_XND_INTERNAL_H */ diff --git a/ruby/ext/ruby_xnd/util.c b/ruby/ext/ruby_xnd/util.c new file mode 100644 index 0000000..c3ba537 --- /dev/null +++ b/ruby/ext/ruby_xnd/util.c @@ -0,0 +1,165 @@ +/* BSD 3-Clause License + * + * Copyright (c) 2018, Quansight and Sameer Deshmukh + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "ruby.h" +#include +#include +#include +#include "ndtypes.h" + +void +raise_error(void) +{ + VALUE exeception = rb_errinfo(); + + rb_set_errinfo(Qnil); + rb_exc_raise(exeception); +} + +void +set_error_info(VALUE err, const char * msg) +{ + rb_set_errinfo(rb_exc_new2(err, msg)); +} + +size_t +safe_downcast(int64_t size) +{ +#if SIZE_MAX < INT64_MAX + if (size > INT32_MAX) { + rb_raise(rb_eSizeError, + "sizes should never exceed INT32_MAX on 32-bit platforms."); + } +#endif + return (size_t)size; +} + +bool +check_invariants(const ndt_t *t) +{ +#if SIZE_MAX < INT64_MAX + return safe_downcast(t->datasize) >= 0; +#else + (void)t; + return 1; +#endif +} + +VALUE +array_new(int64_t size) +{ +#if SIZE_MAX < INT64_MAX + size_t n = safe_downcast(size); + return n < 0 ? NULL : rb_ary_new2(n); +#else + return rb_ary_new2(size); +#endif +} + +VALUE +bytes_from_string_and_size(const char *str, int64_t size) +{ +#if SIZE_MAX < INT64_MAX + size_t n = safe_downcast(size); + return n < 0 ? NULL : rb_str_new(str, n); +#else + return rb_str_new(str, size); +#endif +} + +long long +mod(long long a, long long b) +{ + long long r = a % b; + return r < 0 ? r + b : r; +} + +void +rb_range_unpack(VALUE range, long long *begin, long long *end, long long *step, size_t size) +{ + /* FIXME: As of 27 Aug. 2018 Ruby trunk implements step as a property of + Range and XND will support it as and when it is available. Maybe for + now we can implement a #step iterator in a separate method. + */ + *step = 1; + VALUE rb_begin = rb_funcall(range, rb_intern("begin"), 0, NULL); + VALUE rb_end = rb_funcall(range, rb_intern("end"), 0, NULL); + int exclude_end = RTEST(rb_funcall(range, rb_intern("exclude_end?"), 0, NULL)); + + if (RB_TYPE_P(rb_begin, T_FLOAT)) { + double value = RFLOAT_VALUE(rb_begin); + + if (isinf(value)) { + *begin = 0; + } + } + else { + long long temp = NUM2LL(rb_begin); + + if (temp < 0) { /* if negative index map to positive. */ + temp = mod(temp, (long long)size); + } + + *begin = temp; + } + + if (RB_TYPE_P(rb_end, T_FLOAT)) { + double value = RFLOAT_VALUE(rb_end); + + if (isinf(value)) { + *end = INT64_MAX; + return; + } + } + else { + long long temp = NUM2LL(rb_end); + + if (temp < 0) { /* if negative index map to ppositive. */ + temp = mod(temp, (long long)size); + } + + *end = temp; + } + + /* a[0..0] in Ruby returns the 0th index. + a[0...0] in Ruby returns empty array like a[0:0] in Python. + libxnd does not include the last index by default. + */ + if (!exclude_end) { + *end += 1; + } +} + +int +ndt_exists(void) +{ + return RTEST(rb_const_get(rb_cObject, rb_intern("NDT"))); +} diff --git a/ruby/ext/ruby_xnd/util.h b/ruby/ext/ruby_xnd/util.h new file mode 100644 index 0000000..05f1cc3 --- /dev/null +++ b/ruby/ext/ruby_xnd/util.h @@ -0,0 +1,61 @@ +/* BSD 3-Clause License + * + * Copyright (c) 2018, Quansight and Sameer Deshmukh + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* Utility functions for Ruby XND wrapper. + * + * Author: Sameer Deshmukh (@v0dro) + */ + +#ifndef UTIL_H +#define UTIL_H + +#include "ruby.h" +#include +#include +#include +#include "ndtypes.h" + +/* Raise an error stored in $!. Clears it before raising. */ +void raise_error(void); + +void set_error_info(VALUE err, const char * msg); + +size_t safe_downcast(int64_t size); + +bool check_invariants(const ndt_t *t); + +VALUE array_new(int64_t size); + +VALUE bytes_from_string_and_size(const char *str, int64_t size); + +int ndt_exists(void); + +#endif /* UTIL_H */ diff --git a/ruby/ext/ruby_xnd/xnd b/ruby/ext/ruby_xnd/xnd new file mode 160000 index 0000000..31ec2d8 --- /dev/null +++ b/ruby/ext/ruby_xnd/xnd @@ -0,0 +1 @@ +Subproject commit 31ec2d8501af442e1a5054d9531a5e2cefb242cb diff --git a/ruby/lib/xnd.rb b/ruby/lib/xnd.rb new file mode 100644 index 0000000..18cdbbb --- /dev/null +++ b/ruby/lib/xnd.rb @@ -0,0 +1,314 @@ +# BSD 3-Clause License +# +# Copyright (c) 2018, Quansight and Sameer Deshmukh +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +require 'ndtypes' +begin + require "ruby_xnd.so" +rescue LoadError + require 'ruby_xnd/ruby_xnd.so' +end + +require 'xnd/version' + +INF = Float::INFINITY + +class XND + class Ellipsis + def to_s + "..." + end + + def == other + other.is_a?(XND::Ellipsis) ? true : false + end + end +end + +class XND < RubyXND + # Immutable array type used when specifying XND tuples without wanting to + # specify the type. It is highly recommended to simply specify the type + # and use Ruby Arrays for specifying the data. + # + # If you call #to_a or #value on XND after specifying data as XND::T, note + # that XND will return the data in the form of Ruby Arrays. + # + # The sole purpose for the existence of this class is the facilitation of + # type inference. Before passing to the C interface, all instaces of XND::T + # will be converted into Ruby Arrays. + # + # @example + # + # x = XND.new XND::T.new([]) + # #=> XND([], type: ()) + class T + include Enumerable + attr_reader :data + + def initialize *args, &block + @data = args + end + + def each &block + @data.each(&block) + end + + def map &block + @data.map(&block) + end + + def map! &block + @data.map!(&block) + end + + def [] *index + @data[index] + end + end + + MAX_DIM = NDTypes::MAX_DIM + + # Methods for type inference. + module TypeInference + class << self + # Infer the type of a Ruby value. In general, types should be explicitly + # specified. + def type_of value, dtype: nil + NDTypes.new actual_type_of(value, dtype: dtype) + end + + def actual_type_of value, dtype: nil + ret = nil + if value.is_a?(Array) + data, shapes = data_shapes value + opt = data.include? nil + + if dtype.nil? + if data.nil? + dtype = 'float64' + else + dtype = choose_dtype(data) + + data.each do |x| + if !x.nil? + t = actual_type_of(x) + if t != dtype + raise ValueError, "dtype mismatch: have t=#{t} and dtype=#{dtype}" + end + end + end + end + + dtype = '?' + dtype if opt + end + + t = dtype + + var = shapes.map { |lst| lst.uniq.size > 1 || nil }.any? + shapes.each do |lst| + opt = lst.include? nil + lst.map! { |x| x.nil? ? 0 : x } + t = add_dim(opt: opt, shapes: lst, typ: t, use_var: var) + end + + ret = t + elsif !dtype.nil? + raise TypeError, "dtype argument is only supported for Arrays." + elsif value.is_a? Hash + if value.keys.all? { |k| k.is_a?(String) } + ret = "{" + value.map { |k, v| "#{k} : #{actual_type_of(v)}"}.join(", ") + "}" + else + raise ValueError, "all hash keys must be String." + end + elsif value.is_a? XND::T # tuple + ret = "(" + value.map { |v| actual_type_of(v) }.join(",") + ")" + elsif value.nil? + ret = '?float64' + elsif value.is_a? Float + ret = 'float64' + elsif value.is_a? Complex + ret = 'complex128' + elsif value.is_a? Integer + ret = 'int64' + elsif value.is_a? String + ret = value.encoding == Encoding::ASCII_8BIT ? 'bytes' : 'string' + elsif value.is_a?(TrueClass) || value.is_a?(FalseClass) + ret = 'bool' + else + raise ArgumentError, "cannot infer data type for: #{value}" + end + + ret + end + + def accumulate arr + result = [] + arr.inject(0) do |memo, a| + result << memo + a + memo + a + end + + result + end + + # Construct a new dimension type based on the list of 'shapes' that + # are present in a dimension. + def add_dim *args, opt: false, shapes: nil, typ: nil, use_var: false + if use_var + offsets = [0] + accumulate(shapes) + return "#{opt ? '?' : ''}var(offsets=#{offsets}) * #{typ}" + else + n = shapes.uniq.size + shape = (n == 0 ? 0 : shapes[0]) + return "#{shape} * #{typ}" + end + end + + # Internal function for extracting the data and dimensions of nested arrays. + def search level, data, acc, minmax + raise(ValueError, "too many dimensions: #{level}") if level > MAX_DIM + + current = acc[level] + if data.nil? + current << data + elsif data.is_a?(Array) + current << data.size + next_level = level + 1 + minmax[1] = [next_level, minmax[1]].max + + if !data + minmax[0] = [next_level, minmax[0]].min + else + data.each do |item| + search level+1, item, acc, minmax + end + end + else + acc[minmax[1]] << data + minmax[0] = [level, minmax[0]].min + end + end + + # Extract array data and dimension shapes from a nested Array. The + # Array may contain nil for missing data or dimensions. + # + # @example + # data_shapes [[0, 1], [2, 3, 4], [5, 6, 7, 8]] + # #=> [[0, 1, 2, 3, 4, 5, 6, 7, 8], [[2, 3, 4], [3]]] + # # ^ ^ ^ + # # | | `--- ndim=2: single shape 3 + # # | `-- ndim=1: shapes 2, 3, 4 + # # `--- ndim=0: extracted array data + def data_shapes tree + acc = Array.new(MAX_DIM + 1) { [] } + min_level = MAX_DIM + 1 + max_level = 0 + minmax = [min_level, max_level] + + search max_level, tree, acc, minmax + + min_level = minmax[0] + max_level = minmax[1] + + if acc[max_level] && acc[max_level].all? { |a| a.nil? } + # min_level is not set in this special case. Hence the check. + elsif min_level != max_level + raise ValueError, "unbalanced tree: min depth #{min_level} and max depth #{max_level}" + end + + data = acc[max_level] + shapes = acc[0...max_level].reverse + + [data, shapes] + end + + def choose_dtype array + array.each do |x| + return actual_type_of(x) if !x.nil? + end + + 'float64' + end + + def convert_xnd_t_to_ruby_array data + if data.is_a?(XND::T) + data.map! do |d| + convert_xnd_t_to_ruby_array d + end + + return data.data + elsif data.is_a? Hash + data.each do |k, v| + data[k] = convert_xnd_t_to_ruby_array v + end + elsif data.is_a? Array + data.map! do |d| + convert_xnd_t_to_ruby_array d + end + else + return data + end + end + end + end + + def initialize data, type: nil, dtype: nil, levels: nil, typedef: nil, dtypedef: nil + if [type, dtype, levels, typedef, dtypedef].count(nil) < 2 + raise ArgumentError, "the 'type', 'dtype', 'levels' and 'typedef' arguments are " + "mutually exclusive." + end + + if type + type = NDTypes.new(type) if type.is_a? String + elsif dtype + type = TypeInference.type_of data, dtype: dtype + elsif levels + args = levels.map { |l| l ? l : 'NA' }.join(', ') + t = "#{value.size} * categorical(#{args})" + type = NDTypes.new t + elsif typedef + type = NDTypes.new typedef + if type.abstract? + dtype = type.hidden_dtype + t = TypeInference.type_of data, dtype: dtype + type = NDTypes.instantiate typedef, t + end + elsif dtypedef + dtype = NDTypes.new dtypedef + type = TypeInference.type_of data, dtype: dtype + else + type = TypeInference.type_of data + data = TypeInference.convert_xnd_t_to_ruby_array data + end + + super(type, data) + end + + alias :to_a :value +end diff --git a/ruby/lib/xnd/version.rb b/ruby/lib/xnd/version.rb new file mode 100644 index 0000000..0ca637d --- /dev/null +++ b/ruby/lib/xnd/version.rb @@ -0,0 +1,6 @@ +class RubyXND + # VERSION number should be the version of libxnd this is built against. + VERSION = "0.2.0dev6" + # Working commit in xnd repo + COMMIT = "31ec2d8501af442e1a5054d9531a5e2cefb242cb" +end diff --git a/ruby/test/debug_test.rb b/ruby/test/debug_test.rb new file mode 100644 index 0000000..d66f726 --- /dev/null +++ b/ruby/test/debug_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class TestXND_DEBUG < Minitest::Test + def test_nil_value + assert_equal RubyXND::XND_DEBUG, nil + end +end diff --git a/ruby/test/gc_guard_test.rb b/ruby/test/gc_guard_test.rb new file mode 100644 index 0000000..8a8f7a0 --- /dev/null +++ b/ruby/test/gc_guard_test.rb @@ -0,0 +1,10 @@ +require 'test_helper' + +class TestGCGuard < Minitest::Test + def test_gc_table_value + xnd = XND.new([[1,2,3]]) + + gc_table = RubyXND::GCGuard.instance_variable_get(:@__gc_guard_table) + assert gc_table.keys.size >= 1 + end +end diff --git a/ruby/test/leaktest.rb b/ruby/test/leaktest.rb new file mode 100644 index 0000000..ed6d9fa --- /dev/null +++ b/ruby/test/leaktest.rb @@ -0,0 +1,25 @@ +# Run each Test class N times and start the GC after calling the test +require 'xnd' +require_relative 'test_xnd.rb' + +# ObjectSpace.each_object(Class).map(&:to_s).grep(/TestVarDim/).each do |t| +# klass = Kernel.const_get(t) +# methods = klass.runnable_methods + +# 100.times do +# methods.each do |m| +# Minitest.run ['-n', m] +# end +# end + +# GC.start +# end + + 100.times do + # methods.each do |m| + #Minitest.run ['-n', 'test_var_dim_empty'] + Minitest.run ['-n', 'test_fixed_bytes_equality'] + #end + end + + GC.start diff --git a/ruby/test/test_helper.rb b/ruby/test/test_helper.rb new file mode 100644 index 0000000..75028d9 --- /dev/null +++ b/ruby/test/test_helper.rb @@ -0,0 +1,864 @@ +$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) +require 'xnd' + +require 'minitest/autorun' +require 'minitest/hooks' + +Minitest::Test.parallelize_me! + +MAX_DIM = NDT::MAX_DIM + +def assert_strict_equal x1, x2 + assert x1.strict_equal(x2) + assert_equal x1, x2 +end + +def assert_strict_unequal x1, x2 + assert !x1.strict_equal(x2) + refute_equal x1, x2 +end + +def type_equal t, u + t.match(u) && u.match(t) +end + +def get_inf_or_normal_range start, stop, exclude_end + if start == Float::INFINITY && stop != Float::INFINITY + Range.new 0, stop, exclude_end + elsif start != Float::INFINITY && stop == Float::INFINITY + Range.new start, 9999, exclude_end + elsif start == Float::INFINITY && stop == Float::INFINITY + Range.new 0, 9999, exclude_end + else + Range.new start, stop, exclude_end + end +end + +# ====================================================================== +# Primitive types +# ====================================================================== + +PRIMITIVE = [ + 'int8', 'int16', 'int32', 'int64', + 'uint8', 'uint16', 'uint32', 'uint64', + 'float32', 'float64', + 'complex64', 'complex128' +] + +BOOL_PRIMITIVE = ['bool'] + +EMPTY_TEST_CASES = [ + [0, "%s"], + [[], "0 * %s"], + [[0], "1 * %s"], + [[0, 0], "var(offsets=[0, 2]) * %s"], + [[{"a" => 0, "b" => 0}] * 3, "3 * {a: int64, b: %s}"] +] + +def empty_test_cases(val=0) + [ + [val, "%s"], + [[], "0 * %s"], + [[val], "1 * %s"], + [[val, val], "var(offsets=[0, 2]) * %s"], + [[{"a" => 0, "b" => val}] * 3, "3 * {a: int64, b: %s}"] + ] +end + +# ====================================================================== +# Typed values +# ====================================================================== + +DTYPE_EMPTY_TEST_CASES = [ + # Tuples + [[], "()"], + [[0], "(int8)"], + [[0, 0], "(int8, int64)"], + [[0, [0+0i]], "(uint16, (complex64))"], + [[0, [0+0i]], "(uint16, (complex64), pack=1)"], + [[0, [0+0i]], "(uint16, (complex64), pack=2)"], + [[0, [0+0i]], "(uint16, (complex64), pack=4)"], + [[0, [0+0i]], "(uint16, (complex64), pack=8)"], + [[0, [0+0i]], "(uint16, (complex64), align=16)"], + [[[]], "(0 * bytes)"], + [[[], []], "(0 * bytes, 0 * string)"], + [[[''], [0.0i] * 2, [""] * 3], "(1 * bytes, 2 * complex128, 3 * string)"], + [[[""], [[0.0i, [[""] * 2] * 10]] * 2, [""] * 3], "(1 * bytes, 2 * (complex128, 10 * 2 * string), 3 * string)"], + [[0, [[[[0.0] * 5] * 4] * 3] * 2], "(int64, 2 * 3 * Some(4 * 5 * float64))"], + [[0, [[[[0.0] * 5] * 4] * 3] * 2], "(int64, 2 * 3 * ref(4 * 5 * float64))"], + + # Optional tuples + [nil, "?()"], + [nil, "?(int8)"], + [nil, "?(int8, int64)"], + [nil, "?(uint16, (complex64))"], + [nil, "?(uint16, (complex64), pack=1)"], + [nil, "?(uint16, (complex64), pack=2)"], + [nil, "?(uint16, (complex64), pack=4)"], + [nil, "?(uint16, (complex64), pack=8)"], + [nil, "?(uint16, (complex64), align=16)"], + [nil, "?(0 * bytes)"], + [nil, "?(0 * bytes, 0 * string)"], + [nil, "?(1 * bytes, 2 * complex128, 3 * string)"], + [nil, "?(1 * bytes, 2 * (complex128, 10 * 2 * string), 3 * string)"], + + # Tuples with optional elements + [[nil], "(?int8)"], + [[nil, 0], "(?int8, int64)"], + [[0, nil], "(int8, ?int64)"], + [[nil, nil], "(?int8, ?int64)"], + [nil, "?(?int8, ?int64)"], + + [[0, nil], "(uint16, ?(complex64))"], + [[0, [nil]], "(uint16, (?complex64))"], + [[0, nil], "(uint16, ?(?complex64))"], + + [[nil, [0+0i]], "(?uint16, (complex64), pack=1)"], + [[0, nil], "(uint16, ?(complex64), pack=1)"], + [[0, [nil]], "(uint16, (?complex64), pack=1)"], + + [[[]], "(0 * ?bytes)"], + [[[nil]], "(1 * ?bytes)"], + [[[nil] * 10], "(10 * ?bytes)"], + [[[], []], "(0 * ?bytes, 0 * ?string)"], + [[[nil] * 5, [""] * 2], "(5 * ?bytes, 2 * string)"], + [[[""] * 5, [nil] * 2], "(5 * bytes, 2 * ?string)"], + [[[nil] * 5, [nil] * 2], "(5 * ?bytes, 2 * ?string)"], + + [[[nil], [nil] * 2, [nil] * 3], "(1 * ?bytes, 2 * ?complex128, 3 * ?string)"], + + [[[nil], [ [0.0i, [ [""] * 2 ] * 10 ] ] * 2, [""] * 3 ], "(1 * ?bytes, 2 * (complex128, 10 * 2 * string), 3 * string)"], + [[[""], [nil] * 2, [""] * 3], "(1 * bytes, 2 * ?(complex128, 10 * 2 * string), 3 * string)"], + [[[""], [[0.0i, [[""] * 2] * 10]] * 2, [nil] * 3], "(1 * bytes, 2 * (complex128, 10 * 2 * string), 3 * ?string)"], + [[[nil], [nil] * 2, [""] * 3], "(1 * ?bytes, 2 * ?(complex128, 10 * 2 * string), 3 * string)"], + [[[nil], [[0.0i, [[""] * 2] * 10]] * 2, [nil] * 3], "(1 * ?bytes, 2 * (complex128, 10 * 2 * string), 3 * ?string)"], + [[[nil], [nil] * 2, [nil] * 3], "(1 * ?bytes, 2 * ?(complex128, 10 * 2 * string), 3 * ?string)"], + + [[[""],[[0.0i, [[nil] * 2] * 10]] * 2, [""] * 3], "(1 * bytes, 2 * (complex128, 10 * 2 * ?string), 3 * string)"], + [[[nil], [[0.0i, [[nil] * 2] * 10]] * 2, [""] * 3], "(1 * ?bytes, 2 * (complex128, 10 * 2 * ?string), 3 * string)"], + [[[nil], [[nil , [[nil] * 2] * 10]] * 2, [""] * 3], "(1 * ?bytes, 2 * (?complex128, 10 * 2 * ?string), 3 * string)"], + [[[""], [[nil, [[nil] * 2] * 10]] * 2, [nil] * 3], "(1 * bytes, 2 * (?complex128, 10 * 2 * ?string), 3 * ?string)"], + + [[0, [[[[nil] * 5] * 4] * 3] * 2], "(int64, 2 * 3 * Some(4 * 5 * ?float64))"], + [[0, [[[[nil] * 5] * 4] * 3] * 2], "(int64, 2 * 3 * ref(4 * 5 * ?float64))"], + + # Records + [{}, "{}"], + [{'x' => 0}, "{x: int8}"], + [{'x' => 0, 'y' => 0}, "{x: int8, y: int64}"], + [{'x' => 0, 'y' => {'z' => 0+0i}}, "{x: uint16, y: {z: complex64}}"], + [{'x' => 0, 'y' => {'z' => 0+0i}}, "{x: uint16, y: {z: complex64}, pack=1}"], + [{'x' => 0, 'y' => {'z' => 0+0i}}, "{x: uint16, y: {z: complex64}, pack=2}"], + [{'x' => 0, 'y' => {'z' => 0+0i}}, "{x: uint16, y: {z: complex64}, pack=4}"], + [{'x' => 0, 'y' => {'z' => 0+0i}}, "{x: uint16, y: {z: complex64}, pack=8}"], + [{'x' => 0, 'y' => {'z' => 0+0i}}, "{x: uint16, y: {z: complex64}, align=16}"], + [{'x' => []}, "{x: 0 * bytes}"], + [{'x' => [], 'y' => []}, "{x: 0 * bytes, y: 0 * string}"], + [{'x' => [""], 'y' => [0.0i] * 2, 'z' => [""] * 3}, "{x: 1 * bytes, y: 2 * complex128, z: 3 * string}"], + [{'x' => [""], 'y' => [{'a' => 0.0i, 'b' => [[""] * 2] * 10}] * 2, 'z' => [""] * 3}, "{x: 1 * bytes, y: 2 * {a: complex128, b: 10 * 2 * string}, z: 3 * string}"], + [{'x' => 0, 'y' => [[[[0.0] * 5] * 4] * 3] * 2}, "{x: int64, y: 2 * 3 * Some(4 * 5 * float64)}"], + [{'x' => 0, 'y' => [[[[0.0] * 5] * 4] * 3] * 2}, "{x: int64, y: 2 * 3 * ref(4 * 5 * float64)}"], + + # Optional records + [nil, "?{}"], + [nil, "?{x: int8}"], + [nil, "?{x: int8, y: int64}"], + [nil, "?{x: uint16, y: {z: complex64}}"], + [nil, "?{x: uint16, y: {z: complex64}, pack=1}"], + [nil, "?{x: uint16, y: {z: complex64}, pack=2}"], + [nil, "?{x: uint16, y: {z: complex64}, pack=4}"], + [nil, "?{x: uint16, y: {z: complex64}, pack=8}"], + [nil, "?{x: uint16, y: {z: complex64}, align=16}"], + [nil, "?{x: 0 * bytes}"], + [nil, "?{x: 0 * bytes, y: 0 * string}"], + [nil, "?{x: 1 * bytes, y: 2 * complex128, z: 3 * string}"], + [nil, "?{x: 1 * bytes, y: 2 * {a: complex128, b: 10 * 2 * string}, z: 3 * string}"], + + # Records with optional elements + [{'x' => nil}, "{x: ?int8}"], + [{'x' => nil, 'y' => 0}, "{x: ?int8, y: int64}"], + [{'x' => 0, 'y' => nil}, "{x: int8, y: ?int64}"], + [{'x' => nil, 'y' => nil}, "{x: ?int8, y: ?int64}"], + [nil, "?{x: ?int8, y: ?int64}"], + + [{'x' => 0, 'y' => nil}, " {x: uint16, y: ?{z: complex64}}"], + [{'x' => 0, 'y' => {'z' => nil}}, "{x: uint16, y: {z: ?complex64}}"], + [{'x' => 0, 'y' => nil}, "{x: uint16, y: ?{z: ?complex64}}"], + + [{'x' => nil, 'y' => {'z' => 0+0i}}, "{x: ?uint16, y: {z: complex64}, pack=1}"], + [{'x' => 0, 'y' => nil}, "{x: uint16, y: ?{z: complex64}, pack=1}"], + [{'x' => 0, 'y' => {'z' => nil}}, "{x: uint16, y: {z: ?complex64}, pack=1}"], + + [{'x' => []}, "{x: 0 * ?bytes}"], + [{'x' => [nil] * 1}, "{x: 1 * ?bytes}"], + [{'x' => [nil] * 10}, "{x: 10 * ?bytes}"], + [{'x' => [], 'y' => []}, "{x: 0 * ?bytes, y: 0 * ?string}"], + [{'x' => [nil] * 5, 'y' => [""] * 2}, "{x: 5 * ?bytes, y: 2 * string}"], + [{'x' => [""] * 5, 'y' => [nil] * 2}, "{x: 5 * bytes, y: 2 * ?string}"], + [{'x' => [nil] * 5, 'y' => [nil] * 2}, "{x: 5 * ?bytes, y: 2 * ?string}"], + + [{'x' => [nil], 'y' => [nil] * 2, 'z' => [nil] * 3}, "{x: 1 * ?bytes, y: 2 * ?complex128, z: 3 * ?string}"], + [{'x' => [nil], 'y' => [{'a' => 0.0i, 'b' => [[""] * 2] * 10}] * 2, 'z' => [""] * 3}, "{x: 1 * ?bytes, y: 2 * {a: complex128, b: 10 * 2 * string}, z: 3 * string}"], + [{'x' => [""], 'y' => [nil] * 2, 'z' => [""] * 3}, "{x: 1 * bytes, y: 2 * ?{a: complex128, b: 10 * 2 * string}, z: 3 * string}"], + [{'x' => [""], 'y' => [{'a' => 0.0i, 'b' => [[""] * 2] * 10}] * 2, 'z' => [nil] * 3}, "{x: 1 * bytes, y: 2 * {a: complex128, b: 10 * 2 * string}, z: 3 * ?string}"], + [{'x' => [nil], 'y' => [nil] * 2, 'z' => [""] * 3}, "{x: 1 * ?bytes, y: 2 * ?{a: complex128, b: 10 * 2 * string}, z: 3 * string}"], + [{'x' => [nil], 'y' => [{'a' => 0.0i, 'b' => [[""] * 2] * 10}] * 2, 'z' => [nil] * 3}, "{x: 1 * ?bytes, y: 2 * {a: complex128, b: 10 * 2 * string}, z: 3 * ?string}"], + [{'x' => [nil], 'y' => [nil] * 2, 'z' => [nil] * 3}, "{x: 1 * ?bytes, y: 2 * ?{a: complex128, b: 10 * 2 * string}, z: 3 * ?string}"], + + [{'x' => [""], 'y' => [{'a' => 0.0i, 'b' => [[nil] * 2] * 10}] * 2, 'z' => [""] * 3}, "{x: 1 * bytes, y: 2 * {a: complex128, b: 10 * 2 * ?string}, z: 3 * string}"], + [{'x' => [nil], 'y' => [{'a' => 0.0i, 'b' => [[nil] * 2] * 10}] * 2, 'z' => [""] * 3}, "{x: 1 * ?bytes, y: 2 * {a: complex128, b: 10 * 2 * ?string}, z: 3 * string}"], + [{'x' => [nil], 'y' => [{'a' => nil, 'b' => [[nil] * 2] * 10}] * 2, 'z' => [""] * 3}, "{x: 1 * ?bytes, y: 2 * {a: ?complex128, b: 10 * 2 * ?string}, z: 3 * string}"], + [{'x' => [""], 'y' => [{'a' => nil, 'b' => [[nil] * 2] * 10}] * 2, 'z' => [nil] * 3}, "{x: 1 * bytes, y: 2 * {a: ?complex128, b: 10 * 2 * ?string}, z: 3 * ?string}"], + + [{'x' => 0, 'y' => [[[[nil] * 5] * 4] * 3] * 2}, "{x: int64, y: 2 * 3 * Some(4 * 5 * ?float64)}"], + [{'x' => 0, 'y' => [[[[nil] * 5] * 4] * 3] * 2}, "{x: int64, y: 2 * 3 * ref(4 * 5 * ?float64)}"], + + # Primitive types + [false, "bool"], + + [0, "int8"], + [0, "int16"], + [0, "int32"], + [0, "int64"], + + [0, "uint8"], + [0, "uint16"], + [0, "uint32"], + [0, "uint64"], + + [0.0, "float32"], + [0.0, "float64"], + + [0+0i, "complex64"], + [0+0i, "complex128"], + + [0+0i, "complex64"], + [0+0i, "complex128"], + + # Optional primitive types + [nil, "?bool"], + + [nil, "?int8"], + [nil, "?int16"], + [nil, "?int32"], + [nil, "?int64"], + + [nil, "?uint8"], + [nil, "?uint16"], + [nil, "?uint32"], + [nil, "?uint64"], + + [nil, "?float32"], + [nil, "?float64"], + + [nil, "?complex64"], + [nil, "?complex128"], + + [nil, "?complex64"], + [nil, "?complex128"], + + # References + [false, "&bool"], + + [0, "&int8"], + [0, "&int16"], + [0, "&int32"], + [0, "&int64"], + + [0, "ref(uint8)"], + [0, "ref(uint16)"], + [0, "ref(uint32)"], + [0, "ref(uint64)"], + + [0, "ref(ref(uint8))"], + [0, "ref(ref(uint16))"], + [0, "ref(ref(uint32))"], + [0, "ref(ref(uint64))"], + + [0.0, "ref(float32)"], + [0.0, "ref(float64)"], + + [0+0i, "ref(complex64)"], + [0+0i, "ref(complex128)"], + + [[], "ref(0 * bool)"], + [[0], "ref(1 * int16)"], + [[0] * 2, "ref(2 * int32)"], + [[[0] * 3] * 2, "ref(2 * 3 * int8)"], + + [[], "ref(ref(0 * bool))"], + [[0], "ref(ref(1 * int16))"], + [[0] * 2, "ref(ref(2 * int32))"], + [[[0] * 3] * 2, "ref(ref(2 * 3 * int8))"], + + [[], "ref(!0 * bool)"], + [[0], "ref(!1 * int16)"], + [[0] * 2, "ref(!2 * int32)"], + [[[0] * 3] * 2, "ref(!2 * 3 * int8)"], + + [[], "ref(ref(!0 * bool))"], + [[0], "ref(ref(!1 * int16))"], + [[0] * 2, "ref(ref(!2 * int32))"], + [[[0] * 3] * 2, "ref(ref(!2 * 3 * int8))"], + + # Optional references + [nil, "?&bool"], + + [nil, "?&int8"], + [nil, "?&int16"], + [nil, "?&int32"], + [nil, "?&int64"], + + [nil, "?ref(uint8)"], + [nil, "?ref(uint16)"], + [nil, "?ref(uint32)"], + [nil, "?ref(uint64)"], + + [nil, "?ref(ref(uint8))"], + [nil, "?ref(ref(uint16))"], + [nil, "?ref(ref(uint32))"], + [nil, "?ref(ref(uint64))"], + + [nil, "?ref(float32)"], + [nil, "?ref(float64)"], + + [nil, "?ref(complex64)"], + [nil, "?ref(complex128)"], + + [nil, "?ref(0 * bool)"], + [nil, "?ref(1 * int16)"], + [nil, "?ref(2 * int32)"], + [nil, "?ref(2 * 3 * int8)"], + + [nil, "?ref(ref(0 * bool))"], + [nil, "ref(?ref(0 * bool))"], + [nil, "?ref(?ref(0 * bool))"], + [nil, "?ref(ref(1 * int16))"], + [nil, "ref(?ref(1 * int16))"], + [nil, "?ref(?ref(1 * int16))"], + [nil, "?ref(ref(2 * int32))"], + + [nil, "?ref(!2 * 3 * int8)"], + [nil, "?ref(ref(!2 * 3 * int32))"], + + # References to types with optional data + [nil, "&?bool"], + + [nil, "&?int8"], + [nil, "&?int16"], + [nil, "&?int32"], + [nil, "&?int64"], + + [nil, "ref(?uint8)"], + [nil, "ref(?uint16)"], + [nil, "ref(?uint32)"], + [nil, "ref(?uint64)"], + + [nil, "ref(ref(?uint8))"], + [nil, "ref(ref(?uint16))"], + [nil, "ref(ref(?uint32))"], + [nil, "ref(ref(?uint64))"], + + [nil, "ref(?float32)"], + [nil, "ref(?float64)"], + + [nil, "ref(?complex64)"], + [nil, "ref(?complex128)"], + + [[], "ref(0 * ?bool)"], + [[nil], "ref(1 * ?int16)"], + [[nil] * 2, "ref(2 * ?int32)"], + [[[nil] * 3] * 2, "ref(2 * 3 * ?int8)"], + + [[], "ref(ref(0 * ?bool))"], + [[nil], "ref(ref(1 * ?int16))"], + [[nil] * 2, "ref(ref(2 * ?int32))"], + + [[[nil] * 3] * 2, "ref(!2 * 3 * ?int8)"], + [[[nil] * 3] * 2, "ref(ref(!2 * 3 * ?int8))"], + + # Constructors + [false, "Some(bool)"], + + [0, "Some(int8)"], + [0, "Some(int16)"], + [0, "Some(int32)"], + [0, "Some(int64)"], + + [0, "Some(uint8)"], + [0, "Some(uint16)"], + [0, "Some(uint32)"], + [0, "Some(uint64)"], + + [0.0, "Some(float32)"], + [0.0, "Some(float64)"], + + [0+0i, "Some(complex64)"], + [0+0i, "Some(complex128)"], + + [[0], "ThisGuy(1 * int16)"], + [[0] * 2, "ThisGuy(2 * int32)"], + [[[0.0] * 3] * 2, "ThisGuy(2 * 3 * float32)"], + + [[[0.0] * 3] * 2, "ThisGuy(!2 * 3 * float32)"], + + # Optional constructors + [nil, "?Some(bool)"], + + [nil, "?Some(int8)"], + [nil, "?Some(int16)"], + [nil, "?Some(int32)"], + [nil, "?Some(int64)"], + + [nil, "?Some(uint8)"], + [nil, "?Some(uint16)"], + [nil, "?Some(uint32)"], + [nil, "?Some(uint64)"], + + [nil, "?Some(float32)"], + [nil, "?Some(float64)"], + + [nil, "?Some(complex64)"], + [nil, "?Some(complex128)"], + + [nil, "?ThisGuy(0 * int16)"], + [nil, "?ThisGuy(1 * int16)"], + [nil, "?ThisGuy(2 * int32)"], + [nil, "?ThisGuy(2 * 3 * float32)"], + + [nil, "?ThisGuy(!2 * 3 * float32)"], + + # Constructors with an optional data type argument + [nil, "Some(?bool)"], + + [nil, "Some(?int8)"], + [nil, "Some(?int16)"], + [nil, "Some(?int32)"], + [nil, "Some(?int64)"], + + [nil, "Some(?uint8)"], + [nil, "Some(?uint16)"], + [nil, "Some(?uint32)"], + [nil, "Some(?uint64)"], + + [nil, "Some(?float32)"], + [nil, "Some(?float64)"], + + [nil, "Some(?complex64)"], + [nil, "Some(?complex128)"], + + [[], "ThisGuy(0 * ?int16)"], + [[nil], "ThisGuy(1 * ?int16)"], + [[nil] * 2, "ThisGuy(2 * ?int32)"], + [[[nil] * 3] * 2, "ThisGuy(2 * 3 * ?float32)"], + + [[[nil] * 3] * 2, "ThisGuy(!2 * 3 * ?float32)"], +] + +# +# Test case for richcompare: +# v: value +# t: type of v +# u: equivalent type for v +# w: value different from v +# eq: expected comparison result +# +Struct.new("T", :v, :t, :u, :w, :eq) +T = Struct::T + +EQUAL_TEST_CASES = [ + # Tuples + T.new([], + "()", + nil, + nil, + true), + + T.new([100], + "(int8)", + "?(int8)", + [101], + true), + + T.new([2**7-1, 2**63-1], + "(int8, int64)", + "(int8, ?int64) ", + [2**7-2, 2**63-1], + true), + + T.new([2**16-1, [1.2312222+28i]], + "(uint16, (complex64))", + "(uint16, ?(complex64))", + [2**16-1, [1.23122+28i]], + true), + + T.new([1, [1e22+2i]], + "(uint32, (complex64), pack=1)", + "(uint32, (complex64), align=16)", + [1, [1e22+3i]], + true), + + T.new([[]], + "(0 * bytes)", + nil, + nil, + true), + + T.new([[], []], + "(0 * bytes, 0 * string)", + nil, + nil, + true), + + T.new([['x'], [1.2i] * 2, ["abc"] * 3], + "(1 * bytes, 2 * complex128, 3 * string)", + "(1 * bytes, 2 * ?complex128, 3 * string)", + [['x'], [1.2i] * 2, ["ab"] * 3], + true), + + T.new([['123'], [[3e22+0.2i, [["xyz"] * 2] * 10]] * 2, ["1"] * 3], + "(1 * bytes, 2 * (complex128, 10 * 2 * string), 3 * string)", + "(1 * bytes, 2 * ?(complex128, 10 * 2 * string), 3 * string)", + [['1234'], [[3e22+0.2i, [["xyz"] * 2] * 10]] * 2, ["1"] * 3], + true), + + T.new([10001, [[[[2.250] * 5] * 4] * 3] * 2], + "(int64, 2 * 3 * Some(4 * 5 * float64))", + "(int64, 2 * 3 * Some(4 * 5 * ?float64))", + [10001, [[[[2.251] * 5] * 4] * 3] * 2], + true), + + T.new([-2**63, [[[[10.1] * 5] * 4] * 3] * 2], + "(int64, 2 * 3 * ref(4 * 5 * float64))", + "(int64, 2 * 3 * ?ref(4 * 5 * float64))", + [-2**63+1, [[[[10.1] * 5] * 4] * 3] * 2], + true), + + # Optional tuples + T.new(nil, + "?()", + nil, + nil, + false), + + T.new(nil, + "?(int8)", + nil, + nil, + false), + + T.new(nil, + "?(1 * bytes, 2 * (complex128, 10 * 2 * string), 3 * string)", + nil, + nil, + false), + + # Tuples with optional elements + T.new([nil], + "(?int8)", + nil, + nil, + false), + + T.new([nil, 0], + "(?int8, int64)", + nil, + nil, + false), + + T.new([0, nil], + "(int8, ?int64)", + nil, + nil, + false), + + T.new([nil, nil], + "(?int8, ?int64)", + nil, + nil, + false), + + T.new(nil, + "?(?int8, ?int64)", + nil, + nil, + false), + + T.new([0, nil], + "(uint16, ?(complex64))", + nil, + nil, + false), + + T.new([0, [nil]], + "(uint16, (?complex64))", + nil, + nil, + false), + + T.new([0, nil], + "(uint16, ?(?complex64))", + nil, + nil, + false), + + T.new([nil, [0+0i]], + "(?uint16, (complex64), pack=1)", + nil, + nil, + false), + + T.new([0, nil], + "(uint16, ?(complex64), pack=1)", + nil, + nil, + false), + + T.new([0, [nil]], + "(uint16, (?complex64), pack=1)", + nil, + nil, + false), + + # Records + T.new({}, + "{}", + nil, + nil, + true), + + T.new({'x' => 2**31-1}, + "{x: int32}", + "{x: ?int32}", + {'x' => 2**31-2}, + true), + + T.new({'x' => -128, 'y' => -1}, + "{x: int8, y: int64}", + "{x: int8, y: int64, pack=1}", + {'x' => -127, 'y' => -1}, + true), + + T.new({'x' => 2**32-1, 'y' => {'z' => 10000001e3+36.1e7i}}, + "{x: uint32, y: {z: complex64}}", + "{x: uint32, y: {z: complex64}}", + {'x' => 2**32-2, 'y' => {'z' => 10000001e3+36.1e7i}}, + true), + + T.new({'x' => 255, 'y' => {'z' => 2+3i}}, + "{x: uint8, y: {z: complex64}, pack=1}", + "{x: uint8, y: {z: complex64}, pack=4}", + {'x' => 255, 'y' => {'z' => 3+3i}}, + true), + + T.new({'x' => 10001, 'y' => {'z' => "abc"}}, + "{x: uint16, y: {z: fixed_string(100)}, pack=2}", + "{x: uint16, y: {z: fixed_string(100)}, align=8}", + {'x' => 10001, 'y' => {'z' => "abcd"}}, + true), + + T.new({'x' => []}, + "{x: 0 * bytes}", + nil, + nil, + true), + + T.new({'x' => [], 'y' => []}, + "{x: 0 * bytes, y: 0 * string}", + nil, + nil, + true), + + T.new({'x' => ["".b], 'y' => [2.0i] * 2, 'z' => ["y"] * 3}, + "{x: 1 * fixed_bytes(size=512), y: 2 * complex128, z: 3 * string}", + "{x: 1 * fixed_bytes(size=512, align=256), y: 2 * complex128, z: 3 * string}", + nil, + true), + + T.new({'x' => 100, 'y' => [[[[301.0] * 5] * 4] * 3] * 2}, + "{x: int64, y: 2 * 3 * Some(4 * 5 * ?float64)}", + "{x: int64, y: 2 * 3 * ?Some(4 * 5 * ?float64)}", + {'x' => 100, 'y' => [[[[nil] * 5] * 4] * 3] * 2}, + true), + + # Optional records + T.new(nil, + "?{}", + "?{}", + nil, + false), + + T.new(nil, + "?{x: int8}", + "?{x: int8}", + nil, + false), + + T.new({'x' => 101}, + "?{x: int8}", + "{x: int8}", + {'x' => 100}, + true), + + T.new(nil, + "?{x: 0 * bytes}", + "?{x: 0 * bytes}", + nil, + false), + + T.new(nil, + "?{x: 0 * bytes, y: 0 * string}", + "?{x: 0 * bytes, y: 0 * string}", + nil, + false), + + # Records with optional elements + T.new({'x' => nil}, + "{x: ?int8}", + "{x: ?int8}", + nil, + false), + + T.new({'x' => nil, 'y' => 0}, + "{x: ?int8, y: int64}", + "{x: ?int8, y: int64}", + {'x' => 0, 'y' => 0}, + false), + + T.new({'x' => 100, 'y' => nil}, + "{x: int8, y: ?int64}", + "{x: int8, y: ?int64}", + {'x' => 100, 'y' => 10}, + false), + + T.new({'x' => nil, 'y' => nil}, + "{x: ?int8, y: ?int64}", + "{x: ?int8, y: ?int64}", + {'x' => 1, 'y' => 1}, + false), + + T.new(nil, + "?{x: ?int8, y: ?int64}", + "?{x: ?int8, y: ?int64}", + nil, + false), + + T.new({'x' => 0, 'y' => nil}, + "{x: uint16, y: ?{z: complex64}}", + "{x: uint16, y: ?{z: complex64}}", + {'x' => 1, 'y' => nil}, + false), + + # FIXME: The 'z' => 2 does not seem to bode well with XND. Fix. + # T.new({'x' => 0, 'y' => {'z' => nil}}, + # "{x: uint16, y: {z: ?complex64}}", + # "{x: uint16, y: {z: ?complex64}}", + # {'x' => 0, 'y' => {'z' => 2}}, + # false), + + T.new({'x' => 0, 'y' => nil}, + "{x: uint16, y: ?{z: ?complex64}}", + "{x: uint16, y: ?{z: ?complex64}}", + {'x' => 0, 'y' => {'z' => 1+10i}}, + false), + + T.new({'x' => nil, 'y' => {'z' => 0+0i}}, + "{x: ?uint16, y: {z: complex64}, pack=1}", + "{x: ?uint16, y: {z: complex64}, align=16}", + {'x' => 256, 'y' => {'z' => 0+0i}}, + false), + + T.new({'x' => 0, 'y' => nil}, + "{x: uint16, y: ?{z: complex64}, pack=1}", + "{x: uint16, y: ?{z: complex64}, pack=1}", + {'x' => 0, 'y' => {'z' => 0+1i}}, + false), + + T.new({'x' => 0, 'y' => {'z' => nil}}, + "{x: ?uint16, y: {z: ?complex64}, pack=1}", + "{x: ?uint16, y: {z: ?complex64}, pack=1}", + {'x' => nil, 'y' => {'z' => nil}}, + false), + + T.new({'x' => []}, + "{x: 0 * ?bytes}", + "{x: 0 * ?bytes}", + nil, + true), + + T.new({'x' => [nil] * 1}, + "{x: 1 * ?bytes}", + "{x: 1 * ?bytes}", + nil, + false), + + T.new({'x' => [nil] * 10}, + "{x: 10 * ?bytes}", + "{x: 10 * ?bytes}", + {'x' => ["123"] * 10}, + false), + + T.new({'x' => [], 'y' => []}, + "{x: 0 * ?bytes, y: 0 * ?string}", + "{x: 0 * ?bytes, y: 0 * ?string}", + nil, + true), + + T.new({'x' => ["123"] * 5, 'y' => ["abc"] * 2}, + "{x: 5 * ?bytes, y: 2 * string}", + "{x: 5 * bytes, y: 2 * string}", + {'x' => ["12345"] * 5, 'y' => ["abc"] * 2}, + true), + + T.new({'x' => ["-123"], 'y' => [1e200+10i] * 2, 'z' => ["t" * 100] * 3}, + "{x: 1 * ?bytes, y: 2 * ?complex128, z: 3 * ?string}", + "{x: 1 * ?bytes, y: 2 * ?complex128, z: 3 * ?string}", + {'x' => ["-123"], 'y' => [1e200+10i] * 2, 'z' => ["t" * 100 + "u"] * 3}, + true), + + T.new({'x' => ["x"], 'y' => [{'a' => 0.0i, 'b' => [["c"] * 2] * 10}] * 2, 'z' => ["a"] * 3}, + "{x: 1 * ?bytes, y: 2 * {a: complex128, b: 10 * 2 * string}, z: 3 * string}", + "{x: 1 * ?bytes, y: 2 * {a: complex128, b: 10 * 2 * ?string}, z: 3 * string}", + {'x' => ["x"], 'y' => [{'a' => 1.0i, 'b' => [["c"] * 2] * 10}] * 2, 'z' => ["a"] * 3}, + true), + + # Primitive types + T.new(false, "bool", "?bool", true, true), + T.new(0, "?bool", "bool", 1, true), + + T.new(127, "int8", "?int8", 100, true), + T.new(127, "?int8", "int8", 100, true), + + T.new(-127, "int16", "?int16", 100, true), + T.new(-127, "?int16", "int16", 100, true), + + T.new(127, "int32", "?int32", 100, true), + T.new(127, "?int32", "int32", 100, true), + + T.new(127, "int64", "?int64", 100, true), + T.new(127, "?int64", "int64", 100, true), + + T.new(127, "uint8", "?uint8", 100, true), + T.new(127, "?uint8", "uint8", 100, true), + + T.new(127, "uint16", "?uint16", 100, true), + T.new(127, "?uint16", "uint16", 100, true), + + T.new(127, "uint32", "?uint32", 100, true), + T.new(127, "?uint32", "uint32", 100, true), + + T.new(127, "uint64", "?uint64", 100, true), + T.new(127, "?uint64", "uint64", 100, true), + + T.new(1.122e11, "float32", "?float32", 2.111, true), + T.new(1.233e10, "?float32", "float32", 3.111, true), + + T.new(1.122e11, "float64", "?float64", 2.111, true), + T.new(1.233e10, "?float64", "float64", 3.111, true), + + T.new(1.122e11+1i, "complex64", "?complex64", 1.122e11+2i, true), + T.new(1.122e11+1i, "?complex64", "complex64", 1.1e11+1i, true), + + T.new(1.122e11-100i, "complex128", "?complex128", 1.122e11-101i, true), + T.new(1.122e11-100i, "?complex128", "complex128", 1.122e10-100i, true), +] diff --git a/ruby/test/test_xnd.rb b/ruby/test/test_xnd.rb new file mode 100644 index 0000000..18004c8 --- /dev/null +++ b/ruby/test/test_xnd.rb @@ -0,0 +1,2894 @@ +require_relative 'test_helper' + +class TestModule < Minitest::Test + def test_module + test_cases = [ + "Foo:: 2 * 3 * ?int64", + "Foo:: 10 * 2 * ?string", + "Bar:: !10 * 2 * {a: !2 * ?int64}", + "Quux:: {a: string, b: ?bytes}" + ] + + test_cases.each do |s| + assert_raises(ValueError) { XND.empty(s) } + end + end +end # class TestModule + +class TestFunction < Minitest::Test + def test_function + test_cases = [ + "(2 * 3 * ?int64, complex128) -> (T, T)", + "(2 * 3 * ?int64, {a: float64, b: bytes}) -> bytes", + ] + + test_cases.each do |s| + assert_raises(ValueError) { XND.empty(s) } + end + end +end # class TestFunction + +class TestVoid < Minitest::Test + def test_void + assert_raises(ValueError) { XND.empty("void") } + assert_raises(ValueError) { XND.empty("10 * 2 * void") } + end +end # class TestVoid + +class TestAny < Minitest::Test + def test_any + test_cases = [ + "Any", + "10 * 2 * Any", + "10 * N * int64", + "{a: string, b: Any}" + ] + + test_cases.each do |s| + assert_raises(ValueError) { XND.empty(s) } + end + end +end # class TestAny + +class TestFixedDim < Minitest::Test + def test_fixed_dim_empty + DTYPE_EMPTY_TEST_CASES.each do |v, s| + [ + [[v] * 0, "0 * #{s}" ], + [[v] * 1, "1 * #{s}" ], + [[v] * 2, "2 * #{s}" ], + [[v] * 1000, "1000 * #{s}" ], + + [[[v] * 0] * 0, "0 * 0 * #{s}" ], + [[[v] * 1] * 0, "0 * 1 * #{s}" ], + [[[v] * 0] * 1, "1 * 0 * #{s}" ], + + [[[v] * 1] * 1, "1 * 1 * #{s}" ], + [[[v] * 2] * 1, "1 * 2 * #{s}" ], + [[[v] * 1] * 2, "2 * 1 * #{s}" ], + [[[v] * 2] * 2, "2 * 2 * #{s}" ], + [[[v] * 3] * 2, "2 * 3 * #{s}" ], + [[[v] * 2] * 3, "3 * 2 * #{s}" ], + [[[v] * 40] * 3 , "3 * 40 * #{s}" ] + ].each do |vv, ss| + t = NDT.new ss + x = XND.empty ss + + assert_equal t, x.type + assert_equal vv, x.value + assert_equal vv.size, x.size + end + end + end + + def test_overflow + assert_raises(ValueError) { XND.empty "2147483648 * 2147483648 * 2 * uint8" } + end + + def test_equality + x = XND.new [1,2,3,4] + + assert_strict_equal x, XND.new([1,2,3,4]) + + # different shape and/or data. + assert_strict_unequal x, XND.new([1,2,3,5]) + assert_strict_unequal x, XND.new([1,2,3,100]) + assert_strict_unequal x, XND.new([4,2,3,4,5]) + + # different shape. + assert_strict_unequal x, XND.new([1,2,3]) + assert_strict_unequal x, XND.new([[1,2,3,4]]) + assert_strict_unequal x, XND.new([[1,2], [3,4]]) + + # tests simple multidim array + x = XND.new([[1,2,3], [4,5,6], [7,8,9], [10,11,12]]) + y = XND.new([[1,2,3], [4,5,6], [7,8,9], [10,11,12]]) + + assert_strict_equal x, y + + # C <-> Fortran. + x = XND.new([[1,2,3], [4,5,6], [7,8,9], [10,11,12]]) + y = XND.new([[1,2,3], [4,5,6], [7,8,9], [10,11,12]], type: "!4 * 3 * int64") + + assert_strict_equal x, y + + # slices + x = XND.new([[1,2,3], [4,5,6], [7,8,9], [10,11,12]]) + y = XND.new([1,2,3]) + assert_strict_equal x[0], y + + y = XND.new [1,4,7,10] + assert_strict_equal x[0..Float::INFINITY,0], y + + # test corner cases and many dtypes. + EQUAL_TEST_CASES.each do |struct| + v = struct.v + t = struct.t + u = struct.u + + [ + [[v] * 0, "0 * #{t}", "0 * #{u}"], + [[[v] * 0] * 0, "0 * 0 * #{t}", "0 * 0 * #{u}"], + [[[v] * 1] * 0, "0 * 1 * #{t}", "0 * 1 * #{u}"], + [[[v] * 0] * 1, "1 * 0 * #{t}", "1 * 0 * #{u}"] + ].each do |vv, tt, uu| + ttt = NDT.new tt + + x = XND.new vv, type: ttt + y = XND.new vv, type: ttt + assert_strict_equal x, y + + unless u.nil? + uuu = NDT.new uu + y = XND.new vv, type: uuu + assert_strict_equal x, y + end + + end + end # EQUAL_TEST_CASES.each + + EQUAL_TEST_CASES.each do |struct| + v = struct.v + t = struct.t + u = struct.u + w = struct.w + eq = struct.eq + + [ + [[v] * 1, "1 * #{t}", "1 * #{u}", [0]], + [[v] * 2, "2 * #{t}", "2 * #{u}", [1]], + [[v] * 1000, "1000 * #{t}", "1000 * #{u}", [961]], + + [[[v] * 1] * 1, "1 * 1 * #{t}", "1 * 1 * #{u}", [0, 0]], + [[[v] * 2] * 1, "1 * 2 * #{t}", "1 * 2 * #{u}", [0, 1]], + [[[v] * 1] * 2, "2 * 1 * #{t}", "2 * 1 * #{u}", [1, 0]], + [[[v] * 2] * 2, "2 * 2 * #{t}", "2 * 2 * #{u}", [1, 1]], + [[[v] * 3] * 2, "2 * 3 * #{t}", "2 * 3 * #{u}", [1, 2]], + [[[v] * 2] * 3, "3 * 2 * #{t}", "3 * 2 * #{u}", [2, 1]], + [[[v] * 40] * 3, "3 * 40 * #{t}", "3 * 40 * #{u}", [1, 32]] + ].each do |vv, tt, uu, indices| + ttt = NDT.new tt + + x = XND.new vv, type: ttt + y = XND.new vv, type: ttt + + if eq + assert_strict_equal x, y + else + assert_strict_unequal x, y + end + + unless u.nil? + uuu = NDT.new uu + y = XND.new vv, type: uuu + + if eq + assert_strict_equal x, y + else + assert_strict_unequal x, y + end + end + + unless w.nil? + y = XND.new vv, type: ttt + + y[*indices] = w + assert_strict_unequal x, y + + unless u.nil? + uuu = NDT.new uu + y = XND.new vv, type: uuu + y[*indices] = w + assert_strict_unequal x, y + end + end + end + end + end + + def test_fixed_dim_indexing + # returns single number slice for 1D array/1 number + xnd = XND.new([1,2,3,4]) + assert_equal xnd[1], XND.new(2) + + # returns single number slice for 2D array and 2 indices + xnd = XND.new([[1,2,3], [4,5,6]]) + assert_equal xnd[0,0], XND.new(1) + + # returns row for single index in 2D array + x = XND.new [[1,2,3], [4,5,6], [7,8,9]] + assert_equal x[1], XND.new([4,5,6]) + + # returns single column in 2D array + x = XND.new [[1,2,3], [4,5,6], [7,8,9]] + assert_equal x[0..Float::INFINITY, 0], XND.new([1,4,7]) + + # returns the entire array + x = XND.new [[1,2,3], [4,5,6], [7,8,9]] + assert_equal x[0..Float::INFINITY], x + + [ + [ + [ + [11.12-2.3i, -1222+20e8i], + [Complex(Float::INFINITY, Float::INFINITY), -0.00002i], + [0.201+1i, -1+1e301i] + ], "3 * 2 * complex128"], + [ + [ + [11.12-2.3i, nil], + [Complex(Float::INFINITY, Float::INFINITY), nil], + [0.201+1i, -1+1e301i] + ], "3 * 2 * ?complex128"] + ].each do |v, s| + @arr = v + @t = NDT.new s + @x = XND.new v, type: @t + + assert_equal @x.to_a, @arr.to_a + + 0.upto(2) do |i| + assert_equal @x[i].to_a, @arr[i] + end + + 3.times do |i| + 2.times do |k| + assert @x[i][k].value == @arr[i][k] + assert @x[i, k].value == @arr[i][k] + end + end + + assert_equal @x[INF].value, @arr + + ((-3...4).to_a + [Float::INFINITY]).each do |start| + ((-3...4).to_a + [Float::INFINITY]).each do |stop| + [true, false].each do |exclude_end| + # FIXME: add step count when ruby supports it. + arr_s = get_inf_or_normal_range start, stop, exclude_end + r = Range.new(start, stop, exclude_end) + assert_equal @x[r].value, @arr[arr_s] + end + end + end + assert_equal @x[INF, 0].value, @arr.transpose[0] + assert_equal @x[INF, 1].value, @arr.transpose[1] + end + end + + def test_fixed_dim_assign + #### full data + x = XND.empty "2 * 4 * float64" + v = [[0.0, 1.0, 2.0, 3.0], [4.0, 5.0, 6.0, 7.0]] + + # assigns full slice + x[INF] = v + assert_equal x.value, v + + # assigns subarray + x[INF] = v + + x[0] = v[0] = [1.2, -3e45, Float::INFINITY, -322.25] + assert_equal x.value, v + + x[1] = v[1] = [-11.25, 3.355e301, -0.000002, -5000.2] + assert_equal x.value, v + + # assigns single values + 0.upto(1) do |i| + 0.upto(3) do |j| + x[i][j] = v[i][j] = 3.22 * i + j + end + end + + assert_equal x.value, v + + # supports tuple indexing + 0.upto(1) do |i| + 0.upto(3) do |j| + x[i, j] = v[i][j] = -3.002e1 * i + j + end + end + + assert_equal x.value, v + + ### optional data + x = XND.empty "2 * 4 * ?float64" + v = [[10.0, nil, 2.0, 100.12], [nil, nil, 6.0, 7.0]] + + # assigns full slice + x[INF] = v + assert_equal x.value, v + + # assigns subarray + x[INF] = v + + x[0] = v[0] = [nil, 3e45, Float::INFINITY, nil] + assert_equal x.value, v + + x[1] = v[1] = [-11.25, 3.355e301, -0.000002, nil] + assert_equal x.value, v + + # assigns single values + 2.times do |i| + 4.times do |j| + x[i][j] = v[i][j] = -325.99 * i + j + end + end + + assert_equal x.value, v + + # supports assignment by tuple indexing + 2.times do |i| + 4.times do |j| + x[i, j] = v[i][j] = -8.33e1 * i + j + end + end + + assert_equal x.value, v + end +end # class TestFixedDim + +class TestFortran < Minitest::Test + def test_fortran_empty + DTYPE_EMPTY_TEST_CASES.each do |v, s| + [ + [[v] * 0, "!0 * #{s}"], + [[v] * 1, "!1 * #{s}"], + [[v] * 2, "!2 * #{s}"], + [[v] * 1000, "!1000 * #{s}"], + + [[[v] * 0] * 0, "!0 * 0 * #{s}"], + [[[v] * 1] * 0, "!0 * 1 * #{s}"], + [[[v] * 0] * 1, "!1 * 0 * #{s}"], + + [[[v] * 1] * 1, "!1 * 1 * #{s}"], + [[[v] * 2] * 1, "!1 * 2 * #{s}"], + [[[v] * 1] * 2, "!2 * 1 * #{s}"], + [[[v] * 2] * 2, "!2 * 2 * #{s}"], + [[[v] * 3] * 2, "!2 * 3 * #{s}"], + [[[v] * 2] * 3, "!3 * 2 * #{s}"], + [[[v] * 40] * 3, "!3 * 40 * #{s}"] + ].each do |vv, ss| + t = NDT.new ss + x = XND.empty ss + + assert_equal t, x.type + assert_equal vv, x.value + assert_equal vv.size, x.size + end + end + end + + def test_fortran_slices + [ + [[[11.12-2.3i, -1222+20e8i], + [Complex(Float::INFINITY, Float::INFINITY), -0.00002i], + [0.201+1i, -1+1e301i]], "!3 * 2 * complex128"], + [[[11.12-2.3i, nil], + [Complex(Float::INFINITY, Float::INFINITY), nil], + [0.201+1i, -1+1e301i]], "!3 * 2 * ?complex128"] + ].each do |v, s| + arr = v + t = NDT.new s + x = XND.new v, type: t + + (0).upto(2) do |i| + assert_equal x[i].value, arr[i] + end + + (0).upto(2) do |i| + (0).upto(1) do |k| + assert x[i][k].value == arr[i][k] + assert x[i, k].value == arr[i][k] + end + end + + # checks full slice + assert_equal x[INF].to_a, arr + + # slice with ranges + ((-3..-3).to_a + [Float::INFINITY]).each do |start| + ((-3..-3).to_a + [Float::INFINITY]).each do |stop| + [true, false].each do |exclude_end| + # FIXME: add step count loop post Ruby 2.6 + arr_s = get_inf_or_normal_range start, stop, exclude_end + r = Range.new start, stop, exclude_end + assert_equal x[r].value, arr[arr_s] + end + end + end + + # checks column slices" + assert_equal x[INF, 0].value, arr.transpose[0] + assert_equal x[INF, 1].value, arr.transpose[1] + end + end + + def test_fortran_assign + #### Full data + x = XND.empty "!2 * 4 * float64" + v = [[0.0, 1.0, 2.0, 3.0], [4.0, 5.0, 6.0, 7.0]] + + # assigns full slice + x[INF] = v + assert_equal x.value, v + + # assigns subarray + x[INF] = v + + x[0] = v[0] = [1.2, -3e45, Float::INFINITY, -322.25] + assert_equal x.value, v + + x[1] = v[1] = [-11.25, 3.355e301, -0.000002, -5000.2] + assert_equal x.value, v + + + # assigns single values + 0.upto(1) do |i| + 0.upto(3) do |j| + x[i][j] = v[i][j] = 3.22 * i + j + end + end + + assert_equal x.value, v + + # supports tuple indexing + 0.upto(1) do |i| + 0.upto(3) do |j| + x[i, j] = v[i][j] = -3.002e1 * i + j + end + end + + assert_equal x.value, v + + ### Optional data + x = XND.empty "!2 * 4 * ?float64" + v = [[10.0, nil, 2.0, 100.12], [nil, nil, 6.0, 7.0]] + + # assigns full slice + x[INF] = v + assert_equal x.value, v + + # assigns subarray + x[INF] = v + + x[0] = v[0] = [nil, 3e45, Float::INFINITY, nil] + assert_equal x.value, v + + x[1] = v[1] = [-11.25, 3.355e301, -0.000002, nil] + assert_equal x.value, v + + # assigns single values + 2.times do |i| + 4.times do |j| + x[i][j] = v[i][j] = -325.99 * i + j + end + end + + assert_equal x.value, v + + # supports assignment by tuple indexing + 2.times do |i| + 4.times do |j| + x[i, j] = v[i][j] = -8.33e1 * i + j + end + end + + assert_equal x.value, v + end + + def test_equality + x = XND.new [1,2,3,4], type: "!4 * int64" + + # test basic case + assert_strict_equal x, XND.new([1,2,3,4], type: "!4 * int64") + + # tests different shape and/or data + assert_strict_unequal x, XND.new([1,2,3,100], type: "!4 * int64") + assert_strict_unequal x, XND.new([1,2,3], type: "!3 * int64") + assert_strict_unequal x, XND.new([1,2,3,4,5], type: "!5 * int64") + + # tests different shapes + assert_strict_unequal x, XND.new([1,2,3], type: "!3 * int64") + assert_strict_unequal x, XND.new([[1,2,3,4]], type: "!1 * 4 * int64") + assert_strict_unequal x, XND.new([[1,2], [3,4]], type: "!2 * 2 * int64") + + # tests simple multidimensional arrays + x = XND.new([[1,2,3], [4,5,6], [7,8,9], [10,11,12]], type: "!4 * 3 * int64") + y = XND.new([[1,2,3], [4,5,6], [7,8,9], [10,11,12]], type: "!4 * 3 * int64") + + assert_strict_equal x, y + + # equality after assignment + x = XND.new([[1,2,3], [4,5,6], [7,8,9], [10,11,12]], type: "!4 * 3 * int64") + y = XND.new([[1,2,3], [4,5,6], [7,8,9], [10,11,12]], type: "!4 * 3 * int64") + 4.times do |i| + 3.times do |k| + v = y[i, k] + y[i, k] = 100 + + assert_strict_unequal x, y + y[i, k] = v + end + end + + # tests slices + x = XND.new([[1,2,3], [4,5,6], [7,8,9], [10,11,12]], type: "!4 * 3 * int64") + y = XND.new([[1,2,3], [4,5,6]]) + + assert_strict_equal x[0..1], y + + y = XND.new([1,4,7,10], type: "!4 * int64") + + assert_strict_equal x[INF, 0], y + + EQUAL_TEST_CASES.each do |struct| + v = struct.v + t = struct.t + u = struct.u + + [ + [[v] * 0, "!0 * #{t}", "!0 * #{u}"], + [[[v] * 0] * 0, "!0 * 0 * #{t}", "!0 * 0 * #{u}"], + [[[v] * 1] * 0, "!0 * 1 * #{t}", "!0 * 1 * #{u}"], + [[[v] * 0] * 1, "!1 * 0 * #{t}", "!1 * 0 * #{u}"] + ].each do |vv, tt, uu| + ttt = NDT.new tt + + x = XND.new vv, type: ttt + y = XND.new vv, type: ttt + assert_strict_equal x, y + + unless u.nil? + uuu = NDT.new uu + y = XND.new vv, type: uuu + assert_strict_equal x, y + end + end + end + + EQUAL_TEST_CASES.each do |struct| + v = struct.v + t = struct.t + u = struct.u + w = struct.w + eq = struct.eq + + [ + [[v] * 1, "!1 * #{t}", "!1 * #{u}", [0]], + [[v] * 2, "!2 * #{t}", "!2 * #{u}", [1]], + [[v] * 1000, "!1000 * #{t}", "!1000 * #{u}", [961]], + + [[[v] * 1] * 1, "!1 * 1 * #{t}", "!1 * 1 * #{u}", [0, 0]], + [[[v] * 2] * 1, "!1 * 2 * #{t}", "!1 * 2 * #{u}", [0, 1]], + [[[v] * 1] * 2, "!2 * 1 * #{t}", "!2 * 1 * #{u}", [1, 0]], + [[[v] * 2] * 2, "!2 * 2 * #{t}", "!2 * 2 * #{u}", [1, 1]], + [[[v] * 3] * 2, "!2 * 3 * #{t}", "!2 * 3 * #{u}", [1, 2]], + [[[v] * 2] * 3, "!3 * 2 * #{t}", "!3 * 2 * #{u}", [2, 1]], + [[[v] * 40] * 3, "!3 * 40 * #{t}", "!3 * 40 * #{u}", [1, 32]] + ].each do |vv, tt, uu, indices| + ttt = NDT.new tt + + x = XND.new vv, type: ttt + y = XND.new vv, type: ttt + + if eq + assert_strict_equal x, y + else + assert_strict_unequal x, y + end + + unless u.nil? + uuu = NDT.new uu + y = XND.new vv, type: uuu + if eq + assert_strict_equal x, y + else + assert_strict_unequal x, y + end + end + + unless w.nil? + y = XND.new vv, type: ttt + y[*indices] = w + assert_strict_unequal x, y + + y = XND.new vv, type: uuu + y[*indices] = w + assert_strict_unequal x, y + end + end + end + end +end # class TestFortran + +class TestVarDim < Minitest::Test + def test_var_dim_empty + DTYPE_EMPTY_TEST_CASES[0..10].each do |v, s| + [ + [[v] * 0, "var(offsets=[0,0]) * #{s}"], + [[v] * 1, "var(offsets=[0,1]) * #{s}"], + [[v] * 2, "var(offsets=[0,2]) * #{s}"], + [[v] * 1000, "var(offsets=[0,1000]) * #{s}"], + + [[[v] * 0] * 1, "var(offsets=[0,1]) * var(offsets=[0,0]) * #{s}"], + + [[[v], []], "var(offsets=[0,2]) * var(offsets=[0,1,1]) * #{s}"], + [[[], [v]], "var(offsets=[0,2]) * var(offsets=[0,0,1]) * #{s}"], + + [[[v], [v]], "var(offsets=[0,2]) * var(offsets=[0,1,2]) * #{s}"], + [[[v], [v] * 2, [v] * 5], "var(offsets=[0,3]) * var(offsets=[0,1,3,8]) * #{s}"] + ].each do |vv, ss| + t = NDT.new ss + x = XND.empty ss + + assert_equal x.type, t + assert_equal x.value, vv + assert_equal x.size, vv.size + end + end + + # returns empty view + inner = [[0+0i] * 5] * 4 + x = XND.empty "2 * 3 * ref(4 * 5 * complex128)" + + y = x[1][2] + assert_equal y.is_a?(XND), true + assert_equal y.value, inner + + y = x[1, 2] + assert_equal y.is_a?(XND), true + assert_equal y.value, inner + end + + def test_var_dim_assign + ### regular data + + x = XND.empty "var(offsets=[0,2]) * var(offsets=[0,2,5]) * float64" + v = [[0.0, 1.0], [2.0, 3.0, 4.0]] + + + # assigns full slice + x[INF] = v + assert_equal x.value, v + + + # assigns subarray + x[INF] = v + + x[0] = v[0] = [1.2, 2.5] + assert_equal x.value, v + + x[1] = v[1] = [1.2, 2.5, 3.99] + assert_equal x.value, v + + # assigns individual values + 2.times do |i| + x[0][i] = v[0][i] = 100.0 * i + end + + 3.times do |i| + x[1][i] = v[1][i] = 200.0 * i + end + + assert_equal x.value, v + + # assigns tuple + 2.times do |i| + x[0, i] = v[0][i] = 300.0 * i + 1.222 + end + + 3.times do |i| + x[1, i] = v[1][i] = 400.0 * i + 1.333 + end + + assert_equal x.value, v + + ### optional data + x = XND.empty "var(offsets=[0,2]) * var(offsets=[0,2,5]) * ?float64" + v = [[0.0, nil], [nil, 3.0, 4.0]] + + # assigns full slice + x[INF] = v + assert_equal x.value, v + + # assigns subarray + x[INF] = v + + x[0] = v[0] = [nil, 2.0] + assert_equal x.value, v + + x[1] = v[1] = [1.22214, nil, 10.0] + assert_equal x.value, v + + # assigns individual values + 2.times do |i| + x[0][i] = v[0][i] = 3.14 * i + 1.2222 + end + + 3.times do |i| + x[1][i] = v[1][i] = 23.333 * i + end + + assert_equal x.value, v + + # assigns tuple + 2.times do |i| + x[0, i] = v[0][i] = -122.5 * i + 1.222 + end + + 3.times do |i| + x[1, i] = v[1][i] = -3e22 * i + end + + assert_equal x.value, v + end + + def test_var_dim_overflow + s = "var(offsets=[0, 2]) * var(offsets=[0, 1073741824, 2147483648]) * uint8" + assert_raises(ValueError) { XND.empty(s) } + end + + def test_var_dim_match + x = XND.new([Complex(0),Complex(1),Complex(2),Complex(3),Complex(4)], + type: "var(offsets=[0,5]) * complex128") + sig = NDT.new("var... * complex128 -> var... * complex128") + + spec = sig.apply([x.type]) + assert type_equal(spec.out_types[0], x.type) + end + + def test_var_dim_equality + x = XND.new [1,2,3,4], type: "var(offsets=[0,4]) * int64" + + # compares full array + assert_strict_equal x, XND.new([1,2,3,4], type: "var(offsets=[0,4]) * int64") + + # tests for different shape and/or data + assert_strict_unequal x, XND.new([1,2,3,100], type: "var(offsets=[0,4]) * int64") + assert_strict_unequal x, XND.new([1,2,3], type: "var(offsets=[0,3]) * int64") + assert_strict_unequal x, XND.new([1,2,3,4,5], type: "var(offsets=[0,5]) * int64") + + # tests different shape + assert_strict_unequal x, XND.new([1,2,3], type: "var(offsets=[0,3]) * int64") + assert_strict_unequal x, XND.new([[1,2,3,4]], + type: "var(offsets=[0,1]) * var(offsets=[0,4]) * int64") + assert_strict_unequal x, XND.new( + [[1,2], [3,4]], type: "var(offsets=[0,2]) * var(offsets=[0,2,4]) * int64") + + # tests multidimensional arrays + x = XND.new([[1], [2,3,4,5], [6,7], [8,9,10]]) + y = XND.new([[1], [2,3,4,5], [6,7], [8,9,10]]) + + assert_strict_equal(x, y) + + # tests multidim arrays after assign + x = XND.new([[1], [2,3,4,5], [6,7], [8,9,10]]) + y = XND.new([[1], [2,3,4,5], [6,7], [8,9,10]]) + + (0..3).to_a.zip([1,4,2,3]).each do |i, shape| + shape.times do |k| + v = y[i, k] + y[i, k] = 100 + + assert_strict_unequal x, y + + y[i, k] = v + end + end + + # tests slices + x = XND.new([[1], [4,5], [6,7,8], [9,10,11,12]]) + + y = XND.new([[1], [4,5]]) + assert_strict_equal x[0..1], y + + y = XND.new([[4,5], [6,7,8]]) + assert_strict_equal x[1..2], y + + # TODO: make this pass after Ruby 2.6 step-range + # y = XND.new([[12,11,10,9], [5,4]]) + # assert_strict_equal x[(0..) % -2, (0..) % -1], y + + EQUAL_TEST_CASES.each do |struct| + v = struct.v + t = struct.t + u = struct.u + [ + [[v] * 0, "var(offsets=[0,0]) * #{t}", + "var(offsets=[0,0]) * #{u}"], + [[[v] * 0] * 1, "var(offsets=[0,1]) * var(offsets=[0,0]) * #{t}", + "var(offsets=[0,1]) * var(offsets=[0,0]) * #{u}"] + ].each do |vv, tt, uu| + ttt = NDT.new tt + + x = XND.new vv, type: ttt + y = XND.new vv, type: ttt + assert_strict_equal x, y + + unless u.nil? + uuu = NDT.new uu + y = XND.new vv, type: uuu + assert_strict_equal x, y + end + end + end + + EQUAL_TEST_CASES.each do |struct| + v = struct.v + t = struct.t + u = struct.u + w = struct.w + eq = struct.eq + + [ + [[v] * 1, "var(offsets=[0,1]) * #{t}", "var(offsets=[0,1]) * #{u}", [0]], + [[v] * 2, "var(offsets=[0,2]) * #{t}", "var(offsets=[0,2]) * #{u}", [1]], + [[v] * 1000, "var(offsets=[0,1000]) * #{t}", "var(offsets=[0,1000]) * #{u}", [961]], + [[[v], []], "var(offsets=[0,2]) * var(offsets=[0,1,1]) * #{t}", + "var(offsets=[0,2]) * var(offsets=[0,1,1]) * #{u}", [0, 0]], + [[[], [v]], "var(offsets=[0,2]) * var(offsets=[0,0,1]) * #{t}", + "var(offsets=[0,2]) * var(offsets=[0,0,1]) * #{u}", [1, 0]], + [[[v], [v]], "var(offsets=[0,2]) * var(offsets=[0,1,2]) * #{t}", + "var(offsets=[0,2]) * var(offsets=[0,1,2]) * #{u}", [1, 0]], + [[[v], [v] * 2, [v] * 5], "var(offsets=[0,3]) * var(offsets=[0,1,3,8]) * #{t}", + "var(offsets=[0,3]) * var(offsets=[0,1,3,8]) * #{u}", [2, 3]] + ].each do |vv, tt, uu, indices| + ttt = NDT.new tt + + x = XND.new vv, type: ttt + y = XND.new vv, type: ttt + + if eq + assert_strict_equal x, y + else + assert_strict_unequal x, y + end + + unless u.nil? + uuu = NDT.new uu + y = XND.new vv, type: uuu + if eq + assert_strict_equal x, y + else + assert_strict_unequal x, y + end + end + + unless w.nil? + y = XND.new vv, type: ttt + y[*indices] = w + assert_strict_unequal x, y + + unless u.nil? + uuu = NDT.new uu + y = XND.new vv, type: uuu + y[*indices] = w + assert_strict_unequal x, y + end + end + end + end + end +end # class TestVarDim + +class TestSymbolicDim < Minitest::Test + def test_symbolic_dim_raise + DTYPE_EMPTY_TEST_CASES.each do |_, s| + [ + [ValueError, "N * #{s}"], + [ValueError, "10 * N * #{s}"], + [ValueError, "N * 10 * N * #{s}"], + [ValueError, "X * 10 * N * #{s}"] + ].each do |err, ss| + t = NDT.new ss + + assert_raises(ValueError) { XND.empty t } + end + end + end +end # class TestSymbolicDim + +class TestEllipsisDim < Minitest::Test + def test_tuple_empty + DTYPE_EMPTY_TEST_CASES.each do |_, s| + [ + [ValueError, "... * #{s}"], + [ValueError, "Dims... * #{s}"], + [ValueError, "... * 10 * #{s}"], + [ValueError, "B... *2 * 3 * ref(#{s})"], + [ValueError, "A... * 10 * Some(ref(#{s}))"], + [ValueError, "B... * 2 * 3 * Some(ref(ref(#{s})))"] + ].each do |err, ss| + t = NDT.new ss + + assert_raises(err) { XND.empty t } + end + end + end +end # class TestEllipsisDim + +class TestTuple < Minitest::Test + def test_tuple_empty + DTYPE_EMPTY_TEST_CASES.each do |v, s| + [ + [[v], "(#{s})"], + [[[v]], "((#{s}))"], + [[[[v]]], "(((#{s})))"], + + [[[v] * 0], "(0 * #{s})"], + [[[[v] * 0]], "((0 * #{s}))"], + [[[v] * 1], "(1 * #{s})"], + [[[[v] * 1]], "((1 * #{s}))"], + [[[v] * 3], "(3 * #{s})"], + [[[[v] * 3]], "((3 * #{s}))"] + ].each do |vv, ss| + t = NDT.new ss + x = XND.empty ss + + assert_equal x.type, t + assert_equal x.value, vv + assert_equal x.size, vv.size + end + end + end + + def test_tuple_assign + ### regular data + x = XND.empty "(complex64, bytes, string)" + v = [1+20i, "abc".b, "any"] + + # assigns each element + x[0] = v[0] + x[1] = v[1] + x[2] = v[2] + + assert_equal x.value, v + + ### optional data + + x = XND.empty "(complex64, ?bytes, ?string)" + v = [1+20i, nil, "Some"] + + + # assigns each element + x[0] = v[0] + x[1] = v[1] + x[2] = v[2] + + assert_equal x.value, v + + # assigns new each element + v = [-2.5+125i, nil, nil] + x[0] = v[0] + x[1] = v[1] + x[2] = v[2] + + assert_equal x.value, v + + # assigns tuple and individual values + x = XND.new([ + XND::T.new("a", 100, 10.5), + XND::T.new("a", 100, 10.5) + ]) + x[0][1] = 200000000 + + assert_equal x[0][1].value, 200000000 + assert_equal x[0, 1].value, 200000000 + end + + def test_tuple_overflow + # Type cannot be created. + s = "(4611686018427387904 * uint8, 4611686018427387904 * uint8)" + assert_raises(ValueError) { XND.empty(s) } + end + + def test_tuple_optional_values + lst = [[nil, 1, 2], [3, nil, 4], [5, 6, nil]] + x = XND.new(lst, dtype: "(?int64, ?int64, ?int64)") + assert_equal x.value, lst + end + + def test_tuple_equality + ### simple test + x = XND.new XND::T.new(1, 2.0, "3", "123".b) + + # checks simple equality + assert_strict_equal x, XND.new(XND::T.new(1, 2.0, "3", "123".b)) + + # checks simple inequality + assert_strict_unequal x, XND.new(XND::T.new(2, 2.0, "3", "123".b)) + assert_strict_unequal x, XND.new(XND::T.new(1, 2.1, "3", "123".b)) + assert_strict_unequal x, XND.new(XND::T.new(1, 2.0, "", "123".b)) + assert_strict_unequal x, XND.new(XND::T.new(1, 2.0, "345", "123".b)) + assert_strict_unequal x, XND.new(XND::T.new(1, 2.0, "3", "".b)) + assert_strict_unequal x, XND.new(XND::T.new(1, 2.0, "3", "12345".b)) + + ### nested structures + t = "(uint8, + fixed_string(100, 'utf8'), + (complex128, 2 * 3 * (fixed_bytes(size=64, align=32), bytes)), + ref(string))" + + v = [ + 10, + "\u00001234\u00001001abc", + [ + 12.1e244+3i, + [[ + ["123".b, "22".b * 10], + ["123456".b, "23".b * 10], + ["123456789".b, "24".b * 10] + ], + [ + ["1".b, "a".b], + ["12".b, "ab".b], + ["123".b, "abc".b] + ]]], + "xyz" + ] + + x = XND.new v, type: t + y = XND.new v, type: t + + # simple equality + assert_strict_equal x, y + + # unequal after assignment + w = y[0].value + y[0] = 11 + assert_strict_unequal x, y + y[0] = w + assert_strict_equal x, y + + # unequal after UTF-8 assign + w = y[1].value + y[1] = "\U00001234\U00001001abx" + assert_strict_unequal x, y + y[1] = w + assert_strict_equal x, y + + # equal after tuple assign + w = y[2,0].value + y[2,0] = 12.1e244-3i + assert_strict_unequal x, y + y[2,0] = w + assert_strict_equal x, y + + # assigns large index value + w = y[2,1,1,2,0].value + y[2,1,1,2,0] = "abc".b + assert_strict_unequal x, y + + y[2,1,1,2,0] = w + assert_strict_equal x, y + + # assign empty string + w = y[3].value + y[3] = "" + assert_strict_unequal x, y + + y[3] = w + assert_strict_equal x, y + + ### simple corner cases + EQUAL_TEST_CASES.each do |struct| + v = struct.v + t = struct.t + u = struct.u + + [ + [[[v] * 0], "(0 * #{t})", "(0 * #{u})"], + [[[[v] * 0]], "((0 * #{t}))", "((0 * #{u}))"] + ].each do |vv, tt, uu| + uu = uu + vv = vv + tt = tt + ttt = NDT.new tt + + x = XND.new vv, type: ttt + y = XND.new(vv, type: ttt) + assert_strict_equal x, y + + unless u.nil? + uuu = NDT.new uu + y = XND.new vv, type: uuu + assert_strict_equal x, y + end + end + end + + # tests complex corner cases + EQUAL_TEST_CASES.each do |struct| + v = struct.v + t = struct.t + u = struct.u + w = struct.w + eq = struct.eq + + [ + [[v], "(#{t})", "(#{u})", [0]], + [[[v]], "((#{t}))", "((#{u}))", [0, 0]], + [[[[v]]], "(((#{t})))", "(((#{u})))", [0, 0, 0]], + + [[[v] * 1], "(1 * #{t})", "(1 * #{u})", [0, 0]], + [[[[v] * 1]], "((1 * #{t}))", "((1 * #{u}))", [0, 0, 0]], + [[[v] * 3], "(3 * #{t})", "(3 * #{u})", [0, 2]] + ].each do |vv, tt, uu, indices| + ttt = NDT.new tt + + x = XND.new vv, type: ttt + y = XND.new vv, type: ttt + if eq + assert_strict_equal x, y + else + assert_strict_unequal x, y + end + + unless u.nil? + uuu = NDT.new uu + y = XND.new vv, type: uuu + if eq + assert_strict_equal x, y + else + assert_strict_unequal x, y + end + end + + unless w.nil? + y = XND.new vv, type: ttt + y[*indices] = w + assert_strict_unequal x, y + + unless u.nil? + uuu = NDT.new uu + y = XND.new vv, type: uuu + y[*indices] = w + assert_strict_unequal x, y + end + end + end + end + end +end # class TestTuple + +class TestRecord < Minitest::Test + def test_record_empty + DTYPE_EMPTY_TEST_CASES.each do |v, s| + [ + [{'x' => v}, "{x: #{s}}"], + [{'x' => {'y' => v}}, "{x: {y: #{s}}}"], + + [{'x' => [v] * 0}, "{x: 0 * #{s}}"], + [{'x' => {'y' => [v] * 0}}, "{x: {y: 0 * #{s}}}"], + [{'x' => [v] * 1}, "{x: 1 * #{s}}"], + [{'x' => [v] * 3}, "{x: 3 * #{s}}"] + ].each do |vv, ss| + t = NDT.new ss + x = XND.empty ss + + assert_equal x.type , t + assert_equal x.value, vv + assert_equal x.size, vv.size + end + end + end + + def test_record_assign + ### assigns regular data + x = XND.empty "{x: complex64, y: bytes, z: string}" + v = { 'x' => 1+20i, 'y' => "abc".b, 'z' => "any" } + + x['x'] = v['x'] + x['y'] = v['y'] + x['z'] = v['z'] + + assert_equal x.value, v + + ### optional data + x = XND.empty "{x: complex64, y: ?bytes, z: ?string}" + [ + { 'x' => 1+20i, 'y' => nil, 'z' => "Some" }, + { 'x' => -2.5+125i, 'y' => nil, 'z' => nil } + ].each do |v| + x['x'] = v['x'] + x['y'] = v['y'] + x['z'] = v['z'] + + assert_equal x.value, v + end + end + + def test_record_overflow + # Type cannot be created. + s = "{a: 4611686018427387904 * uint8, b: 4611686018427387904 * uint8}" + assert_raises(ValueError) { XND.empty(s) } + end + + def test_record_optional_values + lst = [ + {'a' => nil, 'b' => 2, 'c' => 3}, + {'a'=> 4, 'b' => nil, 'c' => 5}, + {'a'=> 5, 'b' => 6, 'c'=> nil} + ] + x = XND.new(lst, dtype: "{a: ?int64, b: ?int64, c: ?int64}") + + assert_equal x.value, lst + end + + def test_record_equality + ### simple tests + x = XND.new({'a' => 1, 'b' => 2.0, 'c' => "3", 'd' => "123".b}) + + assert_strict_equal x, XND.new({'a' => 1, 'b' => 2.0, 'c' => "3", 'd' => "123".b}) + + assert_strict_unequal x, XND.new({'z' => 1, 'b' => 2.0, 'c' => "3", 'd' => "123".b}) + assert_strict_unequal x, XND.new({'a' => 2, 'b' => 2.0, 'c' => "3", 'd' => "123".b}) + assert_strict_unequal x, XND.new({'a' => 1, 'b' => 2.1, 'c' => "3", 'd' => "123".b}) + assert_strict_unequal x, XND.new({'a' => 1, 'b' => 2.0, 'c' => "", 'd' => "123".b}) + assert_strict_unequal x, XND.new({'a' => 1, 'b' => 2.0, 'c' => "345", 'd' => "123"}) + assert_strict_unequal x, XND.new({'a' => 1, 'b' => 2.0, 'c' => "3", 'd' => "".b}) + assert_strict_unequal x, XND.new({'a' => 1, 'b' => 2.0, 'c' => "3", 'd' => "12345".b}) + + ### nested structures + t = " + {a: uint8, + b: fixed_string(100, 'utf8'), + c: {x: complex128, + y: 2 * 3 * {v: fixed_bytes(size=64, align=32), + u: bytes}}, + d: ref(string)} + " + v = { + 'a' => 10, + 'b' => "\U00001234\U00001001abc", + 'c' => {'x' => 12.1e244+3i, + 'y' => [[{'v' => "123".b, 'u' => "22".b * 10}, + {'v' => "123456".b, 'u' => "23".b * 10}, + {'v' => "123456789".b, 'u' => "24".b * 10}], + [{'v' => "1".b, 'u' => "a".b}, + {'v' => "12".b, 'u' => "ab".b}, + {'v' => "123".b, 'u' => "abc".b}]] + }, + 'd' => "xyz" + } + + x = XND.new v, type: t + y = XND.new v, type: t + assert_strict_equal x, y + + w = y[0].value + y[0] = 11 + assert_strict_unequal x, y + y[0] = w + assert_strict_equal x, y + + w = y[1].value + y[1] = "\U00001234\U00001001abx" + assert_strict_unequal x, y + y[1] = w + assert_strict_equal x, y + + w = y[2,0].value + y[2,0] = 12.1e244-3i + assert_strict_unequal x, y + + y[2, 0] = w + assert_strict_equal x, y + + w = y[2,1,1,2,0].value + y[2,1,1,2,0] = "abc".b + assert_strict_unequal x, y + + y[2,1,1,2,0] = w + assert_strict_equal x, y + + w = y[3].value + y[3] = "" + assert_strict_unequal x, y + y[3] = w + assert_strict_equal x, y + + # test corner cases + EQUAL_TEST_CASES.each do |struct| + v = struct.v + t = struct.t + u = struct.u + + [ + [{'x' => [v] * 0}, "{x: 0 * #{t}}", "{x: 0 * #{u}}"], + [{'x' => {'y' => [v] * 0}}, "{x: {y: 0 * #{t}}}", "{x: {y: 0 * #{u}}}"] + ].each do |vv, tt, uu| + ttt = NDT.new tt + + x = XND.new vv, type: ttt + + y = XND.new vv, type: ttt + assert_strict_equal x, y + + unless u.nil? + uuu = NDT.new uu + y = XND.new vv, type: uuu + assert_strict_equal x, y + end + end + end + + # test many dtypes + EQUAL_TEST_CASES.each do |struct| + v = struct.v + t = struct.t + u = struct.u + w = struct.w + eq = struct.eq + + [ + [{'x' => v}, "{x: #{t}}", "{x: #{u}}", [0]], + [{'x' => {'y' => v}}, "{x: {y: #{t}}}", "{x: {y: #{u}}}", [0, 0]], + [{'x' => [v] * 1}, "{x: 1 * #{t}}", "{x: 1 * #{u}}", [0, 0]], + [{'x' => [v] * 3}, "{x: 3 * #{t}}", "{x: 3 * #{u}}", [0, 2]] + ].each do |vv, tt, uu, indices| + ttt = NDT.new tt + + x = XND.new vv, type: ttt + + y = XND.new vv, type: ttt + if eq + assert_strict_equal x, y + else + assert_strict_unequal x, y + end + + unless u.nil? + uuu = NDT.new uu + y = XND.new vv, type: uuu + if eq + assert_strict_equal x, y + else + assert_strict_unequal x, y + end + end + + unless w.nil? + y = XND.new vv, type: ttt + y[*indices] = w + + assert_strict_unequal x, y + end + end + end + end +end # class TestRecord + +class TestRef < Minitest::Test + def test_ref_empty + DTYPE_EMPTY_TEST_CASES.each do |v, s| + [ + [v, "ref(#{s})"], + [v, "ref(ref(#{s}))"], + [v, "ref(ref(ref(#{s})))"], + + [[v] * 0, "ref(0 * #{s})"], + [[v] * 0, "ref(ref(0 * #{s}))"], + [[v] * 0, "ref(ref(ref(0 * #{s})))"], + [[v] * 1, "ref(1 * #{s})"], + [[v] * 1, "ref(ref(1 * #{s}))"], + [[v] * 1, "ref(ref(ref(1 * #{s})))"], + [[v] * 3, "ref(3 * #{s})"], + [[v] * 3, "ref(ref(3 * #{s}))"], + [[v] * 3, "ref(ref(ref(3 * #{s})))"] + ].each do |vv, ss| + t = NDT.new ss + x = XND.empty ss + + assert x.type == t + assert x.value == vv + end + end + end + + def test_ref_empty_view + # If a ref is a dtype but contains an array itself, indexing should + # return a view and not a Python value. + inner = [[0+0i] * 5] * 4 + x = XND.empty("2 * 3 * ref(4 * 5 * complex128)") + + y = x[1][2] + assert_kind_of XND, y + assert_equal inner, y.value + + y = x[1, 2] + assert_kind_of XND, y + assert_equal inner, y.value + end + + def test_ref_indexing + # FIXME: If a ref is a dtype but contains an array itself, indexing through + # the ref should work transparently. Make equality transparent. + inner = [['a', 'b', 'c', 'd', 'e'], + ['f', 'g', 'h', 'i', 'j'], + ['k', 'l', 'm', 'n', 'o'], + ['p', 'q', 'r', 's', 't']] + v = [[inner] * 3] * 2 + x = XND.new(v, type: "2 * 3 * ref(4 * 5 * string)") + + (0).upto(1) do |i| + (0).upto(2) do |j| + (0).upto(3) do |k| + (0).upto(4) do |l| + assert_equal x[i][j][k][l], inner[k][l] + assert_equal x[i, j, k, l], inner[k][l] + end + end + end + end + end + + def test_ref_assign + # If a ref is a dtype but contains an array itself, assigning through + # the ref should work transparently. + inner = [['a', 'b', 'c', 'd', 'e'], + ['f', 'g', 'h', 'i', 'j'], + ['k', 'l', 'm', 'n', 'o'], + ['p', 'q', 'r', 's', 't']] + v = [[inner] * 3] * 2 + + x = XND.new(v, type: "2 * 3 * ref(4 * 5 * string)") + + 2.times do |i| + 3.times do |j| + 4.times do |k| + 5.times do |l| + x[i, j, k, l] = inner[k][l] = "#{k * 5 + l}" + end + end + end + end + + assert_equal x.value, v + + 2.times do |i| + 3.times do |j| + 4.times do |k| + 5.times do |l| + x[i, j, k, l] = inner[k][l] = "#{k * 5 + l}" + end + end + end + end + + assert_equal x.value, v + end + + def test_ref_equality + x = XND.new [1,2,3,4], type: "ref(4 * float32)" + + assert_strict_equal x, XND.new([1,2,3,4], type: "ref(4 * float32)") + + assert_strict_unequal x, XND.new([1,2,3,4,5], type: "ref(5 * float32)") + assert_strict_unequal x, XND.new([1,2,3], type: "ref(3 * float32)") + assert_strict_unequal x, XND.new([1,2,3,43,5], type: "ref(5 * float32)") + + # corner cases and many dtypes + EQUAL_TEST_CASES.each do |struct| + v = struct.v + t = struct.t + u = struct.u + + [ + [[v] * 0, "ref(0 * #{t})", "ref(0 * #{u})"], + [[v] * 0, "ref(ref(0 * #{t}))", "ref(ref(0 * #{u}))"], + [[v] * 0, "ref(ref(ref(0 * #{t})))", "ref(ref(ref(0 * #{u})))"] + ].each do |vv, tt, uu| + ttt = NDT.new tt + + x = XND.new vv, type: ttt + y = XND.new vv, type: ttt + assert_strict_equal x, y + + unless u.nil? + uuu = NDT.new uu + y = XND.new vv, type: uuu + assert_strict_equal x, y + end + end + end + + # many dtypes and indices + EQUAL_TEST_CASES.each do |struct| + v = struct.v + t = struct.t + u = struct.u + w = struct.w + eq = struct.eq + + [ + [v, "ref(#{t})", "ref(#{u})", []], + [v, "ref(ref(#{t}))", "ref(ref(#{u}))", []], + [v, "ref(ref(ref(#{t})))", "ref(ref(ref(#{u})))", []], + [[v] * 1, "ref(1 * #{t})", "ref(1 * #{u})", 0], + [[v] * 3, "ref(3 * #{t})", "ref(3 * #{u})", 2] + ].each do |vv, tt, uu, indices| + ttt = NDT.new tt + + x = XND.new vv, type: ttt + + y = XND.new vv, type: ttt + if eq + assert_strict_equal x, y + else + assert_strict_unequal x, y + end + + unless u.nil? + uuu = NDT.new uu + y = XND.new vv, type: uuu + if eq + assert_strict_equal x, y + else + assert_strict_unequal x, y + end + end + + unless w.nil? + y = XND.new vv, type: ttt + y[indices] = w + + assert_strict_unequal x, y + end + end + end + end +end # class TestRef + +class TestConstr < Minitest::Test + def test_constr_empty + DTYPE_EMPTY_TEST_CASES.each do |v, s| + [ + [v, "SomeConstr(#{s})"], + [v, "Just(Some(#{s}))"], + + [[v] * 0, "Some(0 * #{s})"], + [[v] * 1, "Some(1 * #{s})"], + [[v] * 3, "Maybe(3 * #{s})"] + ].each do |vv, ss| + t = NDT.new ss + x = XND.empty ss + + assert x.type == t + assert x.value == vv + if vv == 0 + assert_raises(NoMethodError) { x.size } + end + end + end + end + + def test_constr_empty_view + # If a constr is a dtype but contains an array itself, indexing should + # return a view and not a Python value. + inner = [[""] * 5] * 4 + x = XND.empty("2 * 3 * InnerArray(4 * 5 * string)") + + y = x[1][2] + assert_kind_of XND, y + assert_equal y.value, inner + + y = x[1, 2] + assert_kind_of XND, y + assert_equal y.value, inner + end + + def test_constr_indexing + # FIXME: If a constr is a dtype but contains an array itself, indexing through + # the constructor should work transparently. However, for now it returns + # an XND object, however this will likely change in the future. + inner = [['a', 'b', 'c', 'd', 'e'], + ['f', 'g', 'h', 'i', 'j'], + ['k', 'l', 'm', 'n', 'o'], + ['p', 'q', 'r', 's', 't']] + v = [[inner] * 3] * 2 + x = XND.new(v, type: "2 * 3 * InnerArray(4 * 5 * string)") + + (0).upto(1) do |i| + (0).upto(2) do |j| + (0).upto(3) do |k| + (0).upto(4) do |l| + + assert_equal x[i][j][k][l].value, inner[k][l] + assert_equal x[i, j, k, l].value, inner[k][l] + end + end + end + end + end + + def test_constr_assign + # If a constr is a dtype but contains an array itself, assigning through + # the constructor should work transparently. + inner = [['a', 'b', 'c', 'd', 'e'], + ['f', 'g', 'h', 'i', 'j'], + ['k', 'l', 'm', 'n', 'o'], + ['p', 'q', 'r', 's', 't']] + + v = [[inner] * 3] * 2 + x = XND.new(v, type: "2 * 3 * InnerArray(4 * 5 * string)") + + 2.times do |i| + 3.times do |j| + 4.times do |k| + 5.times do |l| + x[i][j][k][l] = inner[k][l] = "#{k * 5 + l}" + end + end + end + end + + assert_equal x.value, v + + 2.times do |i| + 3.times do |j| + 4.times do |k| + 5.times do |l| + x[i][j][k][l] = inner[k][l] = "#{k * 5 + l + 1}" + end + end + end + end + + assert_equal x.value, v + end + + def test_constr_equality + # simple tests + x = XND.new [1,2,3,4], type: "A(4 * float32)" + + assert_strict_equal x, XND.new([1,2,3,4], type: "A(4 * float32)") + + assert_strict_unequal x, XND.new([1,2,3,4], type: "B(4 * float32)") + assert_strict_unequal x, XND.new([1,2,3,4,5], type: "A(5 * float32)") + assert_strict_unequal x, XND.new([1,2,3], type: "A(3 * float32)") + assert_strict_unequal x, XND.new([1,2,3,4,55], type: "A(5 * float32)") + + # corner cases and dtypes + EQUAL_TEST_CASES.each do |struct| + v = struct.v + t = struct.t + u = struct.u + + [ + [[v] * 0, "A(0 * #{t})", "A(0 * #{u})"], + [[v] * 0, "A(B(0 * #{t}))", "A(B(0 * #{u}))"], + [[v] * 0, "A(B(C(0 * #{t})))", "A(B(C(0 * #{u})))"] + ].each do |vv, tt, uu| + ttt = NDT.new tt + + x = XND.new vv, type: ttt + + y = XND.new vv, type: ttt + assert_strict_equal x, y + + unless u.nil? + uuu = NDT.new uu + y = XND.new vv, type: uuu + assert_strict_equal x, y + end + end + end + + # more dtypes + EQUAL_TEST_CASES.each do |struct| + v = struct.v + t = struct.t + u = struct.u + w = struct.w + eq = struct.eq + + [ + [v, "A(#{t})", "A(#{u})", []], + [v, "A(B(#{t}))", "A(B(#{u}))", []], + [v, "A(B(C(#{t})))", "A(B(C(#{u})))", []], + [[v] * 1, "A(1 * #{t})", "A(1 * #{u})", 0], + [[v] * 3, "A(3 * #{t})", "A(3 * #{u})", 2] + ].each do |vv, tt, uu, indices| + ttt = NDT.new tt + + x = XND.new vv, type: ttt + + y = XND.new vv, type: ttt + if eq + assert_strict_equal x, y + else + assert_strict_unequal x, y + end + + unless u.nil? + uuu = NDT.new uu + y = XND.new vv, type: uuu + if eq + assert_strict_equal x, y + else + assert_strict_unequal x, y + end + end + + unless w.nil? + y = XND.new vv, type: ttt + y[indices] = w + + assert_strict_unequal x, y + end + end + end + end +end # class TestConstr + +class TestNominal < Minitest::Test + def test_nominal_empty + c = 0 + DTYPE_EMPTY_TEST_CASES.each do |v, s| + NDT.typedef "some#{c}", s + NDT.typedef "just#{c}", "some#{c}" + + [ + [v, "some#{c}"], + [v, "just#{c}"] + ].each do |vv, ss| + t = NDT.new ss + x = XND.empty ss + + assert x.type == t + assert x.value == vv + if vv == 0 + assert_raises(NoMethodError) { x.size } + end + end + c += 1 + end + end + + def test_nominal_empty_view + # If a typedef is a dtype but contains an array itself, indexing should + # return a view and not a Python value. + NDT.typedef("inner_array", "4 * 5 * string") + inner = [[""] * 5] * 4 + x = XND.empty("2 * 3 * inner_array") + + y = x[1][2] + assert_equal y.is_a?(XND), true + assert_equal y.value, inner + + y = x[1, 2] + assert_equal y.is_a?(XND), true + assert_equal y.value, inner + end + + def test_nominal_indexing + # FIXME: If a typedef is a dtype but contains an array itself, indexing through + # the constructor should work transparently. + NDT.typedef("inner", "4 * 5 * string") + inner = [['a', 'b', 'c', 'd', 'e'], + ['f', 'g', 'h', 'i', 'j'], + ['k', 'l', 'm', 'n', 'o'], + ['p', 'q', 'r', 's', 't']] + v = [[inner] * 3] * 2 + x = XND.new(v, type: "2 * 3 * inner") + + (0).upto(1) do |i| + (0).upto(2) do |j| + (0).upto(3) do |k| + (0).upto(4) do |l| + assert_equal x[i][j][k][l], inner[k][l] + assert_equal x[i, j, k, l], inner[k][l] + end + end + end + end + end + + def test_nominal_assign + # If a typedef is a dtype but contains an array itself, assigning through + # the constructor should work transparently. + NDT.typedef("in", "4 * 5 * string") + inner = [['a', 'b', 'c', 'd', 'e'], + ['f', 'g', 'h', 'i', 'j'], + ['k', 'l', 'm', 'n', 'o'], + ['p', 'q', 'r', 's', 't']] + + v = [[inner] * 3] * 2 + x = XND.new(v, type: "2 * 3 * in") + + 2.times do |i| + 3.times do |j| + 4.times do |k| + 5.times do |l| + x[i][j][k][l] = inner[k][l] = "#{k * 5 + l}" + end + end + end + end + + assert_equal v, x.value + + 2.times do |i| + 3.times do |j| + 4.times do |k| + 5.times do |l| + x[i][j][k][l] = inner[k][l] = "#{k * 5 + l + 1}" + end + end + end + end + + assert_equal v, x.value + end + + def test_nominal_error + assert_raises(ValueError) { XND.empty("undefined_t") } + end + + def test_nominal_equality + NDT.typedef "some1000", "4 * float32" + NDT.typedef "some1001", "4 * float32" + + x = XND.new([1,2,3,4], type: "some1000") + + assert_strict_equal x, XND.new([1,2,3,4], type: "some1000") + + assert_strict_unequal x, XND.new([1,2,3,4], type: "some1001") + assert_strict_unequal x, XND.new([1,2,3,5], type: "some1000") + end +end # class TestNominal + +class TestScalarKind < Minitest::Test + def test_scalar_kind + assert_raises(ValueError) { XND.empty("Scalar") } + end +end # class TestScalarKind + +class TestCategorical < Minitest::Test + def test_categorical_empty + # Categorical values are stored as indices into the type's categories. + # Since empty xnd objects are initialized to zero, the value of an + # empty categorical entry is always the value of the first category. + # This is safe, since categorical types must have at least one entry. + r = {'a' => "", 'b' => 1.2} + rt = "{a: string, b: categorical(1.2, 10.0, NA)}" + + [ + ["January", "categorical('January')"], + [[nil], "(categorical(NA, 'January', 'August'))"], + [[[1.2] * 2] * 10, "10 * 2 * categorical(1.2, 10.0, NA)"], + [[[100] * 2] * 10, "10 * 2 * categorical(100, 'mixed')"], + [[[r] * 2] * 10, "10 * 2 * #{rt}"], + [[[r] * 2, [r] * 5, [r] * 3], "var(offsets=[0,3]) * var(offsets=[0,2,7,10]) * #{rt}"] + ].each do |v, s| + + t = NDT.new s + x = XND.empty s + + assert_equal x.type, t + assert_equal x.value, v + end + end + + def test_categorical_assign + s = "2 * categorical(NA, 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December')" + x = XND.new [nil, nil], type: s + + # assigns regular data + x[0] = "August" + x[1] = "December" + + assert_equal x.value, ["August", "December"] + + # assigns nil + x[0] = nil + x[1] = "December" + + assert_equal x.value, [nil, "December"] + end + + def test_categorical_equality + t = "3 * categorical(NA, 'January', 'August')" + x = XND.new ['August', 'January', 'January'], type: t + + y = XND.new ['August', 'January', 'January'], type: t + assert_strict_equal x, y + + y = XND.new ['August', 'January', 'August'], type: t + assert_strict_unequal x, y + + x = XND.new ['August', nil, 'August'], type: t + y = XND.new ['August', nil, 'August'], type: t + + assert_strict_unequal x, y + end +end # class TestCategorical + +class TestFixedStringKind < Minitest::Test + def test_fixed_string_kind + assert_raises(ValueError) { XND.empty("FixedString") } + end +end # class TestFixedStringKind + +class TestFixedString < Minitest::Test + def test_fixed_string_empty + # tests kind of string + assert_raises(ValueError) { + XND.empty "FixedString" + } + + [ + ["fixed_string(1)", ""], + ["fixed_string(3)", "" * 3], + ["fixed_string(1, 'ascii')", ""], + ["fixed_string(3, 'utf8')", "" * 3], + ["fixed_string(3, 'utf16')", "" * 3], + ["fixed_string(3, 'utf32')", "" * 3], + ["2 * fixed_string(3, 'utf32')", ["" * 3] * 2], + ].each do |s, v| + + t = NDT.new s + x = XND.empty s + + assert_equal x.type, t + assert_equal x.value, v + end + end + + def test_fixed_string + skip "figure out the best way to do this in ruby." + # creates FixedString utf16 + t = "2 * fixed_string(3, 'utf16')" + v = ["\u1111\u2222\u3333", "\u1112\u2223\u3334"] + x = XND.new v, type: t + + assert_equal x.value, v.map { |q| q.encode("UTF-16") } + + # creates FixedString utf32 - figure a way to specify 32bit codepoints + t = "2 * fixed_string(3, 'utf32')" + v = ["\x00\x01\x11\x11\x00\x02\x22\x22\x00\x03\x33\x33".encode('UTF-32'), + "\x00\x01\x11\x12\x00\x02\x22\x23\x00\x03\x33\x34".encode('UTF-32')] + x = XND.new v, type: t + + assert_equal x.value, v + end + + def test_fixed_string_equality + [ + ["fixed_string(1)", "", "x"], + ["fixed_string(3)", "y" * 3, "yyz"], + ["fixed_string(1, 'ascii')", "a".b, "b".b], + ["fixed_string(3, 'utf8')", "a" * 3, "abc"], + #["fixed_string(3, 'utf16')", "\u1234" * 3, "\u1234\u1235\u1234"], + #["fixed_string(3, 'utf32')", "\U00001234" * 3, "\U00001234\U00001234\U00001235"] + ].each do |t, v, w| + x = XND.new v, type: t + y = XND.new v, type: t + + assert_strict_equal x, y + + y[[]] = w + + assert_strict_unequal x, y + end + end + + def test_fixed_string_assign + skip "Figure how to deal with UTF-32 strings in Ruby." + + t = "2 * fixed_string(3, 'utf32')" + v = ["\U00011111\U00022222\U00033333", "\U00011112\U00022223\U00033334"] + x = XND.new(v, type: t) + + x[0] = "a" + assert_equal x.value, ["a", "\U00011112\U00022223\U00033334"] + + x[0] = "a\x00\x00" + assert_equal x.value, ["a", "\U00011112\U00022223\U00033334"] + + x[1] = "b\x00c" + assert_equal x.value, ["a", "b\x00c"] + end + + def test_fixed_string_overflow + ["fixed_string(9223372036854775808)", + "fixed_string(4611686018427387904, 'utf16')", + "fixed_string(2305843009213693952, 'utf32')" + ].each do |s| + assert_raises(ValueError) { XND.empty(s) } + end + end +end # class TestFixedString + +class TestFixedBytesKind < Minitest::Test + def test_fixed_bytes_kind + assert_raises(ValueError) { XND.empty("FixedBytes") } + end +end # class TestFixedBytesKind + +class TestFixedBytes < Minitest::Test + def test_fixed_bytes_empty + r = {'a' => "\x00".b * 3, 'b' => "\x00".b * 10} + + [ + ["\x00".b, 'fixed_bytes(size=1)'], + ["\x00".b * 100, 'fixed_bytes(size=100)'], + ["\x00".b * 4, 'fixed_bytes(size=4, align=2)'], + ["\x00".b * 128, 'fixed_bytes(size=128, align=16)'], + [r, '{a: fixed_bytes(size=3), b: fixed_bytes(size=10)}'], + [[[r] * 3] * 2, '2 * 3 * {a: fixed_bytes(size=3), b: fixed_bytes(size=10)}'] + ].each do |v, s| + t = NDT.new s + x = XND.empty s + + assert_equal x.type, t + assert_equal x.value, v + end + end + + def test_fixed_bytes_assign + t = "2 * fixed_bytes(size=3, align=1)" + v = ["abc".b, "123".b] + x = XND.new(v, type: t) + + x[0] = "xyz".b + assert_equal x.value, ["xyz".b, "123".b] + end + + def test_fixed_bytes_overflow + # Type cannot be created. + s = "fixed_bytes(size=9223372036854775808)" + + assert_raises(ValueError) { XND.empty(s) } + end + + def test_fixed_bytes_equality + [ + ["a".b, "fixed_bytes(size=1)", "b".b], + ["a".b * 100, "fixed_bytes(size=100)", "a".b * 99 + "b".b], + ["a".b * 4, "fixed_bytes(size=4, align=2)", "a".b * 2 + "b".b] + ].each do |v, t, w| + x = XND.new v, type: t + y = XND.new v, type: t + + assert_strict_equal x, y + + y[[]] = w + assert_strict_unequal x, y + end + + # align + x = XND.new("a".b * 128, type: "fixed_bytes(size=128, align=16)") + y = XND.new("a".b * 128, type: "fixed_bytes(size=128, align=16)") + assert_strict_equal x, y + end +end # class TestFixedBytes + +class TestString < Minitest::Test + def test_string_empty + [ + 'string', + '(string)', + '10 * 2 * string', + '10 * 2 * (string, string)', + '10 * 2 * {a: string, b: string}', + 'var(offsets=[0,3]) * var(offsets=[0,2,7,10]) * {a: string, b: string}' + ].each do |s| + + t = NDT.new s + x = XND.empty s + assert_equal x.type, t + end + + # tests for single value + t = NDT.new "string" + x = XND.empty t + + assert_equal x.type, t + assert_equal x.value, '' + + # tests for multiple values + t = NDT.new "10 * string" + x = XND.empty t + + assert_equal x.type, t + 10.times do |i| + assert_equal x[i], '' + end + end + + def test_string + t = '2 * {a: complex128, b: string}' + x = XND.new([{'a' => 2+3i, 'b' => "thisguy"}, + {'a' => 1+4i, 'b' => "thatguy"}], type: t) + + assert_equal x[0]['b'].value, "thisguy" + assert_equal x[1]['b'].value, "thatguy" + end + + def test_string_assign + t = '2 * {a: complex128, b: string}' + x = XND.new([{ 'a' => 2+3i, 'b' => "thisguy"}, + { 'a' => 1+4i, 'b' => "thatguy" }], type: t) + + x[0] = { 'a' => 220i, 'b' => 'y'} + x[1] = { 'a' => -12i, 'b' => 'z'} + + assert_equal x.value, [{ 'a' => 220i, 'b' => 'y' }, { 'a' => -12i, 'b' => 'z' }] + end + + def test_string_equality + x = XND.new "abc" + + assert_strict_equal x, XND.new("abc") + assert_strict_equal x, XND.new("abc\0\0") + + assert_strict_unequal x, XND.new("acb") + end +end # class TestString + +class TestBytes < Minitest::Test + def test_bytes_empty + r = { 'a' => "".b, 'b' => "".b } + + [ + [''.b, 'bytes(align=16)'], + [[''.b], '(bytes(align=32))'], + [[[''.b] * 2] * 3, '3 * 2 * bytes'], + [[[[''.b, ''.b]] * 2] * 10, '10 * 2 * (bytes, bytes)'], + [[[r] * 2] * 10, '10 * 2 * {a: bytes(align=32), b: bytes(align=1)}'], + [[[r] * 2] * 10, '10 * 2 * {a: bytes(align=1), b: bytes(align=32)}'], + [[[r] * 2, [r] * 5, [r] * 3], 'var(offsets=[0,3]) * var(offsets=[0,2,7,10]) * {a: bytes(align=32), b: bytes}'] + ].each do |v, s| + t = NDT.new s + x = XND.empty t + + assert_equal x.type, t + assert_equal x.value, v + end + end + + def test_bytes_assign + t = "2 * SomeByteArray(3 * bytes)" + inner = ["a".b, "b".b, "c".b] + v = [inner] * 2 + x = XND.new v, type: t + + 2.times do |i| + 3.times do |k| + x[i, k] = inner[k] = ['x'.chr.ord + k].pack("C") + end + end + + assert_equal x.value, v + end +end # class TestBytes + +class TestChar < Minitest::Test + def test_char + assert_raises(ValueError) { XND.empty("char('utf8')")} + assert_raises(ValueError) { XND.new(1, type: "char('utf8')")} + end +end # class TestChar + +class TestBool < Minitest::Test + def test_bool + # from bool + x = XND.new true, type: "bool" + assert_equal x.value, true + + x = XND.new false, type: "bool" + assert_equal x.value, false + + # from int + x = XND.new 1, type: "bool" + assert_equal x.value, true + + x = XND.new 0, type: "bool" + assert_equal x.value, false + + # from object + x = XND.new [1,2,3], type: "bool" + assert_equal x.value, true + + x = XND.new nil, type: "?bool" + assert_nil x.value + + assert_raises(TypeError) { + XND.new nil, type: "bool" + } + end + + def test_bool_equality + assert_strict_equal XND.new(true), XND.new(true) + assert_strict_equal XND.new(false), XND.new(false) + assert_strict_unequal XND.new(true), XND.new(false) + assert_strict_unequal XND.new(false), XND.new(true) + end +end # class TestBool + +class TestSignedKind < Minitest::Test + def test_signed_kind + assert_raises(ValueError) { XND.empty("Signed")} + end +end # class TestSignedKind + +class TestSigned < Minitest::Test + def test_signed + [8, 16, 32, 64].each do |n| + t = "int#{n}" + + v = -2**(n-1) + x = XND.new(v, type: t) + assert_equal x.value, v + assert_raises(RangeError) { XND.new v-1, type: t } + + v = 2**(n-1) - 1 + x = XND.new(v, type: t) + assert_equal x.value, v + assert_raises(RangeError) { XND.new v+1, type: t } + end + end + + def test_signed_equality + ["int8", "int16", "int32", "int64"].each do |t| + assert_strict_equal XND.new(-10, type: t), XND.new(-10, type: t) + assert_strict_unequal XND.new(-10, type: t), XND.new(100, type: t) + end + end +end # class TestSigned + +class TestUnsignedKind < Minitest::Test + def test_unsigned_kind + assert_raises(ValueError) { XND.empty("Unsigned") } + end +end # class TestUnsignedKind + +class TestUnsigned < Minitest::Test + def test_unsigned + [8, 16, 32, 64].each do |n| + t = "uint#{n}" + + v = 0 + x = XND.new v, type: t + assert_equal x.value, v + assert_raises(RangeError) { XND.new v-1, type: t } + + t = "uint#{n}" + + v = 2**n - 2 + x = XND.new v, type: t + assert_equal x.value, v + assert_raises(RangeError) { XND.new v+2, type: t } + end + end + + def test_equality + ["uint8", "uint16", "uint32", "uint64"].each do |t| + assert_strict_equal XND.new(10, type: t), XND.new(10, type: t) + assert_strict_unequal XND.new(10, type: t), XND.new(100, type: t) + end + end +end # class TestUnsigned + +class TestFloatKind < Minitest::Test + def test_float_kind + assert_raises(ValueError) { XND.empty("Float")} + end +end # class TestFloatKind + +class TestFloat < Minitest::Test + def test_float32 + # tests inf bounds + inf = Float("0x1.ffffffp+127") + + assert_raises(RangeError) { XND.new(inf, type: "float32") } + assert_raises(RangeError) { XND.new(-inf, type: "float32") } + + # tests denorm_min bounds + denorm_min = Float("0x1p-149") + + x = XND.new denorm_min, type: "float32" + assert_equal x.value, denorm_min + + # tests lowest bounds + lowest = Float("-0x1.fffffep+127") + + x = XND.new lowest, type: "float32" + assert_equal x.value.nan?, lowest.nan? + + # tests max bounds + max = Float("0x1.fffffep+127") + + x = XND.new max, type: "float32" + assert_equal x.value, max + + # tests special values + x = XND.new Float::INFINITY, type: "float32" + assert_equal x.value.infinite?, 1 + + x = XND.new Float::NAN, type: "float32" + assert_equal x.value.nan?, true + + + # compare + assert_strict_equal XND.new(1.2e7, type: "float32"), + XND.new(1.2e7, type: "float32") + assert_strict_equal XND.new(Float::INFINITY, type: "float32"), + XND.new(Float::INFINITY, type: "float32") + assert_strict_equal XND.new(-Float::INFINITY, type: "float32"), + XND.new(-Float::INFINITY, type: "float32") + + assert_strict_unequal XND.new(1.2e7, type: "float32"), + XND.new(-1.2e7, type: "float32") + assert_strict_unequal XND.new(Float::INFINITY, type: "float32"), + XND.new(-Float::INFINITY, type: "float32") + assert_strict_unequal XND.new(-Float::INFINITY, type: "float32"), + XND.new(Float::INFINITY, type: "float32") + assert_strict_unequal XND.new(Float::NAN, type: "float32"), + XND.new(Float::NAN, type: "float32") + end + + def test_float64 + # tests bounds + denorm_min = Float("0x0.0000000000001p-1022") + lowest = Float("-0x1.fffffffffffffp+1023") + max = Float("0x1.fffffffffffffp+1023") + + x = XND.new denorm_min, type: "float64" + assert_equal x.value, denorm_min + + x = XND.new lowest, type: "float64" + assert_equal x.value, lowest + + x = XND.new max, type: "float64" + assert_equal x.value, max + + + # tests special values + x = XND.new Float::INFINITY, type: "float64" + assert_equal x.value.infinite?, 1 + + x = XND.new Float::NAN, type: "float64" + assert_equal x.value.nan?, true + + # compare + assert_strict_equal XND.new(1.2e7, type: "float64"), + XND.new(1.2e7, type: "float64") + assert_strict_equal XND.new(Float::INFINITY, type: "float64"), + XND.new(Float::INFINITY, type: "float64") + assert_strict_equal XND.new(-Float::INFINITY, type: "float64"), + XND.new(-Float::INFINITY, type: "float64") + + assert_strict_unequal XND.new(1.2e7, type: "float64"), + XND.new(-1.2e7, type: "float64") + assert_strict_unequal XND.new(Float::INFINITY, type: "float64"), + XND.new(-Float::INFINITY, type: "float64") + assert_strict_unequal XND.new(-Float::INFINITY, type: "float64"), + XND.new(Float::INFINITY, type: "float64") + assert_strict_unequal XND.new(Float::NAN, type: "float64"), + XND.new(Float::NAN, type: "float64") + end +end # class Float + +class TestComplexKind < Minitest::Test + def test_complex_kind + assert_raises(ValueError) { XND.empty("Complex") } + end +end # class TestComplexKind + +class TestComplex < Minitest::Test + def test_complex64 + # tests bounds + denorm_min = Float("0x1p-149") + lowest = Float("-0x1.fffffep+127") + max = Float("0x1.fffffep+127") + inf = Float("0x1.ffffffp+127") + + v = Complex(denorm_min, denorm_min) + x = XND.new v, type: "complex64" + assert_equal x.value, v + + v = Complex(lowest, lowest) + x = XND.new v, type: "complex64" + assert_equal x.value, v + + v = Complex(max, max) + x = XND.new v, type: "complex64" + assert_equal x.value, v + + v = Complex(inf, inf) + assert_raises(RangeError) { XND.new v, type: "complex64" } + + v = Complex(-inf, -inf) + assert_raises(RangeError) { XND.new v, type: "complex64" } + + # tests special values + x = XND.new Complex(Float::INFINITY, 0), type: "complex64" + assert_equal x.value.real.infinite?, 1 + assert_equal x.value.imag, 0.0 + + x = XND.new Complex(Float::NAN, 0), type: "complex64" + assert_equal x.value.real.nan?, true + assert_equal x.value.imag, 0.0 + + # compare + t = "complex64" + + inf = Float("0x1.ffffffp+127") + denorm_min = Float("0x1p-149") + lowest = Float("-0x1.fffffep+127") + max = Float("0x1.fffffep+127") + + c = [denorm_min, lowest, max, Float::INFINITY, -Float::INFINITY, Float::NAN] + + c.each do |r| + c.each do |s| + c.each do |i| + c.each do |j| + x = XND.new Complex(r, i), type: t + y = XND.new Complex(s, j), type: t + + if r == s && i == j + assert_strict_equal x, y + else + assert_strict_unequal x, y + end + end + end + end + end + end + + def test_complex128 + # tests bounds + denorm_min = Float("0x0.0000000000001p-1022") + lowest = Float("-0x1.fffffffffffffp+1023") + max = Float("0x1.fffffffffffffp+1023") + + v = Complex(denorm_min, denorm_min) + x = XND.new v, type: "complex128" + assert_equal x.value, v + + v = Complex(lowest, lowest) + x = XND.new v, type: "complex128" + assert_equal x.value, v + + v = Complex(max, max) + x = XND.new v, type: "complex128" + assert_equal x.value, v + + # tests special values + x = XND.new Complex(Float::INFINITY), type: "complex128" + + assert_equal x.value.real.infinite?, 1 + assert_equal x.value.imag, 0.0 + + x = XND.new Complex(Float::NAN), type: "complex128" + + assert_equal x.value.real.nan?, true + assert_equal x.value.imag, 0.0 + + # compare + t = "complex128" + + denorm_min = Float("0x0.0000000000001p-1022") + lowest = Float("-0x1.fffffffffffffp+1023") + max = Float("0x1.fffffffffffffp+1023") + + c = [denorm_min, lowest, max, Float::INFINITY, -Float::INFINITY, Float::NAN] + + c.each do |r| + c.each do |s| + c.each do |i| + c.each do |j| + x = XND.new Complex(r, i), type: t + y = XND.new Complex(s, j), type: t + + if r == s && i == j + assert_strict_equal x, y + else + assert_strict_unequal x, y + end + end + end + end + end + end +end # class TestComplex + +class TestPrimitive < Minitest::Test + def test_primitive_empty + empty_test_cases.each do |value, type_string| + PRIMITIVE.each do |p| + ts = type_string % p + + x = XND.empty ts + + assert_equal x.value, value + assert_equal x.type, NDT.new(ts) + end + end + + empty_test_cases(false).each do |value, type_string| + BOOL_PRIMITIVE.each do |p| + ts = type_string % p + + x = XND.empty ts + + assert_equal x.value, value + assert_equal x.type, NDT.new(ts) + end + end + end +end # class TestPrimitive + +class TestTypevar < Minitest::Test + def test_typevar + [ + "T", + "2 * 10 * T", + "{a: 2 * 10 * T, b: bytes}" + ].each do |ts| + assert_raises(ValueError) { + XND.empty ts + } + end + end +end # class TestTypevar + +class TestTypeInference < Minitest::Test + def test_accumulate + arr = [1,2,3,4,5] + result = [1,3,6,10,15] + + assert_equal XND::TypeInference.accumulate(arr), result + end + + def test_search + data = [[0, 1], [2, 3, 4], [5, 6, 7, 8]] + result = [[3], [2, 3, 4], [0, 1, 2, 3, 4, 5, 6, 7, 8], *(Array.new(MAX_DIM-2) { [] })] + + min_level = MAX_DIM + 1 + max_level = 0 + acc = Array.new(MAX_DIM + 1) { [] } + minmax = [min_level, max_level] + + XND::TypeInference.search max_level, data, acc, minmax + + assert_equal acc, result + assert_equal minmax[0], minmax[1] + end + + def test_data_shapes + # extract shape of nested Array data + data = [[0, 1], [2, 3, 4], [5, 6, 7, 8]] + result = [[0, 1, 2, 3, 4, 5, 6, 7, 8], [[2, 3, 4], [3]]] + + assert_equal XND::TypeInference.data_shapes(data), result + + # empty array + data = [] + result = [[], [[0]]] + + assert_equal XND::TypeInference.data_shapes(data), result + + # empty nested array + data = [[]] + result = [[], [[0], [1]]] + + assert_equal XND::TypeInference.data_shapes(data), result + end + + def test_type_of + # correct ndtype of fixed array + value = [ + [1,2,3], + [5,6,7] + ] + type = NDTypes.new "2 * 3 * int64" + + assert_equal XND::TypeInference.type_of(value), type + + # generates correct ndtype for hash + value = { + "a" => "xyz", + "b" => [1,2,3] + } + type = NDTypes.new "{a : string, b : 3 * int64}" + assert_equal XND::TypeInference.type_of(value), type + end + + def test_tuple + d = {'a' => XND::T.new(2.0, "bytes".b), 'b' => XND::T.new("str", Float::INFINITY) } + typeof_d = "{a: (float64, bytes), b: (string, float64)}" + + [ + [XND::T.new(), "()"], + [XND::T.new(XND::T.new()), "(())"], + [XND::T.new(XND::T.new(), XND::T.new()), "((), ())"], + [XND::T.new(XND::T.new(XND::T.new()), XND::T.new()), "((()), ())"], + [XND::T.new(XND::T.new(XND::T.new()), XND::T.new(XND::T.new(), XND::T.new())), + "((()), ((), ()))"], + [XND::T.new(1, 2, 3), "(int64, int64, int64)"], + [XND::T.new(1.0, 2, "str"), "(float64, int64, string)"], + [XND::T.new(1.0, 2, XND::T.new("str", "bytes".b, d)), + "(float64, int64, (string, bytes, #{typeof_d}))"] + ].each do |v, t| + x = XND.new v + + assert_equal x.type, NDT.new(t) + assert_equal x.value, XND::TypeInference.convert_xnd_t_to_ruby_array(v) + end + end + + def test_record + d = {'a' => XND::T.new(2.0, "bytes".b), 'b' => XND::T.new("str", Float::INFINITY) } + typeof_d = "{a: (float64, bytes), b: (string, float64)}" + + [ + [{}, "{}"], + [{'x' => {}}, "{x: {}}"], + [{'x' => {}, 'y' => {}}, "{x: {}, y: {}}"], + [{'x' => {'y' => {}}, 'z' => {}}, "{x: {y: {}}, z: {}}"], + [{'x' => {'y' => {}}, 'z' => {'a' => {}, 'b' => {}}}, "{x: {y: {}}, z: {a: {}, b: {}}}"], + [d, typeof_d] + ].each do |v, t| + x = XND.new v + + assert_equal x.type, NDT.new(t) + assert_equal x.value, v + end + end + + def test_float64 + d = {'a' => 2.221e100, 'b' => Float::INFINITY} + typeof_d = "{a: float64, b: float64}" + + [ + # 'float64' is the default dtype if there is no data at all. + [[], "0 * float64"], + [[[]], "1 * 0 * float64"], + [[[], []], "2 * 0 * float64"], + [[[[]], [[]]], "2 * 1 * 0 * float64"], + [[[[]], [[], []]], + "var(offsets=[0, 2]) * var(offsets=[0, 1, 3]) * var(offsets=[0, 0, 0, 0]) * float64"], + + [[0.0], "1 * float64"], + [[0.0, 1.2], "2 * float64"], + [[[0.0], [1.2]], "2 * 1 * float64"], + + [d, typeof_d], + [[d] * 2, "2 * %s" % typeof_d], + [[[d] * 2] * 10, "10 * 2 * #{typeof_d}"] + ].each do |v, t| + x = XND.new v + + assert_equal x.type, NDT.new(t) + assert_equal x.value, v + end + end + + def test_complex128 + d = {'a' => 3.123+10i, 'b' => Complex(Float::INFINITY, Float::INFINITY)} + typeof_d = "{a: complex128, b: complex128}" + + [ + [[1+3e300i], "1 * complex128"], + [[-2.2-5i, 1.2-10i], "2 * complex128"], + [[-2.2-5i, 1.2-10i, nil], "3 * ?complex128"], + [[[-1+3i], [-3+5i]], "2 * 1 * complex128"], + + [d, typeof_d], + [[d] * 2, "2 * #{typeof_d}"], + [[[d] * 2] * 10, "10 * 2 * #{typeof_d}"] + ].each do |v, t| + x = XND.new v + + assert_equal x.type, NDT.new(t) + assert_equal x.value, v + end + end + + def test_int64 + t = XND::T.new(1, -2, -3) + typeof_t = "(int64, int64, int64)" + + [ + [[0], "1 * int64"], + [[0, 1], "2 * int64"], + [[[0], [1]], "2 * 1 * int64"], + + [t, typeof_t], + [[t] * 2, "2 * #{typeof_t}"], + [[[t] * 2] * 10, "10 * 2 * #{typeof_t}"] + ].each do |v, t| + x = XND.new v + + assert_equal x.type, NDT.new(t) + assert_equal x.value, XND::TypeInference.convert_xnd_t_to_ruby_array(v) + end + end + + def test_string + t = XND::T.new("supererogatory", "exiguous") + typeof_t = "(string, string)" + + [ + [["mov"], "1 * string"], + [["mov", "$0"], "2 * string"], + [[["cmp"], ["$0"]], "2 * 1 * string"], + + [t, typeof_t], + [[t] * 2, "2 * %s" % typeof_t], + [[[t] * 2] * 10, "10 * 2 * %s" % typeof_t] + ].each do |v, t| + x = XND.new v + + assert_equal x.type, NDT.new(t) + assert_equal x.value, XND::TypeInference.convert_xnd_t_to_ruby_array(v) + end + end + + def test_bytes + t = XND::T.new("lagrange".b, "points".b) + typeof_t = "(bytes, bytes)" + + [ + [["L1".b], "1 * bytes"], + [["L2".b, "L3".b, "L4".b], "3 * bytes"], + [[["L5".b], ["none".b]], "2 * 1 * bytes"], + + [t, typeof_t], + [[t] * 2, "2 * %s" % typeof_t], + [[[t] * 2] * 10, "10 * 2 * %s" % typeof_t] + ].each do |v, t| + x = XND.new v + + assert_equal x.type, NDT.new(t) + assert_equal x.value, XND::TypeInference.convert_xnd_t_to_ruby_array(v) + end + end + + def test_optional + [ + [nil, "?float64"], + [[nil], "1 * ?float64"], + [[nil, nil], "2 * ?float64"], + [[nil, 10], "2 * ?int64"], + [[nil, 'abc'.b], "2 * ?bytes"], + [[nil, 'abc'], "2 * ?string"] + ].each do |v, t| + x = XND.new v + + assert_equal x.type, NDT.new(t) + assert x.value == v + end + + [ + [nil, []], + [[], nil], + [nil, [10]], + [[nil, [0, 1]], [[2, 3]]] + ].each do |v| + assert_raises(NotImplementedError) { XND.new v } + end + end +end # class TestTypeInference + +class TestEach < Minitest::Test + def test_each + DTYPE_EMPTY_TEST_CASES.each do |v, s| + [ + [[[v] * 1] * 1, "!1 * 1 * #{s}"], + [[[v] * 2] * 1, "!1 * 2 * #{s}"], + [[[v] * 1] * 2, "!2 * 1 * #{s}"], + [[[v] * 2] * 2, "2 * 2 * #{s}"], + [[[v] * 3] * 2, "2 * 3 * #{s}"], + [[[v] * 2] * 3, "3 * 2 * #{s}"] + ].each do |vv, ss| + x = XND.new vv, type: ss + + lst = [] + x.each do |v| + lst << v + end + + x.each_with_index do |i, z| + assert_equal z.value, lst[i].value + end + end + end + end +end # class TestEach + +class TestAPI < Minitest::Test + def test_short_value + x = XND.new [1,2] + q = XND::Ellipsis.new + + assert_equal x.short_value(0), [] + assert_equal x.short_value(1), [XND::Ellipsis.new] + assert_equal x.short_value(2), [1, XND::Ellipsis.new] + assert_equal x.short_value(3), [1, 2] + + x = XND.new [[1,2], [3]] + assert_equal [], x.short_value(0) + assert_equal [XND::Ellipsis.new], x.short_value(1) + assert_equal [[1, XND::Ellipsis.new], XND::Ellipsis.new], x.short_value(2) + assert_equal [[1, 2], [3]], x.short_value(3) + assert_raises(ArgumentError) { x.short_value(-1) } + + x = XND.new({'a' => 1, 'b' => 2 }) + assert_equal x.short_value(0), {} + assert_equal x.short_value(3), {'a' => 1, 'b'=> 2} + assert_raises(ArgumentError){ x.short_value(-1) } + end +end # class TestAPI + +class TestToS < Minitest::Test + def test_to_s + lst = [[[{'a'=> 100, 'b' => "xyz", 'c'=> ['abc', 'uvw']}] * 23] * 19] * 10 + x = XND.new lst + r = x.to_s + + assert r.size < 100000 + end +end # class TestToS + +class TestBuffer < Minitest::Test + def test_from_nmatrix + + end + + def test_from_numbuffer + + end + + def test_from_narray + + end +end # class TestBuffer + +class TestSplit < Minitest::Test + def test_split + + end + + def test_split_limit_outer + + end +end # class TestSplit + +class TestView < Minitest::Test + def test_view_subscript + + end + + def test_view_new + + end +end + diff --git a/ruby/xnd.gemspec b/ruby/xnd.gemspec new file mode 100644 index 0000000..f1aaabb --- /dev/null +++ b/ruby/xnd.gemspec @@ -0,0 +1,48 @@ +# coding: utf-8 +$:.unshift File.expand_path("../lib", __FILE__) + +require 'xnd/version.rb' + +def self.get_files + files = [] + ['ext', 'lib', 'spec'].each do |folder| + files.concat Dir.glob "#{folder}/**/*" + end + + files.concat( + ["CONTRIBUTING.md", "Gemfile", "History.md", "xnd.gemspec", + "README.md", "Rakefile" + ]) + + files +end +files = get_files + +RubyXND::DESCRIPTION = < 5.11' + spec.add_development_dependency 'minitest-hooks' + spec.add_development_dependency 'rake-compiler' + spec.add_development_dependency 'pry' + spec.add_development_dependency 'pry-byebug' + + spec.add_runtime_dependency 'ndtypes', '>= 0.2.0dev5' +end