diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 8dfc7ca..0000000 --- a/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -/COPYING -/ChangeLog -/Makefile.in -/Makefile -/aclocal.m4 -/autom4te.cache -/build-aux -/configure -/config.log -/config.status -/luarocks -/luarocks-config.lua -/release-notes-* -/stdlib-*.tar.gz -/*.rockspec diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..cab15a2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,38 @@ +language: python + +sudo: false + +env: + matrix: + - VLUA="lua=5.4" + - VLUA="lua=5.3" + - VLUA="lua=5.2" + - VLUA="lua=5.1" + - VLUA="luajit=2.1" + - VLUA="luajit=2.0" + + +before_install: + - pip install hererocks + - hererocks here --luarocks 3 --$VLUA + - export PATH=$PWD/here/bin:$PATH + + - hererocks tools --luajit=2.0 + - export PATH=$PATH:$PWD/tools/bin + +install: + - luarocks --lua-dir=$PWD/tools --tree=$PWD/tools install ansicolors + - luarocks --lua-dir=$PWD/tools --tree=$PWD/tools install specl + - luarocks --lua-dir=$PWD/tools --tree=$PWD/tools install luacov + +script: + - make all + - luarocks make + - make check SPECL_OPTS='-vfreport --coverage' + +after_success: + - tail luacov.report.out + - bash <(curl -s https://codecov.io/bash) -f luacov.report.out + +notifications: + slack: aspirinc:JyWeNrIdS0J5nf2Pn2BS1cih diff --git a/AUTHORS b/AUTHORS index 0cbc134..9a36600 100644 --- a/AUTHORS +++ b/AUTHORS @@ -6,9 +6,13 @@ be on it, please tell the mailing list (see README for the address). Thanks also to all those who have contributed bug fixes, suggestions and support. +Gary V. Vaughan now maintains stdlib, having rewritten and reorganised +the libraries for hygiene, consistent argument type checking in debug +mode, and object orientation, in addition to adding a lot of new +functionality. -Reuben Thomas started and maintains the standard libraries project, -wrote many of the libraries, and integrated code from other authors. +Reuben Thomas started the standard libraries project, wrote many of the +libraries, and integrated code from other authors. John Belmonte helped set the project up on lua-users, and contributed to the early organisation of the libraries. @@ -21,8 +25,3 @@ private standard library. Johann Hibschman supplied the code on which math.floor and math.round were based. - -Diego Nehab wrote the original version of the mbox parser module. - -Gary V. Vaughan contributed table key support to the tree module, and -the memoize implementation. diff --git a/INSTALL b/INSTALL deleted file mode 100644 index 7d1c323..0000000 --- a/INSTALL +++ /dev/null @@ -1,365 +0,0 @@ -Installation Instructions -************************* - -Copyright (C) 1994, 1995, 1996, 1999, 2000, 2001, 2002, 2004, 2005, -2006, 2007, 2008, 2009 Free Software Foundation, Inc. - - Copying and distribution of this file, with or without modification, -are permitted in any medium without royalty provided the copyright -notice and this notice are preserved. This file is offered as-is, -without warranty of any kind. - -Basic Installation -================== - - Briefly, the shell commands `./configure; make; make install' should -configure, build, and install this package. The following -more-detailed instructions are generic; see the `README' file for -instructions specific to this package. Some packages provide this -`INSTALL' file but do not implement all of the features documented -below. The lack of an optional feature in a given package is not -necessarily a bug. More recommendations for GNU packages can be found -in *note Makefile Conventions: (standards)Makefile Conventions. - - The `configure' shell script attempts to guess correct values for -various system-dependent variables used during compilation. It uses -those values to create a `Makefile' in each directory of the package. -It may also create one or more `.h' files containing system-dependent -definitions. Finally, it creates a shell script `config.status' that -you can run in the future to recreate the current configuration, and a -file `config.log' containing compiler output (useful mainly for -debugging `configure'). - - It can also use an optional file (typically called `config.cache' -and enabled with `--cache-file=config.cache' or simply `-C') that saves -the results of its tests to speed up reconfiguring. Caching is -disabled by default to prevent problems with accidental use of stale -cache files. - - If you need to do unusual things to compile the package, please try -to figure out how `configure' could check whether to do them, and mail -diffs or instructions to the address given in the `README' so they can -be considered for the next release. If you are using the cache, and at -some point `config.cache' contains results you don't want to keep, you -may remove or edit it. - - The file `configure.ac' (or `configure.in') is used to create -`configure' by a program called `autoconf'. You need `configure.ac' if -you want to change it or regenerate `configure' using a newer version -of `autoconf'. - - The simplest way to compile this package is: - - 1. `cd' to the directory containing the package's source code and type - `./configure' to configure the package for your system. - - Running `configure' might take a while. While running, it prints - some messages telling which features it is checking for. - - 2. Type `make' to compile the package. - - 3. Optionally, type `make check' to run any self-tests that come with - the package, generally using the just-built uninstalled binaries. - - 4. Type `make install' to install the programs and any data files and - documentation. When installing into a prefix owned by root, it is - recommended that the package be configured and built as a regular - user, and only the `make install' phase executed with root - privileges. - - 5. Optionally, type `make installcheck' to repeat any self-tests, but - this time using the binaries in their final installed location. - This target does not install anything. Running this target as a - regular user, particularly if the prior `make install' required - root privileges, verifies that the installation completed - correctly. - - 6. You can remove the program binaries and object files from the - source code directory by typing `make clean'. To also remove the - files that `configure' created (so you can compile the package for - a different kind of computer), type `make distclean'. There is - also a `make maintainer-clean' target, but that is intended mainly - for the package's developers. If you use it, you may have to get - all sorts of other programs in order to regenerate files that came - with the distribution. - - 7. Often, you can also type `make uninstall' to remove the installed - files again. In practice, not all packages have tested that - uninstallation works correctly, even though it is required by the - GNU Coding Standards. - - 8. Some packages, particularly those that use Automake, provide `make - distcheck', which can by used by developers to test that all other - targets like `make install' and `make uninstall' work correctly. - This target is generally not run by end users. - -Compilers and Options -===================== - - Some systems require unusual options for compilation or linking that -the `configure' script does not know about. Run `./configure --help' -for details on some of the pertinent environment variables. - - You can give `configure' initial values for configuration parameters -by setting variables in the command line or in the environment. Here -is an example: - - ./configure CC=c99 CFLAGS=-g LIBS=-lposix - - *Note Defining Variables::, for more details. - -Compiling For Multiple Architectures -==================================== - - You can compile the package for more than one kind of computer at the -same time, by placing the object files for each architecture in their -own directory. To do this, you can use GNU `make'. `cd' to the -directory where you want the object files and executables to go and run -the `configure' script. `configure' automatically checks for the -source code in the directory that `configure' is in and in `..'. This -is known as a "VPATH" build. - - With a non-GNU `make', it is safer to compile the package for one -architecture at a time in the source code directory. After you have -installed the package for one architecture, use `make distclean' before -reconfiguring for another architecture. - - On MacOS X 10.5 and later systems, you can create libraries and -executables that work on multiple system types--known as "fat" or -"universal" binaries--by specifying multiple `-arch' options to the -compiler but only a single `-arch' option to the preprocessor. Like -this: - - ./configure CC="gcc -arch i386 -arch x86_64 -arch ppc -arch ppc64" \ - CXX="g++ -arch i386 -arch x86_64 -arch ppc -arch ppc64" \ - CPP="gcc -E" CXXCPP="g++ -E" - - This is not guaranteed to produce working output in all cases, you -may have to build one architecture at a time and combine the results -using the `lipo' tool if you have problems. - -Installation Names -================== - - By default, `make install' installs the package's commands under -`/usr/local/bin', include files under `/usr/local/include', etc. You -can specify an installation prefix other than `/usr/local' by giving -`configure' the option `--prefix=PREFIX', where PREFIX must be an -absolute file name. - - You can specify separate installation prefixes for -architecture-specific files and architecture-independent files. If you -pass the option `--exec-prefix=PREFIX' to `configure', the package uses -PREFIX as the prefix for installing programs and libraries. -Documentation and other data files still use the regular prefix. - - In addition, if you use an unusual directory layout you can give -options like `--bindir=DIR' to specify different values for particular -kinds of files. Run `configure --help' for a list of the directories -you can set and what kinds of files go in them. In general, the -default for these options is expressed in terms of `${prefix}', so that -specifying just `--prefix' will affect all of the other directory -specifications that were not explicitly provided. - - The most portable way to affect installation locations is to pass the -correct locations to `configure'; however, many packages provide one or -both of the following shortcuts of passing variable assignments to the -`make install' command line to change installation locations without -having to reconfigure or recompile. - - The first method involves providing an override variable for each -affected directory. For example, `make install -prefix=/alternate/directory' will choose an alternate location for all -directory configuration variables that were expressed in terms of -`${prefix}'. Any directories that were specified during `configure', -but not in terms of `${prefix}', must each be overridden at install -time for the entire installation to be relocated. The approach of -makefile variable overrides for each directory variable is required by -the GNU Coding Standards, and ideally causes no recompilation. -However, some platforms have known limitations with the semantics of -shared libraries that end up requiring recompilation when using this -method, particularly noticeable in packages that use GNU Libtool. - - The second method involves providing the `DESTDIR' variable. For -example, `make install DESTDIR=/alternate/directory' will prepend -`/alternate/directory' before all installation names. The approach of -`DESTDIR' overrides is not required by the GNU Coding Standards, and -does not work on platforms that have drive letters. On the other hand, -it does better at avoiding recompilation issues, and works well even -when some directory options were not specified in terms of `${prefix}' -at `configure' time. - -Optional Features -================= - - If the package supports it, you can cause programs to be installed -with an extra prefix or suffix on their names by giving `configure' the -option `--program-prefix=PREFIX' or `--program-suffix=SUFFIX'. - - Some packages pay attention to `--enable-FEATURE' options to -`configure', where FEATURE indicates an optional part of the package. -They may also pay attention to `--with-PACKAGE' options, where PACKAGE -is something like `gnu-as' or `x' (for the X Window System). The -`README' should mention any `--enable-' and `--with-' options that the -package recognizes. - - For packages that use the X Window System, `configure' can usually -find the X include and library files automatically, but if it doesn't, -you can use the `configure' options `--x-includes=DIR' and -`--x-libraries=DIR' to specify their locations. - - Some packages offer the ability to configure how verbose the -execution of `make' will be. For these packages, running `./configure ---enable-silent-rules' sets the default to minimal output, which can be -overridden with `make V=1'; while running `./configure ---disable-silent-rules' sets the default to verbose, which can be -overridden with `make V=0'. - -Particular systems -================== - - On HP-UX, the default C compiler is not ANSI C compatible. If GNU -CC is not installed, it is recommended to use the following options in -order to use an ANSI C compiler: - - ./configure CC="cc -Ae -D_XOPEN_SOURCE=500" - -and if that doesn't work, install pre-built binaries of GCC for HP-UX. - - On OSF/1 a.k.a. Tru64, some versions of the default C compiler cannot -parse its `' header file. The option `-nodtk' can be used as -a workaround. If GNU CC is not installed, it is therefore recommended -to try - - ./configure CC="cc" - -and if that doesn't work, try - - ./configure CC="cc -nodtk" - - On Solaris, don't put `/usr/ucb' early in your `PATH'. This -directory contains several dysfunctional programs; working variants of -these programs are available in `/usr/bin'. So, if you need `/usr/ucb' -in your `PATH', put it _after_ `/usr/bin'. - - On Haiku, software installed for all users goes in `/boot/common', -not `/usr/local'. It is recommended to use the following options: - - ./configure --prefix=/boot/common - -Specifying the System Type -========================== - - There may be some features `configure' cannot figure out -automatically, but needs to determine by the type of machine the package -will run on. Usually, assuming the package is built to be run on the -_same_ architectures, `configure' can figure that out, but if it prints -a message saying it cannot guess the machine type, give it the -`--build=TYPE' option. TYPE can either be a short name for the system -type, such as `sun4', or a canonical name which has the form: - - CPU-COMPANY-SYSTEM - -where SYSTEM can have one of these forms: - - OS - KERNEL-OS - - See the file `config.sub' for the possible values of each field. If -`config.sub' isn't included in this package, then this package doesn't -need to know the machine type. - - If you are _building_ compiler tools for cross-compiling, you should -use the option `--target=TYPE' to select the type of system they will -produce code for. - - If you want to _use_ a cross compiler, that generates code for a -platform different from the build platform, you should specify the -"host" platform (i.e., that on which the generated programs will -eventually be run) with `--host=TYPE'. - -Sharing Defaults -================ - - If you want to set default values for `configure' scripts to share, -you can create a site shell script called `config.site' that gives -default values for variables like `CC', `cache_file', and `prefix'. -`configure' looks for `PREFIX/share/config.site' if it exists, then -`PREFIX/etc/config.site' if it exists. Or, you can set the -`CONFIG_SITE' environment variable to the location of the site script. -A warning: not all `configure' scripts look for a site script. - -Defining Variables -================== - - Variables not defined in a site shell script can be set in the -environment passed to `configure'. However, some packages may run -configure again during the build, and the customized values of these -variables may be lost. In order to avoid this problem, you should set -them in the `configure' command line, using `VAR=value'. For example: - - ./configure CC=/usr/local2/bin/gcc - -causes the specified `gcc' to be used as the C compiler (unless it is -overridden in the site shell script). - -Unfortunately, this technique does not work for `CONFIG_SHELL' due to -an Autoconf bug. Until the bug is fixed you can use this workaround: - - CONFIG_SHELL=/bin/bash /bin/bash ./configure CONFIG_SHELL=/bin/bash - -`configure' Invocation -====================== - - `configure' recognizes the following options to control how it -operates. - -`--help' -`-h' - Print a summary of all of the options to `configure', and exit. - -`--help=short' -`--help=recursive' - Print a summary of the options unique to this package's - `configure', and exit. The `short' variant lists options used - only in the top level, while the `recursive' variant lists options - also present in any nested packages. - -`--version' -`-V' - Print the version of Autoconf used to generate the `configure' - script, and exit. - -`--cache-file=FILE' - Enable the cache: use and save the results of the tests in FILE, - traditionally `config.cache'. FILE defaults to `/dev/null' to - disable caching. - -`--config-cache' -`-C' - Alias for `--cache-file=config.cache'. - -`--quiet' -`--silent' -`-q' - Do not print messages saying which checks are being made. To - suppress all normal output, redirect it to `/dev/null' (any error - messages will still be shown). - -`--srcdir=DIR' - Look for the package's source code in directory DIR. Usually - `configure' can determine that directory automatically. - -`--prefix=DIR' - Use DIR as the installation prefix. *note Installation Names:: - for more details, including other options available for fine-tuning - the installation locations. - -`--no-create' -`-n' - Run the configure checks, but stop before creating any output - files. - -`configure' also accepts some other, not widely useful, options. Run -`configure --help' for more details. - diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..c2b4620 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,20 @@ +Copyright (C) 2002-2018 stdlib authors + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGE- +MENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fefe092 --- /dev/null +++ b/Makefile @@ -0,0 +1,50 @@ +# Lua Standard Libraries for Lua 5.1, 5.2, 5.3 & 5.4 +# Copyright (C) 2002-2018 stdlib authors + +LDOC = ldoc +LUA = lua +MKDIR = mkdir -p +SED = sed +SPECL = specl + +VERSION = 41.2.2 + +luadir = lib/std +SOURCES = \ + $(luadir).lua \ + $(luadir)/base.lua \ + $(luadir)/container.lua \ + $(luadir)/debug.lua \ + $(luadir)/debug_init/init.lua \ + $(luadir)/functional.lua \ + $(luadir)/io.lua \ + $(luadir)/list.lua \ + $(luadir)/math.lua \ + $(luadir)/object.lua \ + $(luadir)/operator.lua \ + $(luadir)/optparse.lua \ + $(luadir)/package.lua \ + $(luadir)/set.lua \ + $(luadir)/strbuf.lua \ + $(luadir)/strict.lua \ + $(luadir)/string.lua \ + $(luadir)/table.lua \ + $(luadir)/tree.lua \ + $(NOTHING_ELSE) + +all: + +doc: build-aux/config.ld $(SOURCES) + $(LDOC) -c build-aux/config.ld . + +build-aux/config.ld: build-aux/config.ld.in + $(SED) -e "s,@PACKAGE_VERSION@,$(VERSION)," '$<' > '$@' + + +CHECK_ENV = LUA=$(LUA) + +check: $(SOURCES) + LUA=$(LUA) $(SPECL) --unicode $(SPECL_OPTS) spec/*_spec.yaml + + +.FORCE: diff --git a/Makefile.am b/Makefile.am deleted file mode 100644 index 578ea34..0000000 --- a/Makefile.am +++ /dev/null @@ -1,66 +0,0 @@ -## Process this file with automake to produce Makefile.in - -ACLOCAL_AMFLAGS = -I m4 - -LUA_PATH ?= ; -LUA_ENV = LUA_PATH="$(abs_srcdir)/src/?.lua;$(LUA_PATH)" - -SOURCES = $(wildcard $(srcdir)/src/*.lua) $(srcdir)/src/std.lua -dist_data_DATA = $(SOURCES) - -dist_doc_DATA = \ - $(top_srcdir)/src/index.html \ - $(top_srcdir)/src/luadoc.css -filesdir = $(docdir)/files -dist_files_DATA = $(wildcard $(top_srcdir)/src/files/*.html) -modulesdir = $(docdir)/modules -dist_modules_DATA = $(wildcard $(top_srcdir)/src/modules/*.html) - -EXTRA_DIST = \ - src/std.lua.in \ - $(PACKAGE).rockspec.in - -DISTCLEANFILES = $(PACKAGE).rockspec - -# In order to avoid regenerating std.lua at configure time, which -# causes the documentation to be rebuilt and hence requires users to -# have luadoc installed, put src/std.lua in as a Makefile dependency. -# (Strictly speaking, distributing an AC_CONFIG_FILE would be wrong.) -src/std.lua: src/std.lua.in - ./config.status --file=$@ - -$(dist_doc_DATA): $(SOURCES) - cd src && $(LUADOC) *.lua - -rockspecs: - rm -f *.rockspec - $(LUA_ENV) $(LUA) mkrockspecs.lua $(PACKAGE) $(VERSION) - $(LUA_ENV) $(LUA) mkrockspecs.lua $(PACKAGE) git - -bootstrap: - autoreconf -i && \ - ./configure - -tag-release: - git diff --exit-code && \ - git tag -a -m "Release tag" v$(VERSION) && \ - git push && git push --tags - -check-in-release: distcheck - git checkout release && \ - tar zxf $(PACKAGE)-$(VERSION).tar.gz && \ - cp -af $(PACKAGE)-$(VERSION)/* . && \ - git add . && git ci -m "Release v$(VERSION)" && \ - git tag -a -m "Full source release tag" release-v$(VERSION) && \ - git push && git push --tags && \ - git checkout master && \ - rm -rf $(PACKAGE)-$(VERSION)/ - -# After check-in-release we need to bootstrap to get the build files back -release: rockspecs - $(MAKE) tag-release && \ - $(MAKE) check-in-release && \ - $(MAKE) bootstrap && \ - $(MAKE) rockspecs && \ - LUAROCKS_CONFIG=$(abs_srcdir)/luarocks-config.lua luarocks --tree=$(abs_srcdir)/luarocks build $(PACKAGE)-$(VERSION)-1.rockspec && \ - woger lua package=$(PACKAGE) package_name=$(PACKAGE_NAME) version=$(VERSION) description="`LUA_INIT= LUA_PATH='$(abs_srcdir)/?.rockspec.in' $(LUA) -l$(PACKAGE) -e 'print (description.summary)'`" notes=release-notes-$(VERSION) home="`LUA_INIT= LUA_PATH='$(abs_srcdir)/?.rockspec.in' $(LUA) -l$(PACKAGE) -e 'print (description.homepage)'`" diff --git a/NEWS.md b/NEWS.md new file mode 100644 index 0000000..2649118 --- /dev/null +++ b/NEWS.md @@ -0,0 +1,1325 @@ +# Stdlib NEWS - User visible changes + +## Noteworthy changes in release 41.2.2 (2018-09-16) [stable] + +### New Features + + - Initial support for Lua 5.4 + + +## Noteworthy changes in release 41.2.1 (2017-08-07) [stable] + +### Bug fixes + + - `std.version` reports the correct release again. + + +## Noteworthy changes in release 41.2.0 (2015-03-08) [stable] + +### New features + + - New iterators, `std.npairs` and `std.rnpairs` behave like + `std.ipairs` and `std.ripairs` resp., except that they will visit + all integer keyed elements, including nil-valued "holes". This is + useful for iterating over argument lists with nils: + + ```lua + function fn (a, b, c) for _, v in npairs {...} do print (v) end + fn (nil, nil, 3) --> nil nil 3 + ``` + + - New `debug.getfenv` and `debug.setfenv` that work with Lua 5.2 and + 5.3. + + - `debug.argscheck` will skip typecheck for `self` parameter if the + function name specification contains a colon. + + - New `debug.resulterror` is much like `debug.argerror`, but uses the + message format "bad result #n from 'fname'". + + - New `debug.extramsg_mismatch` to generate `extramsg` argument for + `debug.argerror` or `debug.resulterror` on encountering a type + mismatch. + + - New `debug.extramsg_toomany` to generate a too many arguments or + similar `extramsg` argument. + +### Deprecations + + - `debug.toomanyargmsg` has been deprecated in favour of the more + orthogal `debug.extramsg_toomany` api. You can rewrite clients of + deprecated api like this: + + ```lua + if maxn (argt) > 7 then + argerror ("fname", 8, extramsg_toomany ("argument", 7, maxn (argt)), 2) + end + ``` + +### Bug fixes + + - `std.getmetamethod` no longer rejects non-table subjects when + `_DEBUG.argcheck` is set. + + - `functional.bind`, `functional.collect`, `functional.compose`, + `functional.filter` and `functional.map` propagate nil valued + arguments correctly. + + - `functional.callable` no longer raises an argument error when passed + a nil valued argument. + + - `debug.argcheck` and `debug.argscheck` accept "bool" as an alias for + "boolean" consistently. + + - `io.catdir` and `io.dirname` no longer leak extra results from + implementation details. + +### Incompatible changes + + - `functional.collect` uses `std.npairs` as a default iterator rather + than `std.ipairs`. + + +## Noteworthy changes in release 41.1.1 (2015-01-31) [stable] + +### Bug fixes + + - `std.barrel` no longer gets stuck in an infinite loop when called in + Lua 5.3. + + +## Noteworthy changes in release 41.1.0 (2015-01-30) [stable] + +### New features + + - Anything that responds to `tostring` can be appended to a `std.strbuf`: + + ```lua + local a, b = StrBuf { "foo", "bar" }, StrBuf { "baz", "quux" } + a = a .. b --> "foobarbazquux" + ``` + + - `std.strbuf` stringifies lazily, so adding tables to a StrBuf + object, and then changing the content of them before calling + `tostring` also changes the contents of the buffer. See LDocs for + an example. + + - `debug.argscheck` accepts square brackets around final optional + parameters, which is distinct to the old way of appending `?` or + `|nil` in that no spurious "or nil" is reported for type mismatches + against a final bracketed argument. + + - `debug.argscheck` can also check types of function return values, when + specified as: + + ```lua + fn = argscheck ("fname (?any...) => int, table or nil, string", fname) + ``` + + Optional results can be marked with brackets, and an ellipsis following + the final type denotes any additional results must match that final + type specification. Alternative result type groups are separated by "or". + + - New `table.unpack (t, [i, [j]])` function that defaults j to + `table.maxn (t)`, even on luajit which stops before the first nil + valued numeric index otherwise. + +### Deprecations + + - `std.strbuf.tostring` has been deprecated in favour of `tostring`. + Why write `std.strbuf.tostring (sb)` or `sb:tostring ()` when it is + more idiomatic to write `tostring (sb)`? + +### Bug fixes + + - `std.barrel` and the various `monkey_patch` functions now return + their parent module table as documented. + + - stdlib modules are all `std.strict` compliant; require "std.strict" + before requiring other modules no longer raises an error. + + - `debug.argscheck` can now diagnose when there are too many arguments, + even in the case where the earlier arguments match parameters by + skipping bracketed optionals, and the total number of arguments is + still less than the absolute maximum allowed if optionals are counted + too. + + - `package.normalize` now leaves valid ./../../ path prefixes unmolested. + +### Incompatible changes + + - `debug.argscheck` requires nil parameter type `?` notation to be + prepended to match Specl and TypedLua syntax. `?` suffixes are a + syntax error. + + - `debug.argscheck` uses `...` instead of `*` appended to the final element + if all unmatched argument types should match. The trailing `*` syntax + was confusing, because it was easy to misread it as "followed by zero-or- + more of this type". + + +## Noteworthy changes in release 41.0.0 (2015-01-03) [beta] + +### New features + + - Preliminary Lua 5.3.0 compatibility. + + - `object.prototype` now reports "file" for open file handles, and + "closed file" for closed file handles. + + - New `debug.argerror` and `debug.argcheck` functions that provide Lua + equivalents of `luaL_argerror` and `luaL_argcheck`. + + - New `debug.argscheck` function for checking all function parameter + types with a single function call in the common case. + + - New `debug.export` function, which returns a wrapper function for + checking all arguments of an inner function against a type list. + + - New `_DEBUG.argcheck` field that disables `debug.argcheck`, and + changes `debug.argscheck` to return its function argument unwrapped, + for production code. Similarly `_DEBUG = false` deactivates these + functions in the same way. + + - New `std.operator` module, with easier to type operator names (`conj`, + `deref`, `diff`, `disj`, `eq`, `neg`, `neq`, `prod`, `quot`, and `sum`), + and a functional operator for concatenation `concat`; plus new mathematical + operators `mod`, and `pow`; and relational operators `lt`, `lte`, `gt` and + `gte`. + + - `functional.case` now accepts non-callable branch values, which are + simply returned as is, and functable values which are called and + their return value propagated back to the case caller. Function + values behave the same as in previous releases. + + - `functional.collect`, `functional.filter`, `functional.map` and + `functional.reduce` now work with standard multi-return iterators, + such as `std.pairs`. + + - `functional.collect` defaults to using `std.ipairs` as an iterator. + + - New `functional.cond`, for evaluating multiple distinct expressions + to determine what following value to be the returned. + + - `functional.filter` and `functional.map` default to using `std.pairs` + as an iterator. + + - The init argument to `functional.foldl` and `functional.foldr` is now + optional; when omitted these functions automatically start with + the left- or right-most element of the table argument resp. + + - New `functional.callable` function for unwrapping objects or + primitives that can be called as if they were a function. + + - New `functional.lambda` function for compiling lambda strings: + + ```lua + table.sort (t, lambda "|a,b| a + explaining why any deprecation should be reinstated or at least kept + around for more than 1 year. + + - By default, deprecated APIs will issue a warning to stderr on every + call. However, in production code, you can turn off these warnings + entirely with any of: + + ```lua + _DEBUG = false + _DEBUG = { deprecate = false } + require "std.debug_init".deprecate = false + ``` + + Or, to confirm you're not trying to call a deprecated function at + runtime, you can prevent deprecated functions from being defined at + all with any of: + + ```lua + _DEBUG = true + _DEBUG = { deprecate = true } + require "std.debug_init".deprecate = true + ``` + + The `_DEBUG` global must be set before requiring any stdlib modules, + but you can adjust the fields in the `std.debug_init` table at any + time. + + - `functional.eval` has been moved to `std.eval`, the old name now + gives a deprecation warning. + + - `functional.fold` has been renamed to `functional.reduce`, the old + name now gives a deprecation warning. + + - `functional.op` has been moved to a new `std.operator` module, the + old function names now gives deprecation warnings. + + - `list.depair` and `list.enpair` have been moved to `table.depair` and + `table.enpair`, the old names now give deprecation warnings. + + - `list.filter` has been moved to `functional.filter`, the old name now + gives a deprecation warning. + + - `list.flatten` has been moved to `table.flatten`, the old name now + gives a deprecation warning. + + - `list.foldl` and `list.foldr` have been replaced by the richer + `functional.foldl` and `functional.foldr` respectively. The old + names now give a deprecation warning. Note that List object methods + `foldl` and `foldr` are not affected. + + - `list.index_key` and `list.index_value` have been deprecated. These + functions are not general enough to belong in lua-stdlib, because + (among others) they only work correctly with tables that can be + inverted without loss of key values. They currently give deprecation + warnings. + + - `list.map` and `list.map_with` has been deprecated, in favour of the + more powerful new `functional.map` and `functional.map_with` which + handle tables as well as lists. + + - `list.project` has been deprecated in favour of `table.project`, the + old name now gives a deprecation warning. + + - `list.relems` has been deprecated, in favour of the more idiomatic + `functional.compose (std.ireverse, std.ielems)`. + + - `list.reverse` has been deprecated in favour of the more general + and more accurately named `std.ireverse`. + + - `list.shape` has been deprecated in favour of `table.shape`, the old + name now gives a deprecation warning. + + - `list.transpose` has been deprecated in favour of `functional.zip`, + see above for details. + + - `list.zip_with` has been deprecated in favour of `functional.zip_with`, + see above for details. + + - `string.assert` has been moved to `std.assert`, the old name now + gives a deprecation warning. + + - `string.require_version` has been moved to `std.require`, the old + name now gives a deprecation warning. + + - `string.tostring` has been moved to `std.tostring`, the old name now + gives a deprecation warning. + + - `table.metamethod` has been moved to `std.getmetamethod`, the old + name now gives a deprecation warning. + + - `table.ripairs` has been moved to `std.ripairs`, the old name now + gives a deprecation warning. + + - `table.totable` has been deprecated and now gives a warning when used. + +### Incompatible changes + + - `std.monkey_patch` works the same way as the other submodule + monkey_patch functions now, by injecting its methods into the given + (or global) namespace. To get the previous effect of running all the + monkey_patch functions, either run them all manually, or call + `std.barrel ()` as before. + + - `functional.bind` sets fixed positional arguments when called as + before, but when the newly bound function is called, those arguments + fill remaining unfixed positions rather than being overwritten by + original fixed arguments. For example, where this would have caused + an error previously, it now prints "100" as expected. + + ```lua + local function add (a, b) return a + b end + local incr = functional.bind (add, {1}) + print (incr (99)) + ``` + + If you have any code that calls functions returned from `bind`, you + need to remove the previously ignored arguments that correspond to + the fixed argument positions in the `bind` invocation. + + - `functional.collect`, `functional.filter` and `functional.map` still + make a list from the results from an iterator that returns single + values, but when an iterator returns multiple values they now make a + table with key:value pairs taken from the first two returned values of + each iteration. + + - The `functional.op` table has been factored out into its own new + module `std.operator`. It will also continue to be available from the + legacy `functional.op` access point for the forseeable future. + + - The `functional.op[".."]` operator is no longer a list concatenation + only loaded when `std.list` is required, but a regular string + concatenation just like Lua's `..` operator. + + - `io.catdir` now raises an error when called with no arguments, for + consistency with `io.catfile`. + + - `io.die` no longer calls `io.warn` to write the error message to + stderr, but passes that error message to the core `error` function. + + - `std.set` objects used to be lax about enforcing type correctness in + function arguments, but now that we have strict type-checking on all + apis, table arguments are not coerced to Set objects but raise an + error. Due to an accident of implementation, you can get the old + inconsistent behaviour back for now by turning off type checking + before loading any stdlib modules: + + ```lua + _DEBUG = { argcheck = false } + local set = require "std.set" + ``` + + - `string.pad` will still (by implementation accident) coerce non- + string initial arguments to a string using `string.tostring` as long + as argument checking is disabled. Under normal circumstances, + passing a non-string will now raise an error as specified in the api + documentation. + + - `table.totable` is deprecated, and thus objects no longer provide or + use a `__totable` metamethod. Instead, using a `__pairs` metamethod + to return key/value pairs, and that will automatically be used by + `__tostring`, `object.mapfields` etc. The base object now provides a + `__pairs` metamethod that returns key/value pairs in order, and + ignores private fields. If you have objects that relied on the + previous treatment of `__totable`, please convert them to set a + custom `__pairs` instead. + + +### Bug fixes + + - Removed LDocs for unused `_DEBUG.std` field. + + - `debug.trace` works with Lua 5.2.x again. + + - `list:foldr` works again instead of raising a "bad argument #1 to + 'List'" error. + + - `list.transpose` works again, and handles empty lists without + raising an error; but is deprecated and will be removed in a future + release (see above). + + - `list.zip_with` no longer raises an argument error on every call; but, + like `list.transpose`, is also deprecated (see above). + + - `optparse.on` now works with `std.strict` enabled. + + - `std.require` (nee `string.require_version`) now extracts the last + substring made entirely of digits and periods from the required + module's version string before splitting on period. That means, for + version strings like luaposix's "posix library for Lua 5.2 / 32" we + now correctly compare just the numeric part against specified version + range rather than an ASCII comparison of the whole thing as before! + + - The documentation now correcly notes that `std.require` looks + first in `module.version` and then `module._VERSION` to match the + long-standing implementation. + + - `string.split` now really does split on whitespace when no split + pattern argument is provided. Also, the documentation now + correctly cites `%s+` as the default whitespace splitting pattern + (not `%s*` which splits between every non-whitespace character). + + +## Noteworthy changes in release 40 (2014-05-01) [stable] + +### New features + + - `functional.memoize` now accepts a user normalization function, + falling back on `string.tostring` otherwise. + + - `table.merge` now supports `map` and `nometa` arguments orthogonally + to `table.clone`. + + - New `table.merge_select` function, orthogonal to + `table.clone_select`. See LDocs for details. + +### Incompatible changes + + - Core methods and metamethods are no longer monkey patched by default + when you `require "std"` (or `std.io`, `std.math`, `std.string` or + `std.table`). Instead they provide a new `monkey_patch` method you + should use when you don't care about interactions with other + modules: + + ```lua + local io = require "std.io".monkey_patch () + ``` + + To install all of stdlib's monkey patches, the `std` module itself + has a `monkey_patch` method that loads all submodules with their own + `monkey_patch` method and runs them all. + + If you want full compatibility with the previous release, in addition + to the global namespace scribbling snippet above, then you need to + adjust the first line to: + + ```lua + local std = require "std".monkey_patch () + ``` + + - The global namespace is no longer clobbered by `require "std"`. To + get the old behaviour back: + + ```lua + local std = require "std".barrel (_G) + ``` + + This will execute all available monkey_patch functions, and then + scribble all over the `_G` namespace, just like the old days. + + - The `metamethod` call is no longer in `std.functional`, but has moved + to `std.table` where it properly belongs. It is a utility method for + tables and has nothing to do with functional programming. + + - The following deprecated camelCase names have been removed, you + should update your code to use the snake_case equivalents: + `std.io.processFiles`, `std.list.indexKey`, `std.list.indexValue`, + `std.list.mapWith`, `std.list.zipWith`, `std.string.escapePattern`, + `std.string. escapeShell`, `std.string.ordinalSuffix`. + + - The following deprecated function names have been removed: + `std.list.new` (call `std.list` directly instead), + `std.list.slice` (use `std.list.sub` instead), + `std.set.new` (call `std.set` directly instead), + `std.strbuf.new` (call `std.strbuf` directly instead), and + `std.tree.new` (call `std.tree` directly instead). + +### Bug fixes + + - Allow `std.object` derived tables as `std.tree` keys again. + + +## Noteworthy changes in release 39 (2014-04-23) [stable] + +### New features + + - New `std.functional.case` function for rudimentary case statements. + The main difference from serial if/elseif/end comparisons is that + `with` is evaluated only once, and then the match function is looked + up with an O(1) table reference and function call, as opposed to + hoisting an expression result into a temporary variable, and O(n) + comparisons. + + The function call overhead is much more significant than several + comparisons, and so `case` is slower for all but the largest series + of if/elseif/end comparisons. It can make your code more readable, + however. + + See LDocs for usage. + + - New pathstring management functions in `std.package`. + + Manage `package.path` with normalization, duplicate removal, + insertion & removal of elements and automatic folding of '/' and '?' + onto `package.dirsep` and `package.path_mark`, for easy addition of + new paths. For example, instead of all this: + + ```lua + lib = std.io.catfile (".", "lib", package.path_mark .. ".lua") + paths = std.string.split (package.path, package.pathsep) + for i, path in ipairs (paths) do + -- ... lots of normalization code... + end + i = 1 + while i <= #paths do + if paths[i] == lib then + table.remove (paths, i) + else + i = i + 1 + end + end + table.insert (paths, 1, lib) + package.path = table.concat (paths, package.pathsep) + ``` + + You can now write just: + + ```lua + package.path = package.normalize ("./lib/?.lua", package.path) + ``` + + - `std.optparse:parse` accepts a second optional parameter, a table of + default option values. + + - `table.clone` accepts an optional table of key field renames in the + form of `{oldkey = newkey, ...}` subsuming the functionality of + `table.clone_rename`. The final `nometa` parameter is supported + whether or not a rename map is given: + + ```lua + r = table.clone (t, "nometa") + r = table.clone (t, {oldkey = newkey}, "nometa") + ``` + +### Deprecations + + - `table.clone_rename` now gives a warning on first call, and will be + removed entirely in a few releases. The functionality has been + subsumed by the improvements to `table.clone` described above. + +### Bug fixes + + - `std.optparse` no longer throws an error when it encounters an + unhandled option in a combined (i.e. `-xyz`) short option string. + + - Surplus unmapped fields are now discarded during object cloning, for + example when a prototype has `_init` set to `{ "first", "second" }`, + and is cloned using `Proto {'one', 'two', 'three'}`, then the + unmapped `three` argument is now discarded. + + - The path element returned by `std.tree.nodes` can now always be + used as a key list to dereference the root of the tree, particularly + `tree[{}]` now returns the root node of `tree`, to match the initial + `branch` and final `join` results from a full traversal by + `std.tree.nodes (tree)`. + +### Incompatible changes + + - `std.string` no longer sets `__append`, `__concat` and `__index` in + the core strings metatable by default, though `require "std"` does + continue to do so. See LDocs for `std.string` for details. + + - `std.optparse` no longer normalizes unhandled options. For example, + `--unhandled-option=argument` is returned unmolested from `parse`, + rather than as two elements split on the `=`; and if a combined + short option string contains an unhandled option, then whatever was + typed at the command line is returned unmolested, rather than first + stripping off and processing handled options, and returning only the + unhandled substring. + + - Setting `_init` to `{}` in a prototype object will now discard all + positional parameters passed during cloning, because a table valued + `_init` is a list of field names, beyond which surplus arguments (in + this case, all arguments!) are discarded. + + +## Noteworthy changes in release 38 (2014-01-30) [stable] + +### New features + + - The separator parameter to `std.string.split` is now optional. It + now splits strings with `%s+` when no separator is specified. The + new implementation is faster too. + + - New `std.object.mapfields` method factors out the table field copying + and mapping performed when cloning a table `_init` style object. This + means you can call it from a function `_init` style object after + collecting a table to serve as `src` to support derived objects with + normal std.object syntax: + + ```lua + Proto = Object { + _type = "proto" + _init = function (self, arg, ...) + if type (arg) == "table" then + mapfields (self, arg) + else + -- non-table instantiation code + end + end, + } + new = Proto (str, #str) + Derived = proto { _type = "Derived", ... } + ``` + + - Much faster object cloning; `mapfields` is in imperative style and + makes one pass over each table it looks at, where previous releases + used functional style (stack frame overhead) and multiple passes over + input tables. + + On my 2013 Macbook Air with 1.3GHz Core i5 CPU, I can now create a + million std.objects with several assorted fields in 3.2s. Prior to + this release, the same process took 8.15s... and even release 34.1, + with drastically simpler Objects (19SLOC vs over 120) took 5.45s. + + - `std.object.prototype` is now almost an order of magnitude faster + than previous releases, taking about 20% of the time it previously + used to return its results. + + - `io.warn` and `io.die` now integrate properly with `std.optparse`, + provided you save the `opts` return from `parser:parse` back to the + global namespace where they can access it: + + ```lua + local OptionParser = require "std.optparse" + local parser = OptionParser "eg 0\nUsage: eg\n" + _G.arg, _G.opts = parser:parse (_G.arg) + if not _G.opts.keep_going then + require "std.io".warn "oh noes!" + end + ``` + + will, when run, output to stderr: "eg: oh noes!" + +### Bug fixes + + - Much improved documentation for `optparse`, so you should be able + to use it without reading the source code now! + + - `io.warn` and `io.die` no longer output a line-number when there is + no file name to append it to. + + - `io.warn` and `io.die` no longer crash in the absence of a global + `prog` table. + + - `string.split` no longer goes into an infinite loop when given an + empty separator string. + + - Fix `getmetatable (container._functions) == getmetatable (container)`, + which made tostring on containers misbehave, among other latent bugs. + + - `_functions` is never copied into a metatable now, finally solving + the conflicted concerns of needing metatables to be shared between + all objects of the same `_type` (for `__lt` to work correctly for one + thing) and not leaving a dangling `_functions` list in the metatable + of cloned objects, which could delete functions with matching names + from subsequent clones. + + +## Noteworthy changes in release 37 (2014-01-19) [stable] + +### New features + + - Lazy loading of submodules into `std` on first reference. On initial + load, `std` has the usual single `version` entry, but the `__index` + metatable will automatically require submodules on first reference: + + ```lua + local std = require "std" + local prototype = std.container.prototype + ``` + + - New `std.optparse` module: A civilised option parser. + (L)Documentation distributed in doc/classes/std.optparse.html. + +### Bug fixes + + - Modules no longer leak `new' and `proper_subset' into the global + table. + + - Cloned `Object` and `Container` derived types are more aggressive + about sharing metatables, where previously the metatable was copied + unnecessarily the base object used `_functions` for module functions + + - The retracted release 36 changed the operand order of many `std.list` + module functions unnecessarily. Now that `_function` support is + available, there's no need to be so draconian, so the original v35 + and earlier operand order works as before again. + + - `std.list.new`, `std.set.new`, `set.strbuf.new` and `std.tree.new` + are available again for backwards compatibility. + + - LuaRocks install doesn't copy config.ld and config.ld to $docdir. + +### Incompatible changes + + - `std.getopt` is no more. It appears to have no users, though if there + is a great outcry, it should be easy to make a compatibility api over + `std.optparse` in the next release. + + +## Noteworthy changes in release 36 (2014-01-16) [stable] + +### New features + + - Modules have been refactored so that they can be safely + required individually, and without loading themselves or any + dependencies on other std modules into the global namespace. + + - Objects derived from the `std.object` prototype have a new + :prototype () method that returns the contents of the + new internal `_type` field. This can be overridden during cloning + with, e.g.: + + ```lua + local Object = require "std.object" + Prototype = Object { _type = "Prototype", } + ``` + + - Objects derived from the `std.object` prototype return a new table + with a shallow copy of all non-private fields (keys that do not + begin with "_") when passed to `table.totable` - unless overridden + in the derived object's __totable field. + + - list and strbuf are now derived from `std.object`, which means that + they respond to `object.prototype` with appropriate type names ("List", + "StrBuf", etc.) and can be used as prototypes for further derived + objects or clones; support object:prototype (); respond to totable etc. + + - A new Container module at `std.container` makes separation between + container objects (which are free to use __index as a "[]" access + metamethod, but) which have no object methods, and regular objects + (which do have object methods, but) which cannot use the __index + metamethod for "[]" access to object contents. + + - set and tree are now derived from `std.container`, so there are no + object methods. Instead there are a full complement of equivalent + module functions. Metamethods continue to work as before. + + - `string.prettytostring` always displays table elements in the same + order, as provided by `table.sort`. + + - `table.totable` now accepts a string, and returns a list of the + characters that comprise the string. + + - Can now be installed directly from a release tarball by `luarocks`. + No need to run `./configure` or `make`, unless you want to install to + a custom location, or do not use LuaRocks. + +### Bug fixes + + - string.escape_pattern is now Lua 5.2 compatible. + + - all objects now reuse prototype metatables, as required for __le and + __lt metamethods to work as documented. + +### Deprecations + + - To avoid confusion between the builtin Lua `type` function and the + method for finding the object prototype names, `std.object.type` is + deprecated in favour of `std.object.prototype`. `std.object.type` + continues to work for now, but might be removed from a future + release. + + ```lua + local prototype = (require 'std.object').prototype + ``` + + ...makes for more readable code, rather than confusion between the + different flavours of `type`. + +### Incompatible changes + + - Following on from the Grand Renaming™ change in the last release, + `std.debug_ext`, `std.io_ext`, `std.math_ext`, `std.package_ext`, + `std.string_ext` and `std.table_ext` no longer have the spurious + `_ext` suffix. Instead, you must now use, e.g.: + + ```lua + local string = require "std.string" + ``` + + These names are now stable, and will be available from here for + future releases. + + - The `std.list` module, as a consequence of returning a List object + prototype rather than a table of functions including a constructor, + now always has the list operand as the first argument, whether that + function is called with `.` syntax or `:` syntax. Functions which + previously had the list operand in a different position when called + with `.` syntax were: list.filter, list.foldl, list.foldr, + list.index_key, list.index_value, list.map, list.map_with, + list.project, list.shape and list.zip_with. Calls made as object + methods using `:` calling syntax are unchanged. + + - The `std.set` module is a `std.container` with no object methods, + and now uses prototype functions instead: + + ```lua + local union = Set.union (set1, set2) + ``` + + +## Noteworthy changes in release 35 (2013-05-06) [stable] + +### New features + + - Move to the Slingshot release system. + - Continuous integration from Travis automatically builds stdilb + with Lua 5.1, Lua 5.2 and luajit-2.0 with every commit, which + should help prevent future release breaking compatibility with + one or another of those interpreters. + +### Bug fixes + + - `std.package_ext` no longer overwrites the core `package` table, + leaving the core holding on to memory that Lua code could no + longer access. + +### Incompatible changes + + - The Grand Renaming™ - everything now installs to $luaprefix/std/, + except `std.lua` itself. Importing individual modules now involves: + + ```lua + local list = require "std.list" + ``` + + If you want to have all the symbols previously available from the + global and core module namespaces, you will need to put them there + yourself, or import everything with: + + ```lua + require "std" + ``` + + which still behaves per previous releases. + + Not all of the modules work correctly when imported individually + right now, until we figure out how to break some circular dependencies. + + +## Noteworthy changes in release 34.1 (2013-04-01) [stable] + + - This is a maintenance release to quickly fix a breakage in getopt + from release v34. Getopt no longer parses non-options, but stops + on the first non-option... if a use case for the other method + comes up, we can always add it back in. + + +## Noteworthy changes in release 34 (2013-03-25) [stable] + + - stdlib is moving towards supporting separate requirement of individual + modules, without scribbling on the global environment; the work is not + yet complete, but we're collecting tests along the way to ensure that + once it is all working, it will carry on working; + + - there are some requirement loops between modules, so not everything can + be required independently just now; + + - `require "std"` will continue to inject std symbols into the system + tables for backwards compatibility; + + - stdlib no longer ships a copy of Specl, which you will need to install + separately if you want to run the bundled tests; + + - getopt supports parsing of undefined options; useful for programs that + wrap other programs; + + - getopt.Option constructor is no longer used, pass a plain Lua table of + options, and getopt will do the rest; + + +## Noteworthy changes in release 33 (2013-07-27) [stable] + + - This release improves stability where Specl has helped locate some + corner cases that are now fixed. + + - `string_ext.wrap` and `string_ext.tfind` now diagnose invalid arguments. + + - Specl code coverage is improving. + + - OrdinalSuffix improvements. + + - Use '%' instead of math.mod, as the latter does not exist in Lua 5.2. + + - Accept negative arguments. + + +## Noteworthy changes in release 32 (2013-02-22) [stable] + + - This release fixes a critical bug preventing getopt from returning + anything in getopt.opt. Gary V. Vaughan is now a co-maintainer, currently + reworking the sources to use (Lua 5.1 compatible) Lua 5.2 style module + packaging, which requires you to assign the return values from your imports: + + ```lua + getopt = require "getopt" + ``` + + - Extension modules, table_ext, package_ext etc. return the unextended module + table before injecting additional package methods, so you can ignore those + return values or save them for programatically backing out the changes: + + ```lua + table_unextended = require "table_ext" + ``` + + - Additionally, Specl (see http://github.com/gvvaughan/specl/) specifications + are being written for stdlib modules to help us stop accidentally breaking + things between releases. + + +## Noteworthy changes in release 31 (2013-02-20) [stable] + + - This release improves the list module: lists now have methods, list.slice + is renamed to list.sub (the old name is provided as an alias for backwards + compatibility), and all functions that construct a new list return a proper + list, not a table. As a result, it is now often possible to write code that + works on both lists and strings. + + +## Noteworthy changes in release 30 (2013-02-17) [stable] + + - This release changes some modules to be written in a Lua 5.2 style (but + not the way they work with 5.1). Some fixes and improvements were made to + the build system. Bugs in the die function, the parser module, and a nasty + bug in the set module introduced in the last release (29) were fixed. + + +## Noteworthy changes in release 29 (2013-02-06) [stable] + + - This release overhauls the build system to have LuaRocks install releases + directly from git rather than from tarballs, and fixes a bug in set (issue + #8). + + +## Noteworthy changes in release 28 (2012-10-28) [stable] + + - This release improves the documentation and build system, and improves + require_version to work by default with more libraries. + + +## Noteworthy changes in release 27 (2012-10-03) [stable] + + - This release changes getopt to return all arguments in a list, rather than + optionally processing them with a function, fixes an incorrect definition + of set.elems introduced in release 26, turns on debugging by default, + removes the not-very-useful string.gsubs, adds constructor functions for + objects, renames table.rearrange to the more descriptive table.clone_rename + and table.indices to table.keys, and makes table.merge not clone but modify + its left-hand argument. A function require_version has been added to allow + version constraints on a module being required. Gary Vaughan has + contributed a memoize function, and minor documentation and build system + improvements have been made. Usage information is now output to stdout, not + stderr. The build system has been fixed to accept Lua 5.2. The luarock now + installs documentation, and the build command used is now more robust + against previous builds in the same tree. + + +## Noteworthy changes in release 26 (2012-02-18) [stable] + + - This release improves getoptâs output messages and conformance to + standard practice for default options. io.processFiles now unsets prog.file + when it finishes, so that a program can tell when itâs no longer + processing a file. Three new tree iterators, inodes, leaves and ileaves, + have been added; the set iterator set.elements (renamed to set.elems for + consistency with list.elems) is now leaves rather than pairs. tree indexing + has been made to work in more circumstances (thanks, Gary Vaughan). + io.writeline is renamed io.writelines for consistency with io.readlines and + its function. A slurping function, io.slurp, has been added. Strings now + have a __concat metamethod. + + +## Noteworthy changes in release 25 (2011-09-19) [stable] + + - This release adds a version string to the std module and fixes a buglet in + the build system. + + +## Noteworthy changes in release 24 (2011-09-19) [stable] + + - This release fixes a rename missing from release 23, and makes a couple of + fixes to the new build system, also from release 23. + + +## Noteworthy changes in release 23 (2011-09-17) [stable] + + - This release removes the posix_ext module, which is now part of luaposix, + renames string.findl to string.tfind to be the same as lrexlib, and + autotoolizes the build system, as well as providing a rockspec file. + + +## Noteworthy changes in release 22 (2011-09-02) [stable] + + - This release adds two new modules: strbuf, a trivial string buffers + implementation, which is used to speed up the stdlib tostring method for + tables, and bin, which contains a couple of routines for converting binary + data into numbers and strings. Some small documentation and build system + fixes have been made. + + +## Noteworthy changes in release 21 (2011-06-06) [stable] + + - This release converts the documentation of stdlib to LuaDoc, adds an + experimental Lua 5.2 module "fstable", for storing tables directly on + disk as files and directories, and fixes a few minor bugs (with help from + David Favro). + + - This release has been tested lightly on Lua 5.2 alpha, but is not + guaranteed to work fully. + + +## Noteworthy changes in release 20 (2011-04-14) [stable] + + - This release fixes a conflict between the global _DEBUG setting and the use + of strict.lua, changes the argument order of some list functions to favour + OO-style use, adds posix.euidaccess, and adds OO-style use to set. mk1file + can now produce a single-file version of a user-supplied list of modules, + not just the standard set. + + +## Noteworthy changes in release 19 (2011-02-26) [stable] + + - This release puts the package.config reflection in a new package_ext + module, where it belongs. Thanks to David Manura for this point, and for a + small improvement to the code. + + +## Noteworthy changes in release 18 (2011-02-26) [stable] + + - This release provides named access to the contents of package.config, which + is undocumented in Lua 5.1. See luaconf.h and the Lua 5.2 manual for more + details. + + +## Noteworthy changes in release 17 (2011-02-07) [stable] + + - This release fixes two bugs in string.pad (thanks to Bob Chapman for the + fixes). + + +## Noteworthy changes in release 16 (2010-12-09) [stable] + + - Adds posix module, using luaposix, and makes various other small fixes and + improvements. + + +## Noteworthy changes in release 15 (2010-06-14) [stable] + + - This release fixes list.foldl, list.foldr, the fold iterator combinator and + io.writeLine. It also simplifies the op table, which now merely sugars the + built-in operators rather than extending them. It adds a new tree module, + which subsumes the old table.deepclone and table.lookup functions. + table.subscript has become op["[]"], and table.subscripts has been removed; + the old treeIter iterator has been simplified and generalised, and renamed + to nodes. The mk1file script and std.lua library loader have had the module + list factored out into modules.lua. strict.lua from the Lua distribution is + now included in stdlib, which has been fixed to work with it. Some minor + documentation and other code improvements and fixes have been made. + + +## Noteworthy changes in release 14 (2010-06-07) [stable] + + - This release makes stdlib compatible with strict.lua, which required a + small change to the debug_ext module. Some other minor changes have also + been made to that module. The table.subscripts function has been removed + from the table_ext.lua. + + +## Noteworthy changes in release 13 (2010-06-02) [stable] + + - This release removes the lcs module from the standard set loaded by + "std", removes an unnecessary definition of print, and tidies up the + implementation of the "op" table of functional versions of the infix + operators and logical operators. + + +## Noteworthy changes in release 12 (2009-09-07) [stable] + + - This release removes io.basename and io.dirname, which are now available in + lposix, and the little-used functions addSuffix and changeSuffix which + dependend on them. io.pathConcat is renamed to io.catdir and io.pathSplit + to io.splitdir, making them behave the same as the corresponding Perl + functions. The dependency on lrexlib has been removed along with the rex + wrapper module. Some of the more esoteric and special-purpose modules + (mbox, xml, parser) are no longer loaded by 'require "std"'. + + This leaves stdlib with no external dependencies, and a rather more + coherent set of basic modules. + + +## Noteworthy changes in release 11 (2009-03-15) [stable] + + - This release fixes a bug in string.format, removes the redundant + string.join (it's the same as table.concat), and adds to table.clone and + table.deepclone the ability to copy without metatables. Thanks to David + Kantowitz for pointing out the various deficiencies. + + +## Noteworthy changes in release 10 (2009-03-13) [stable] + + - This release fixes table.deepclone to copy metatables, as it should. + Thanks to David Kantowitz for the fix. + + +## Noteworthy changes in release 9 (2009-02-19) [stable] + + - This release updates the object module to be the same as that published + in "Lua Gems", and fixes a bug in the utility mk1file which makes a + one-file version of the library, to stop it permanently redefining require. + + +## Noteworthy changes in release 8 (2008-09-04) [stable] + + - This release features fixes and improvements to the set module; thanks to + Jiutian Yanling for a bug report and suggestion which led to this work. + + +## Noteworthy changes in release 7 (2008-09-04) [stable] + + - just a bug fix + + +## Noteworthy changes in release 6 (2008-07-28) [stable] + + - This release rewrites the iterators in a more Lua-ish 5.1 style. + + +## Noteworthy changes in release 5 (2008-03-04) [stable] + + - I'm happy to announce a new release of my standard Lua libraries. It's been + nearly a year since the last release, and I'm happy to say that since then + only one bug has been found (thanks Roberto!). Two functions have been + added in this release, to deal with file paths, and one removed (io.length, + which is handled by lfs.attributes) along with one constant (INTEGER_BITS, + handled by bitlib's bit.bits). + + - For those not familiar with stdlib, it's a pure-Lua library of mostly + fundamental data structures and algorithms, in particular support for + functional and object-oriented programming, string and regex operations and + extensible pretty printing of data structures. More specific modules + include a getopt implementation, a generalised least common subsequences + (i.e. diff algorithm) implementation, a recursive-descent parser generator, + and an mbox parser. + + - It's quite a mixed bag, but almost all written for real projects. It's + written in a doc-string-ish style with the supplied very simple ldoc tool. + + - I am happy with this code base, but there are various things it could use: + + 0. Tests. Tests. Tests. The code has no unit tests. It so needs them. + + 1. More code. Nothing too specialised (unless it's too small to be released + on its own, although very little seems "too small" in the Lua + community). Anything that either has widespread applicability (like + getopt) or is very general (data structures, algorithms, design + patterns) is good. + + 2. Refactoring. The code is not ideally factored. At the moment it is + divided into modules that extend existing libraries, and new modules + constructed along similar lines, but I think that some of the divisions + are confusing. For example, the functional programming support is spread + between the list and base modules, and would probably be better in its + own module, as those who aren't interested in the functional style won't + want the functional list support or the higher-order functions support, + and those who want one will probably want the other. + + 3. Documentation work. There's not a long wrong with the existing + documentation, but it would be nice, now that there is a stable LuaDoc, + to use that instead of the built-in ldoc, which I'm happy to discard now + that LuaDoc is stable. ldoc was always designed as a minimal LuaDoc + substitute in any case. + + 4. Maintenance and advocacy. For a while I have been reducing my work on + Lua, and am also now reducing my work in Lua. If anyone would like to + take on stdlib, please talk to me. It fills a much-needed function: I + suspect a lot of Lua programmers have invented the wheels with which it + is filled over and over again. In particular, many programmers could + benefit from the simplicity of its simple and well-designed functional, + string and regex capabilities, and others will love its comprehensive + getopt. + + +## Noteworthy changes in release 4 (2007-04-26) [beta] + + - This release removes the dependency on the currently unmaintained lposix + library, includes pre-built HTML documentation, and fixes some 5.0-style + uses of variadic arguments. + + Thanks to Matt for pointing out all these problems. stdlib is very much + user-driven at the moment, since it already does everything I need, and I + don't have much time to work on it, so do please contact me if you find + bugs or problems or simply don't understand it, as the one thing I *do* + want to do is make it useful and accessible! + + +## Noteworthy changes in release 3 (2007-02-25) [beta] + + - This release fixes the "set" and "lcs" (longest common subsequence, or + "grep") libraries, which were broken, and adds one or two other bug and + design fixes. Thanks are due to Enrico Tassi for pointing out some of the + problems. + + +## Noteworthy changes in release 2 (2007-01-05) [beta] + + - This release includes some bug fixes, and compatibility with lrexlib 2.0. + + +## Noteworthy changes in release 1 (2011-09-02) [beta] + + - It's just a snapshot of CVS, but it's pretty stable at the moment; stdlib, + until such time as greater interest or participation enables (or forces!) + formal releases will be in permanent beta, and tracking CVS is recommended. diff --git a/README b/README deleted file mode 100644 index 41d1ff2..0000000 --- a/README +++ /dev/null @@ -1,51 +0,0 @@ - Standard Lua libraries - ---------------------- - - by the stdlib project (http://github.com/rrthomas/lua-stdlib/) - - -This is a collection of Lua libraries for Lua 5.1 and 5.2. The -libraries are copyright by their authors 2000-2013 (see the AUTHORS -file for details), and released under the MIT license (the same -license as Lua itself). There is no warranty. - -The standard subset of stdlib has no prerequisites beyond a standard -Lua system. The following modules have extra dependencies: - - fstable: Lua 5.2 - - -Installation ------------- - -The simplest way to install stdlib is with LuaRocks -(http://www.luarocks.org/ ): - -luarocks install stdlib - - -Use ---- - -As well as requiring individual libraries, you can load the standard -set with - - require "std" - -Modules not in the standard set may be removed from future versions of -stdlib. - - -Documentation -------------- - -The libraries are documented in LuaDoc. Pre-built HTML files are -included. - - -Bug reports and code contributions ----------------------------------- - -These libraries are maintained and extended by their users. Please -make bug report and suggestions on GitHub (see URL at top of file). -Pull requests are especially appreciated. diff --git a/README.md b/README.md new file mode 100644 index 0000000..77230ab --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +Standard Lua libraries +====================== + +by the [stdlib project][github] + +[github]: http://github.com/lua-stdlib/lua-stdlib/ "Github repository" + +[![travis-ci status](https://secure.travis-ci.org/lua-stdlib/lua-stdlib.png?branch=master)](http://travis-ci.org/lua-stdlib/lua-stdlib/builds) +[![Stories in Ready](https://badge.waffle.io/lua-stdlib/lua-stdlib.png?label=ready&title=Ready)](https://waffle.io/lua-stdlib/lua-stdlib) + + +This is a collection of Lua libraries for Lua 5.1 (including LuaJIT), 5.2, +5.3 and 5.4. The libraries are copyright by their authors 2000-2018, and +released under the MIT license (the same license as Lua itself). There is +no warranty. + +Stdlib has no prerequisites beyond a standard Lua system. + + +Installation +------------ + +The simplest way to install stdlib is with [LuaRocks][]. To install the +latest release (recommended): + + luarocks install stdlib + +To install current git master (for testing): + + luarocks install https://raw.githubusercontent.com/lua-stdlib/lua-stdlib/release/stdlib-git-1.rockspec + +To install without LuaRocks, check out the sources from the +[repository][github], and then run the following commands: the +dependencies are listed in the dependencies entry of the file +`stdlib-rockspec.lua`. You will also need autoconf and automake. + + cd lua-stdlib + autoreconf --force --version --install + ./configure --prefix=INSTALLATION-ROOT-DIRECTORY + make all check install + +See [INSTALL][] for instructions for `configure`. + +[luarocks]: http://www.luarocks.org "LuaRocks Project" +[install]: https://raw.githubusercontent.com/lua-stdlib/lua-stdlib/master/INSTALL + +Use +--- + +As well as requiring individual libraries, you can load the standard +set with + + require "std" + +Modules not in the standard set may be removed from future versions of +stdlib. + + +Documentation +------------- + +The libraries are [documented in LDoc][github.io]. Pre-built HTML +files are included in the release. + +[github.io]: http://lua-stdlib.github.io/lua-stdlib + + +Bug reports and code contributions +---------------------------------- + +These libraries are written and maintained by their users. Please make +bug report and suggestions on GitHub (see URL at top of file). Pull +requests are especially appreciated. diff --git a/build-aux/config.ld b/build-aux/config.ld new file mode 100644 index 0000000..856fa87 --- /dev/null +++ b/build-aux/config.ld @@ -0,0 +1,48 @@ +-- -*- lua -*- +title = "stdlib 41.2.2 Reference" +project = "stdlib 41.2.2" +description = [[ +# Standard Lua Libraries + +This is a collection of Lua libraries for Lua 5.1 (including LuaJIT), +5.2, 5.3 and 5.4. Stdlib has no prerequisites beyond a standard Lua +system. + +## LICENSE + +The libraries are copyright by their authors 2000-2018, and released under +the MIT license (the same license as Lua itself). There is no warranty. +]] + +dir = "../doc" + +file = { + -- Modules + "../lib/std.lua", + "../lib/std/debug.lua", + "../lib/std/functional.lua", + "../lib/std/io.lua", + "../lib/std/math.lua", + "../lib/std/operator.lua", + "../lib/std/package.lua", + "../lib/std/strict.lua", + "../lib/std/string.lua", + "../lib/std/table.lua", + "../lib/std/tree.lua", + + -- Classes + "../lib/std/container.lua", + "../lib/std/object.lua", + "../lib/std/list.lua", + "../lib/std/optparse.lua", + "../lib/std/set.lua", + "../lib/std/strbuf.lua", +} + +new_type ("object", "Objects", false, "Fields") + +format = "markdown" + +sort = true + +backtick_references = false diff --git a/build-aux/config.ld.in b/build-aux/config.ld.in new file mode 100644 index 0000000..0571ab7 --- /dev/null +++ b/build-aux/config.ld.in @@ -0,0 +1,48 @@ +-- -*- lua -*- +title = "stdlib @PACKAGE_VERSION@ Reference" +project = "stdlib @PACKAGE_VERSION@" +description = [[ +# Standard Lua Libraries + +This is a collection of Lua libraries for Lua 5.1 (including LuaJIT), +5.2, 5.3 and 5.4. Stdlib has no prerequisites beyond a standard Lua +system. + +## LICENSE + +The libraries are copyright by their authors 2000-2018, and released under +the MIT license (the same license as Lua itself). There is no warranty. +]] + +dir = "../doc" + +file = { + -- Modules + "../lib/std.lua", + "../lib/std/debug.lua", + "../lib/std/functional.lua", + "../lib/std/io.lua", + "../lib/std/math.lua", + "../lib/std/operator.lua", + "../lib/std/package.lua", + "../lib/std/strict.lua", + "../lib/std/string.lua", + "../lib/std/table.lua", + "../lib/std/tree.lua", + + -- Classes + "../lib/std/container.lua", + "../lib/std/object.lua", + "../lib/std/list.lua", + "../lib/std/optparse.lua", + "../lib/std/set.lua", + "../lib/std/strbuf.lua", +} + +new_type ("object", "Objects", false, "Fields") + +format = "markdown" + +sort = true + +backtick_references = false diff --git a/configure.ac b/configure.ac deleted file mode 100644 index d6daabf..0000000 --- a/configure.ac +++ /dev/null @@ -1,18 +0,0 @@ -dnl Process this file with autoconf to produce a configure script - -dnl Initialise autoconf and automake -AC_INIT(stdlib, 30, rrt@sc3d.org) -AC_CONFIG_AUX_DIR([build-aux]) -AM_INIT_AUTOMAKE([foreign]) -AM_SILENT_RULES([yes]) - -dnl Lua -AC_SUBST([LUA_MIN_VERSION], [5.1]) -AX_PROG_LUA([$LUA_MIN_VERSION], [5.3]) - -AX_WITH_PROG([LUADOC], [luadoc], [:]) - -dnl Generate output files -AC_CONFIG_MACRO_DIR(m4) -AC_CONFIG_FILES([Makefile luarocks-config.lua]) -AC_OUTPUT diff --git a/doc/classes/std.container.html b/doc/classes/std.container.html new file mode 100644 index 0000000..221e4f4 --- /dev/null +++ b/doc/classes/std.container.html @@ -0,0 +1,178 @@ + + + + + stdlib 41.2.2 Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Class std.container

+

Container prototype.

+

+ + +

A container is a std.object with no methods. It's functionality is + instead defined by its metamethods.

+ +

Where an Object uses the __index metatable entry to hold object + methods, a Container stores its contents using __index, preventing + it from having methods in there too.

+ +

Although there are no actual methods, Containers are free to use + metamethods (__index, __sub, etc) and, like Objects, can supply + module functions by listing them in _functions. Also, since a + std.container is a std.object, it can be passed to the + std.object module functions, or anywhere else a std.object is + expected.

+ +

When making your own prototypes, derive from std.container if you want + to access the contents of your objects with the [] operator, or from + std.object if you want to access the functionality of your objects with + named object methods.

+ +

Prototype Chain

+ + +
+table
+ `-> Object
+      `-> Container
+
+ +

+ + +

Objects

+ + + + + +
std.container.ContainerContainer prototype.
+ +
+
+ + +

Objects

+ +
+
+ + std.container.Container +
+
+ Container prototype.

+ +

Container also inherits all the fields and methods from + std.object.Object. + + +

Fields:

+
    +
  • _type + string + object name + (default "Container") +
  • +
+ + + +

See also:

+ + +

Usage:

+
    +
    local std = require "std"
    +local Container = std.container {}
    +
    +local Graph = Container {
    +  _type = "Graph",
    +  _functions = {
    +    nodes = function (graph)
    +      local n = 0
    +      for _ in std.pairs (graph) do n = n + 1 end
    +      return n
    +    end,
    +  },
    +}
    +local g = Graph { "node1", "node2" }
    +--> 2
    +print (Graph.nodes (g))
    +
+ +
+
+ + +
+
+
+generated by LDoc 1.4.6 +Last updated 2018-09-16 19:20:25 +
+
+ + diff --git a/doc/classes/std.list.html b/doc/classes/std.list.html new file mode 100644 index 0000000..0098775 --- /dev/null +++ b/doc/classes/std.list.html @@ -0,0 +1,567 @@ + + + + + stdlib 41.2.2 Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Class std.list

+

Tables as lists.

+

+ + +

Prototype Chain

+ + +
+table
+ `-> Object
+      `-> List
+
+ +

+ + +

Objects

+ + + + + +
std.list.ListAn Object derived List.
+

Functions

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
std.list.append (l, x)Append an item to a list.
std.list.compare (l, m)Compare two lists element-by-element, from left-to-right.
std.list.concat (l, ...)Concatenate the elements from any number of lists.
std.list.cons (l, x)Prepend an item to a list.
std.list.rep (l, n)Repeat a list.
std.list.sub (l[, from=1[, to=#l]])Return a sub-range of a list.
std.list.tail (l)Return a list with its first element removed.
+

Metamethods

+ + + + + + + + + + + + + + + + + +
std.list:__add (l, e)Append element to list.
std.list:__concat (l, m)Concatenate lists.
std.list:__le (l, m)List equality or order operator.
std.list:__lt (l, m)List order operator.
+ +
+
+ + +

Objects

+ +
+
+ + std.list.List +
+
+ An Object derived List. + + + + + + + +
+
+

Functions

+ Methods +
+
+ + std.list.append (l, x) +
+
+ Append an item to a list. + + +

Parameters:

+
    +
  • l + List + a list +
  • +
  • x + item +
  • +
+ +

Returns:

+
    + + List + new list with x appended +
+ + + +

Usage:

+
    +
    longer = append (short, "last")
    +
+ +
+
+ + std.list.compare (l, m) +
+
+ Compare two lists element-by-element, from left-to-right. + + +

Parameters:

+
    +
  • l + List + a list +
  • +
  • m + List or table + another list, or table +
  • +
+ +

Returns:

+
    + + -1 if l is less than m, 0 if they are the same, and 1 + if l is greater than m +
+ + + +

Usage:

+
    +
    if a_list:compare (another_list) == 0 then print "same" end
    +
+ +
+
+ + std.list.concat (l, ...) +
+
+ Concatenate the elements from any number of lists. + + +

Parameters:

+
    +
  • l + List + a list +
  • +
  • ... + tuple of lists +
  • +
+ +

Returns:

+
    + + List + new list with elements from arguments +
+ + + +

Usage:

+
    +
    --> {1, 2, 3, {4, 5}, 6, 7}
    +list.concat ({1, 2, 3}, {{4, 5}, 6, 7})
    +
+ +
+
+ + std.list.cons (l, x) +
+
+ Prepend an item to a list. + + +

Parameters:

+
    +
  • l + List + a list +
  • +
  • x + item +
  • +
+ +

Returns:

+
    + + List + new list with x followed by elements of l +
+ + + +

Usage:

+
    +
    --> {"x", 1, 2, 3}
    +list.cons ({1, 2, 3}, "x")
    +
+ +
+
+ + std.list.rep (l, n) +
+
+ Repeat a list. + + +

Parameters:

+
    +
  • l + List + a list +
  • +
  • n + int + number of times to repeat +
  • +
+ +

Returns:

+
    + + List + n copies of l appended together +
+ + + +

Usage:

+
    +
    --> {1, 2, 3, 1, 2, 3, 1, 2, 3}
    +list.rep ({1, 2, 3}, 3)
    +
+ +
+
+ + std.list.sub (l[, from=1[, to=#l]]) +
+
+ Return a sub-range of a list. + (The equivalent of ??? on strings; negative list indices + count from the end of the list.) + + +

Parameters:

+
    +
  • l + List + a list +
  • +
  • from + int + start of range + (default 1) +
  • +
  • to + int + end of range + (default #l) +
  • +
+ +

Returns:

+
    + + List + new list containing elements between from and to + inclusive +
+ + + +

Usage:

+
    +
    --> {3, 4, 5}
    +list.sub ({1, 2, 3, 4, 5, 6}, 3, 5)
    +
+ +
+
+ + std.list.tail (l) +
+
+ Return a list with its first element removed. + + +

Parameters:

+
    +
  • l + List + a list +
  • +
+ +

Returns:

+
    + + List + new list with all but the first element of l +
+ + + +

Usage:

+
    +
    --> {3, {4, 5}, 6, 7}
    +list.tail {{1, 2}, 3, {4, 5}, 6, 7}
    +
+ +
+
+

Metamethods

+ +
+
+ + std.list:__add (l, e) +
+
+ Append element to list. + + +

Parameters:

+
    +
  • l + List + a list +
  • +
  • e + element to append +
  • +
+ + + +

See also:

+ + +

Usage:

+
    +
    list = list + "element"
    +
+ +
+
+ + std.list:__concat (l, m) +
+
+ Concatenate lists. + + +

Parameters:

+
    +
  • l + List + a list +
  • +
  • m + List or table + another list, or table (hash part is ignored) +
  • +
+ + + +

See also:

+ + +

Usage:

+
    +
    new = alist .. {"append", "these", "elements"}
    +
+ +
+
+ + std.list:__le (l, m) +
+
+ List equality or order operator. + + +

Parameters:

+
    +
  • l + List + a list +
  • +
  • m + List + another list +
  • +
+ + + +

See also:

+ + +

Usage:

+
    +
    min = list1 <= list2 and list1 or list2
    +
+ +
+
+ + std.list:__lt (l, m) +
+
+ List order operator. + + +

Parameters:

+
    +
  • l + List + a list +
  • +
  • m + List + another list +
  • +
+ + + +

See also:

+ + +

Usage:

+
    +
    max = list1 > list2 and list1 or list2
    +
+ +
+
+ + +
+
+
+generated by LDoc 1.4.6 +Last updated 2018-09-16 19:20:25 +
+
+ + diff --git a/doc/classes/std.object.html b/doc/classes/std.object.html new file mode 100644 index 0000000..24cb886 --- /dev/null +++ b/doc/classes/std.object.html @@ -0,0 +1,505 @@ + + + + + stdlib 41.2.2 Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Class std.object

+

Prototype-based objects.

+

+ + +

This module creates the root prototype object from which every other + object is descended. There are no classes as such, rather new objects + are created by cloning an existing object, and then changing or adding + to the clone. Further objects can then be made by cloning the changed + object, and so on.

+ +

Objects are cloned by simply calling an existing object, which then + serves as a prototype from which the new object is copied.

+ +

Note that Object methods are stored in the __index field of their + metatable, and so cannot also use __index to lookup references with + square brackets. See std.container objects if you want to do that.

+ +

Prototype Chain

+ + +
+table
+ `-> Object
+
+ +

+ + +

Objects

+ + + + + +
std.object.ObjectRoot object.
+

Functions

+ + + + + + + + + + + + + +
std.object.clone (obj, ...)Clone an Object.
std.object.mapfields (obj, src[, map={}])Return obj with references to the fields of src merged in.
std.object.prototype (x)Type of an object, or primitive.
+

Metamethods

+ + + + + + + + + + + + + +
std.object:__call (...)Return a clone of this object, and its metatable.
std.object:__pairs ()Return an in-order iterator over public object fields.
std.object:__tostring ()Return a string representation of this object.
+ +
+
+ + +

Objects

+ +
+
+ + std.object.Object +
+
+ Root object.

+ +

Changing the values of these fields in a new object will change the + corresponding behaviour. + + +

Fields:

+
    +
  • _init + table or function + object initialisation + (default {}) +
  • +
  • _functions + table + module functions omitted when cloned +
  • +
  • _type + string + object name + (default "Object") +
  • +
+ + + +

See also:

+ + +

Usage:

+
    +
  • -- `_init` can be a list of keys; then the unnamed `init_1` through
    +-- `init_m` values from the argument table are assigned to the
    +-- corresponding keys in `new_object`.
    +local Process = Object {
    +  _type = "Process",
    +  _init = { "status", "out", "err" },
    +}
    +local process = Process {
    +  procs[pid].status, procs[pid].out, procs[pid].err, -- auto assigned
    +  command = pipeline[pid],                           -- manual assignment
    +}
  • +
  • -- Or it can be a function, in which the arguments passed to the
    +-- prototype during cloning are simply handed to the `_init` function.
    +local Bag = Object {
    +  _type = "Bag",
    +  _init = function (obj, ...)
    +    for e in std.elems {...} do
    +      obj[#obj + 1] = e
    +    end
    +    return obj
    +  end,
    +}
    +local bag = Bag ("function", "arguments", "sent", "to", "_init")
  • +
+ +
+
+

Functions

+ Methods +
+
+ + std.object.clone (obj, ...) +
+
+ Clone an Object.

+ +

Objects are essentially tables of field_n = value_n pairs.

+ +

Normally new_object automatically shares a metatable with + proto_object. However, field names beginning with "_" are private, + and moved into the object metatable during cloning. So, adding new + private fields to an object during cloning will result in a new + metatable for new_object that also happens to contain a copy of all + the entries from the proto_object metatable.

+ +

While clones of Object inherit all properties of their prototype, + it's idiomatic to always keep separate tables for the module table and + the root object itself: That way you can't mistakenly engage the slower + clone-from-module-table process unnecessarily. + + +

Parameters:

+
    +
  • obj + Object + an object +
  • +
  • ... + a list of arguments if obj._init is a function, or a + single table if obj._init is a table. +
  • +
+ +

Returns:

+
    + + Object + a clone of obj +
+ + +

See also:

+ + +

Usage:

+
    +
    local object = require "std.object"  -- module table
    +local Object = object {}             -- root object
    +local o = Object {
    +  field_1 = "value_1",
    +  method_1 = function (self) return self.field_1 end,
    +}
    +print (o.field_1)                    --> value_1
    +o.field_2 = 2
    +function o:method_2 (n) return self.field_2 + n end
    +print (o:method_2 (2))               --> 4
    +os.exit (0)
    +
+ +
+
+ + std.object.mapfields (obj, src[, map={}]) +
+
+ Return obj with references to the fields of src merged in.

+ +

More importantly, split the fields in src between obj and its + metatable. If any field names begin with "_", attach a metatable + to obj by cloning the metatable from src, and then copy the + "private" _ prefixed fields there.

+ +

You might want to use this function to instantiate your derived + object clones when the src._init is a function -- when + src._init is a table, the default (inherited unless you overwrite + it) clone method calls mapfields automatically. When you're + using a function _init setting, clone doesn't know what to + copy into a new object from the _init function's arguments... + so you're on your own. Except that calling mapfields inside + _init is safer than manually splitting src into obj and + its metatable, because you'll pick up any fixes and changes when + you upgrade stdlib. + + +

Parameters:

+
    +
  • obj + table + destination object +
  • +
  • src + table + fields to copy int clone +
  • +
  • map + table + key renames as {old_key=new_key, ...} + (default {}) +
  • +
+ +

Returns:

+
    + + table + obj with non-private fields from src merged, + and a metatable with private fields (if any) merged, both sets + of keys renamed according to map +
+ + + +

Usage:

+
    +
    myobject.mapfields = function (obj, src, map)
    +  object.mapfields (obj, src, map)
    +  ...
    +end
    +
+ +
+
+ + std.object.prototype (x) +
+
+ Type of an object, or primitive.

+ +

It's conventional to organise similar objects according to a + string valued _type field, which can then be queried using this + function.

+ +

Additionally, this function returns the results of ??? for + file objects, or type otherwise. + + +

Parameters:

+
    +
  • x + anything +
  • +
+ +

Returns:

+
    + + string + type of x +
+ + + +

Usage:

+
    +
    local Stack = Object {
    +  _type = "Stack",
    +
    +  __tostring = function (self) ... end,
    +
    +  __index = {
    +    push = function (self) ... end,
    +    pop  = function (self) ... end,
    +  },
    +}
    +local stack = Stack {}
    +assert (stack:prototype () == getmetatable (stack)._type)
    +
    +local prototype = Object.prototype
    +assert (prototype (stack) == getmetatable (stack)._type)
    +
    +local h = io.open (os.tmpname (), "w")
    +assert (prototype (h) == io.type (h))
    +
    +assert (prototype {} == type {})
    +
+ +
+
+

Metamethods

+ +
+
+ + std.object:__call (...) +
+
+ Return a clone of this object, and its metatable.

+ +

Private fields are stored in the metatable. + + +

Parameters:

+
    +
  • ... + arguments for prototype's _init +
  • +
+ +

Returns:

+
    + + Object + a clone of the this object. +
+ + +

See also:

+ + +

Usage:

+
    +
    local Object = require "std.object" {} -- not a typo!
    +new = Object {"initialisation", "elements"}
    +
+ +
+
+ + std.object:__pairs () +
+
+ Return an in-order iterator over public object fields. + + + +

Returns:

+
    +
  1. + function + iterator function
  2. +
  3. + Object + self
  4. +
+ + + +

Usage:

+
    +
    for k, v in std.pairs (anobject) do process (k, v) end
    +
+ +
+
+ + std.object:__tostring () +
+
+ Return a string representation of this object.

+ +

First the object type, and then between { and } a list of the + array part of the object table (without numeric keys) followed + by the remaining key-value pairs.

+ +

This function doesn't recurse explicity, but relies upon suitable + __tostring metamethods in field values. + + + +

Returns:

+
    + + string + stringified object representation +
+ + +

See also:

+ + +

Usage:

+
    +
    print (anobject)
    +
+ +
+
+ + +
+
+
+generated by LDoc 1.4.6 +Last updated 2018-09-16 19:20:25 +
+
+ + diff --git a/doc/classes/std.optparse.html b/doc/classes/std.optparse.html new file mode 100644 index 0000000..40569e3 --- /dev/null +++ b/doc/classes/std.optparse.html @@ -0,0 +1,893 @@ + + + + + stdlib 41.2.2 Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Class std.optparse

+

Parse and process command line options.

+

+ + +

Prototype Chain

+ + +
+table
+ `-> Object
+      `-> OptionParser
+
+ +

+ + +

Objects

+ + + + + +
std.optparse.OptionParserOptionParser prototype object.
+

Functions

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
std.optparse.OptionParser_Init (spec)Signature for initialising a custom OptionParser.
std.optparse.boolean (opt[, optarg="1"])Return a Lua boolean equivalent of various optarg strings.
std.optparse.file (opt, optarg)Report an option parse error unless optarg names an + existing file.
std.optparse.finished (arglist, i)Finish option processing

+ +

This is the handler automatically assigned to the option written as + -- in the OptionParser spec argument.

std.optparse.flag (arglist, i[, value])Option at arglist[i] is a boolean switch.
std.optparse.help ()Option should display help text, then exit.
std.optparse.opterr (msg)Report an option parse error, then exit with status 2.
std.optparse.optional (arglist, i[, value=true])Option at arglist[i] can take an argument.
std.optparse.required (arglist, i[, value])Option at arglist[i} requires an argument.
std.optparse.version ()Option should display version text, then exit.
+

Tables

+ + + + + + + + + +
std.optparse.boolvalsMap various option strings to equivalent Lua boolean values.
std.optparse.optsParsed options table, with a key for each encountered option, each + with value set by that option's on_handler.
+

Methods

+ + + + + + + + + + + + + +
std.optparse:on (name, handler, value)Add an option handler.
std.optparse:on_handler (arglist, i[, value=nil])Function signature of an option handler for on.
std.optparse:parse (arglist[, defaults])Parse an argument list.
+ +
+
+ + +

Objects

+ +
+
+ + std.optparse.OptionParser +
+
+ OptionParser prototype object.

+ +

Most often, after instantiating an OptionParser, everything else + is handled automatically.

+ +

Then, calling parser:parse as shown below saves unparsed arguments + into _G.arg (usually filenames or similar), and _G.opts will be a + table of successfully parsed option values. The keys into this table + are the long-options with leading hyphens stripped, and non-word + characters turned to _. For example if --another-long had been + found in the initial _G.arg, then _G.opts will have a key named + another_long, with an appropriate value. If there is no long + option name, then the short option is used, i.e. _G.opts.b will be + set.

+ +

The values saved against those keys are controlled by the option + handler, usually just true or the option argument string as + appropriate. + + +

Fields:

+
    +
  • _init + OptionParser_Init + initialisation function +
  • +
  • program + string + the first word following "Usage:" from spec +
  • +
  • version + string + the last white-space delimited word on the first line + of text from spec +
  • +
  • versiontext + string + everything preceding "Usage:" from spec, and + which will be displayed by the version on_handler +
  • +
  • helptext + string + everything including and following "Usage:" from + spec string and which will be displayed by the help + on_handler +
  • +
+ + + + +

Usage:

+
    +
    local std = require "std"
    +
    +local optparser = std.optparse [[
    +any text VERSION
    +Additional lines of text to show when the --version
    +option is passed.
    +
    +Several lines or paragraphs are permitted.
    +
    +Usage: PROGNAME
    +
    +Banner text.
    +
    +Optional long description text to show when the --help
    +option is passed.
    +
    +Several lines or paragraphs of long description are permitted.
    +
    +Options:
    +
    +  -b                       a short option with no long option
    +      --long               a long option with no short option
    +      --another-long       a long option with internal hypen
    +  -v, --verbose            a combined short and long option
    +  -n, --dryrun, --dry-run  several spellings of the same option
    +  -u, --name=USER          require an argument
    +  -o, --output=[FILE]      accept an optional argument
    +      --version            display version information, then exit
    +      --help               display this help, then exit
    +
    +Footer text.  Several lines or paragraphs are permitted.
    +
    +Please report bugs at bug-list@yourhost.com
    +]]
    +
    +-- Note that std.io.die and std.io.warn will only prefix messages
    +-- with `parser.program` if the parser options are assigned back to
    +-- `_G.opts`:
    +_G.arg, _G.opts = optparser:parse (_G.arg)
    +
+ +
+
+

Functions

+ Methods +
+
+ + std.optparse.OptionParser_Init (spec) +
+
+ Signature for initialising a custom OptionParser.

+ +

Read the documented options from spec and return custom parser that + can be used for parsing the options described in spec from a run-time + argument list. Options in spec are recognised as lines that begin + with at least two spaces, followed by a hyphen. + + +

Parameters:

+
    +
  • spec + string + option parsing specification +
  • +
+ +

Returns:

+
    + + OptionParser + a parser for options described by spec +
+ + + +

Usage:

+
    +
    customparser = std.optparse (optparse_spec)
    +
+ +
+
+ + std.optparse.boolean (opt[, optarg="1"]) +
+
+ Return a Lua boolean equivalent of various optarg strings. + Report an option parse error if optarg is not recognised.

+ +

Pass this as the value function to on when you want various + "truthy" or "falsey" option arguments to be coerced to a Lua true + or false respectively in the options table. + + +

Parameters:

+
    +
  • opt + string + option name +
  • +
  • optarg + string + option argument, must be a key in boolvals + (default "1") +
  • +
+ +

Returns:

+
    + + bool + true or false +
+ + + +

Usage:

+
    +
    parser:on ("--enable-nls", parser.optional, parser.boolean)
    +
+ +
+
+ + std.optparse.file (opt, optarg) +
+
+ Report an option parse error unless optarg names an + existing file.

+ +

Pass this as the value function to on when you want to accept + only option arguments that name an existing file. + + +

Parameters:

+
    +
  • opt + string + option name +
  • +
  • optarg + string + option argument, must be an existing file +
  • +
+ +

Returns:

+
    + + string + optarg +
+ + + +

Usage:

+
    +
    parser:on ("--config-file", parser.required, parser.file)
    +
+ +
+
+ + std.optparse.finished (arglist, i) +
+
+ Finish option processing

+ +

This is the handler automatically assigned to the option written as + -- in the OptionParser spec argument. You can also pass it as + the handler argument to on if you want to manually add an end + of options marker without writing it in the OptionParser spec.

+ +

This handler tells the parser to stop processing arguments, so that + anything after it will be an argument even if it otherwise looks + like an option. + + +

Parameters:

+
    +
  • arglist + table + list of arguments +
  • +
  • i + int + index of last processed element of arglist +
  • +
+ +

Returns:

+
    + + int + index of next element of arglist to process +
+ + + +

Usage:

+
    +
    parser:on ("--", parser.finished)
    +
+ +
+
+ + std.optparse.flag (arglist, i[, value]) +
+
+ Option at arglist[i] is a boolean switch.

+ +

This is the handler automatically assigned to options that have + --long-opt or -x style specifications in the OptionParser spec + argument. You can also pass it as the handler argument to on for + options you want to add manually without putting them in the + OptionParser spec.

+ +

Beware that, unlike required, this handler will store multiple + occurrences of a command-line option as a table only when given a + value function. Automatically assigned handlers do not do this, so + the option will simply be true if the option was given one or more + times on the command-line. + + +

Parameters:

+
    +
  • arglist + table + list of arguments +
  • +
  • i + int + index of last processed element of arglist +
  • +
  • value + either a function to process the option argument, + or a value to store when this flag is encountered + (optional) +
  • +
+ +

Returns:

+
    + + int + index of next element of arglist to process +
+ + + +

Usage:

+
    +
    parser:on ({"--long-opt", "-x"}, parser.flag)
    +
+ +
+
+ + std.optparse.help () +
+
+ Option should display help text, then exit.

+ +

This is the handler automatically assigned tooptions that have + --help in the specification, e.g. -h, -?, --help. + + + + + + +

Usage:

+
    +
    parser:on ("-?", parser.version)
    +
+ +
+
+ + std.optparse.opterr (msg) +
+
+ Report an option parse error, then exit with status 2.

+ +

Use this in your custom option handlers for consistency with the + error output from built-in std.optparse error messages. + + +

Parameters:

+
    +
  • msg + string + error message +
  • +
+ + + + + +
+
+ + std.optparse.optional (arglist, i[, value=true]) +
+
+ Option at arglist[i] can take an argument. + Argument is accepted only if there is a following entry that does not + begin with a '-'.

+ +

This is the handler automatically assigned to options that have + --opt=[ARG] style specifications in the OptionParser spec + argument. You can also pass it as the handler argument to on for + options you want to add manually without putting them in the + OptionParser spec.

+ +

Like required, this handler will store multiple occurrences of a + command-line option. + + +

Parameters:

+
    +
  • arglist + table + list of arguments +
  • +
  • i + int + index of last processed element of arglist +
  • +
  • value + either a function to process the option + argument, or a default value if encountered without an optarg + (default true) +
  • +
+ +

Returns:

+
    + + int + index of next element of arglist to process +
+ + + +

Usage:

+
    +
    parser:on ("--enable-nls", parser.option, parser.boolean)
    +
+ +
+
+ + std.optparse.required (arglist, i[, value]) +
+
+ +

Option at arglist[i} requires an argument.

+ +

This is the handler automatically assigned to options that have + --opt=ARG style specifications in the OptionParser spec argument. + You can also pass it as the handler argument to on for options + you want to add manually without putting them in the OptionParser + spec.

+ +

Normally the value stored in the opt table by this handler will be + the string given as the argument to that option on the command line. + However, if the option is given on the command-line multiple times, + opt["name"] will end up with all those arguments stored in the + array part of a table:

+ +
 $ cat ./prog
+ ...
+ parser:on ({"-e", "-exec"}, required)
+ _G.arg, _G.opt = parser:parse (_G.arg)
+ print std.string.tostring (_G.opt.exec)
+ ...
+ $ ./prog -e '(foo bar)' -e '(foo baz)' -- qux
+ {1=(foo bar),2=(foo baz)}
+
+ + + +

Parameters:

+
    +
  • arglist + table + list of arguments +
  • +
  • i + int + index of last processed element of arglist +
  • +
  • value + either a function to process the option argument, + or a forced value to replace the user's option argument. + (optional) +
  • +
+ +

Returns:

+
    + + int + index of next element of arglist to process +
+ + + +

Usage:

+
    +
    parser:on ({"-o", "--output"}, parser.required)
    +
+ +
+
+ + std.optparse.version () +
+
+ Option should display version text, then exit.

+ +

This is the handler automatically assigned tooptions that have + --version in the specification, e.g. -V, --version. + + + + + + +

Usage:

+
    +
    parser:on ("-V", parser.version)
    +
+ +
+
+

Tables

+ +
+
+ + std.optparse.boolvals +
+
+ Map various option strings to equivalent Lua boolean values. + + +

Fields:

+
    +
  • false + false +
  • +
  • 0 + false +
  • +
  • no + false +
  • +
  • n + false +
  • +
  • true + true +
  • +
  • 1 + true +
  • +
  • yes + true +
  • +
  • y + true +
  • +
+ + + + + +
+
+ + std.optparse.opts +
+
+ Parsed options table, with a key for each encountered option, each + with value set by that option's on_handler. Where an option + has one or more long-options specified, the key will be the first + one of those with leading hyphens stripped and non-alphanumeric + characters replaced with underscores. For options that can only be + specified by a short option, the key will be the letter of the first + of the specified short options:

+ +
 {"-e", "--eval-file"} => opts.eval_file
+ {"-n", "--dryrun", "--dry-run"} => opts.dryrun
+ {"-t", "-T"} => opts.t
+
+ +

Generally there will be one key for each previously specified + option (either automatically assigned by OptionParser or + added manually with on) containing the value(s) assigned by the + associated on_handler. For automatically assigned handlers, + that means true for straight-forward flags and + optional-argument options for which no argument was given; or else + the string value of the argument passed with an option given only + once; or a table of string values of the same for arguments given + multiple times.

+ +
 ./prog -x -n -x => opts = { x = true, dryrun = true }
+ ./prog -e '(foo bar)' -e '(foo baz)'
+     => opts = {eval_file = {"(foo bar)", "(foo baz)"} }
+
+ +

If you write your own handlers, or otherwise specify custom + handling of options with on, then whatever value those handlers + return will be assigned to the respective keys in opts. + + + + + + + +

+
+

Methods

+ +
+
+ + std.optparse:on (name, handler, value) +
+
+ Add an option handler.

+ +

When the automatically assigned option handlers don't do everything + you require, or when you don't want to put an option into the + OptionParser spec argument, use this function to specify custom + behaviour. If you write the option into the spec argument anyway, + calling this function will replace the automatically assigned handler + with your own.

+ +

When writing your own handlers for std.optparse:on, you only need + to deal with normalised arguments, because combined short arguments + (-xyz), equals separators to long options (--long=ARG) are fully + expanded before any handler is called. + + +

Parameters:

+
    +
  • name + opts + of the option, or list of option names +
  • +
  • handler + on_handler + function to call when any of opts is + encountered +
  • +
  • value + additional value passed to on_handler +
  • +
+ + + + +

Usage:

+
    +
    -- Don't process any arguments after `--`
    +parser:on ('--', parser.finished)
    +
+ +
+
+ + std.optparse:on_handler (arglist, i[, value=nil]) +
+
+ Function signature of an option handler for on. + + +

Parameters:

+
    +
  • arglist + table + list of arguments +
  • +
  • i + int + index of last processed element of arglist +
  • +
  • value + additional value registered with on + (default nil) +
  • +
+ +

Returns:

+
    + + int + index of next element of arglist to process +
+ + + + +
+
+ + std.optparse:parse (arglist[, defaults]) +
+
+ Parse an argument list. + + +

Parameters:

+
    +
  • arglist + table + list of arguments +
  • +
  • defaults + table + table of default option values + (optional) +
  • +
+ +

Returns:

+
    +
  1. + table + a list of unrecognised arglist elements
  2. +
  3. + opts + parsing results
  4. +
+ + + + +
+
+ + +
+
+
+generated by LDoc 1.4.6 +Last updated 2018-09-16 19:20:25 +
+
+ + diff --git a/doc/classes/std.set.html b/doc/classes/std.set.html new file mode 100644 index 0000000..cce8a30 --- /dev/null +++ b/doc/classes/std.set.html @@ -0,0 +1,863 @@ + + + + + stdlib 41.2.2 Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Class std.set

+

Set container prototype.

+

+ + +

Note that Functions listed below are only available from the Set + prototype returned by requiring this module, because Container + objects cannot have object methods.

+ +

Prototype Chain

+ + +
+table
+ `-> Object
+      `-> Container
+           `-> Set
+
+ +

+

See also:

+ + + +

Objects

+ + + + + +
std.set.SetSet prototype object.
+

Functions

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
std.set.delete (set, e)Delete an element from a set.
std.set.difference (set1, set2)Find the difference of two sets.
std.set.difference (set, e)Say whether an element is in a set.
std.set.elems (set)Iterator for sets.
std.set.equal (set1, set2)Find whether two sets are equal.
std.set.insert (set, e)Insert an element into a set.
std.set.intersection (set1, set2)Find the intersection of two sets.
std.set.proper_subset (set1, set2)Find whether one set is a proper subset of another.
std.set.subset (set1, set2)Find whether one set is a subset of another.
std.set.symmetric_difference (set1, set2)Find the symmetric difference of two sets.
std.set.union (set1, set2)Find the union of two sets.
+

Metamethods

+ + + + + + + + + + + + + + + + + + + + + + + + + +
std.set.__add (set1, set2)Union operator.
std.set.__div (set1, set2)Symmetric difference operator.
std.set.__le (set1, set2)Subset operator.
std.set.__lt (set1, set2)Proper subset operator.
std.set.__mul (set1, set2)Intersection operator.
std.set.__sub (set1, set2)Difference operator.
+ +
+
+ + +

Objects

+ +
+
+ + std.set.Set +
+
+ Set prototype object.

+ +

Set also inherits all the fields and methods from + std.container.Container. + + +

Fields:

+
    +
  • _type + string + object name + (default "Set") +
  • +
+ + + +

See also:

+ + +

Usage:

+
    +
    local std = require "std"
    +std.prototype (std.set) --> "Set"
    +os.exit (0)
    +
+ +
+
+

Functions

+ Methods +
+
+ + std.set.delete (set, e) +
+
+ Delete an element from a set. + + +

Parameters:

+
    +
  • set + Set + a set +
  • +
  • e + element +
  • +
+ +

Returns:

+
    + + Set + the modified set +
+ + + +

Usage:

+
    +
    set.delete (available, found)
    +
+ +
+
+ + std.set.difference (set1, set2) +
+
+ Find the difference of two sets. + + +

Parameters:

+
    +
  • set1 + Set + a set +
  • +
  • set2 + Set + another set +
  • +
+ +

Returns:

+
    + + Set + a copy of set1 with elements of set2 removed +
+ + + +

Usage:

+
    +
    all = set.difference (all, {32, 49, 56})
    +
+ +
+
+ + std.set.difference (set, e) +
+
+ Say whether an element is in a set. + + +

Parameters:

+
    +
  • set + Set + a set +
  • +
  • e + element +
  • +
+ +

Returns:

+
    + + true if e is in set, otherwise false + otherwise +
+ + + +

Usage:

+
    +
    if not set.member (keyset, pressed) then return nil end
    +
+ +
+
+ + std.set.elems (set) +
+
+ Iterator for sets. + + +

Parameters:

+
    +
  • set + Set + a set +
  • +
+ +

Returns:

+
    + + *set* + iterator +
+ + + +

Usage:

+
    +
    for code in set.elems (isprintable) do print (code) end
    +
+ +
+
+ + std.set.equal (set1, set2) +
+
+ Find whether two sets are equal. + + +

Parameters:

+
    +
  • set1 + Set + a set +
  • +
  • set2 + Set + another set +
  • +
+ +

Returns:

+
    + + boolean + true if set1 and set2 each contain identical + elements, false otherwise +
+ + + +

Usage:

+
    +
    if set.equal (keys, {META, CTRL, "x"}) then process (keys) end
    +
+ +
+
+ + std.set.insert (set, e) +
+
+ Insert an element into a set. + + +

Parameters:

+
    +
  • set + Set + a set +
  • +
  • e + element +
  • +
+ +

Returns:

+
    + + Set + the modified set +
+ + + +

Usage:

+
    +
    for byte = 32,126 do
    +  set.insert (isprintable, string.char (byte))
    +end
    +
+ +
+
+ + std.set.intersection (set1, set2) +
+
+ Find the intersection of two sets. + + +

Parameters:

+
    +
  • set1 + Set + a set +
  • +
  • set2 + Set + another set +
  • +
+ +

Returns:

+
    + + Set + a new set with elements in both set1 and set2 +
+ + + +

Usage:

+
    +
    common = set.intersection (a, b)
    +
+ +
+
+ + std.set.proper_subset (set1, set2) +
+
+ Find whether one set is a proper subset of another. + + +

Parameters:

+
    +
  • set1 + Set + a set +
  • +
  • set2 + Set + another set +
  • +
+ +

Returns:

+
    + + boolean + true if set2 contains all elements in set1 + but not only those elements, false otherwise +
+ + + +

Usage:

+
    +
    if set.proper_subset (a, b) then
    +  for e in set.elems (set.difference (b, a)) do
    +    set.delete (b, e)
    +  end
    +end
    +assert (set.equal (a, b))
    +
+ +
+
+ + std.set.subset (set1, set2) +
+
+ Find whether one set is a subset of another. + + +

Parameters:

+
    +
  • set1 + Set + a set +
  • +
  • set2 + Set + another set +
  • +
+ +

Returns:

+
    + + boolean + true if all elements in set1 are also in set2, + false otherwise +
+ + + +

Usage:

+
    +
    if set.subset (a, b) then a = b end
    +
+ +
+
+ + std.set.symmetric_difference (set1, set2) +
+
+ Find the symmetric difference of two sets. + + +

Parameters:

+
    +
  • set1 + Set + a set +
  • +
  • set2 + Set + another set +
  • +
+ +

Returns:

+
    + + Set + a new set with elements that are in set1 or set2 + but not both +
+ + + +

Usage:

+
    +
    unique = set.symmetric_difference (a, b)
    +
+ +
+
+ + std.set.union (set1, set2) +
+
+ Find the union of two sets. + + +

Parameters:

+
    +
  • set1 + Set + a set +
  • +
  • set2 + Set + another set +
  • +
+ +

Returns:

+
    + + Set + a copy of set1 with elements in set2 merged in +
+ + + +

Usage:

+
    +
    all = set.union (a, b)
    +
+ +
+
+

Metamethods

+ +
+
+ + std.set.__add (set1, set2) +
+
+ Union operator. + + +

Parameters:

+
    +
  • set1 + Set + a set +
  • +
  • set2 + Set + another set +
  • +
+ +

Returns:

+
    + + Set + everything from set1 plus everything from set2 +
+ + +

See also:

+ + +

Usage:

+
    +
    union = set1 + set2
    +
+ +
+
+ + std.set.__div (set1, set2) +
+
+ Symmetric difference operator. + + +

Parameters:

+
    +
  • set1 + Set + a set +
  • +
  • set2 + Set + another set +
  • +
+ +

Returns:

+
    + + Set + everything from set1 or set2 but not both +
+ + +

See also:

+ + +

Usage:

+
    +
    symmetric_difference = set1 / set2
    +
+ +
+
+ + std.set.__le (set1, set2) +
+
+ Subset operator. + + +

Parameters:

+
    +
  • set1 + Set + a set +
  • +
  • set2 + Set + another set +
  • +
+ +

Returns:

+
    + + boolean + true if everything in set1 is also in set2 +
+ + +

See also:

+ + +

Usage:

+
    +
    issubset = set1 <= set2
    +
+ +
+
+ + std.set.__lt (set1, set2) +
+
+ Proper subset operator. + + +

Parameters:

+
    +
  • set1 + Set + set +
  • +
  • set2 + Set + another set +
  • +
+ +

Returns:

+
    + + boolean + true if set2 is not equal to set1, but does + contain everything from set1 +
+ + +

See also:

+ + +

Usage:

+
    +
    ispropersubset = set1 < set2
    +
+ +
+
+ + std.set.__mul (set1, set2) +
+
+ Intersection operator. + + +

Parameters:

+
    +
  • set1 + Set + a set +
  • +
  • set2 + Set + another set +
  • +
+ +

Returns:

+
    + + Set + anything this is in both set1 and set2 +
+ + +

See also:

+ + +

Usage:

+
    +
    intersection = set1 * set2
    +
+ +
+
+ + std.set.__sub (set1, set2) +
+
+ Difference operator. + + +

Parameters:

+
    +
  • set1 + Set + a set +
  • +
  • set2 + Set + another set +
  • +
+ +

Returns:

+
    + + Set + everything from set1 that is not also in set2 +
+ + +

See also:

+ + +

Usage:

+
    +
    difference = set1 - set2
    +
+ +
+
+ + +
+
+
+generated by LDoc 1.4.6 +Last updated 2018-09-16 19:20:25 +
+
+ + diff --git a/doc/classes/std.strbuf.html b/doc/classes/std.strbuf.html new file mode 100644 index 0000000..91f0d1d --- /dev/null +++ b/doc/classes/std.strbuf.html @@ -0,0 +1,296 @@ + + + + + stdlib 41.2.2 Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Class std.strbuf

+

String buffers.

+

+ + +

Buffers are mutable by default, but being based on objects, they can + also be used in a functional style:

+ + +
+local StrBuf = require "std.strbuf" {}
+local a = StrBuf {"a"}
+local b = a:concat "b"    -- mutate *a*
+print (a, b)              --> ab   ab
+local c = a {} .. "c"     -- copy and append
+print (a, c)              --> ab   abc
+
+ +

Prototype Chain

+ + +
+table
+ `-> Object
+      `-> StrBuf
+
+ +

+ + +

Objects

+ + + + + +
std.strbuf.StrBufStrBuf prototype object.
+

Functions

+ + + + + +
std.strbuf.concat (x)Add a object to a buffer.
+

Metamethods

+ + + + + + + + + +
std.strbuf:__concat (buffer, x)Support concatenation to StrBuf objects.
std.strbuf:__tostring (buffer)Support fast conversion to Lua string.
+ +
+
+ + +

Objects

+ +
+
+ + std.strbuf.StrBuf +
+
+ StrBuf prototype object.

+ +

Set also inherits all the fields and methods from + std.object.Object. + + +

Fields:

+
    +
  • _type + string + object name + (default "StrBuf") +
  • +
+ + + +

See also:

+ + +

Usage:

+
    +
    local std = require "std"
    +local StrBuf = std.strbuf {}
    +local a = {1, 2, 3}
    +local b = {a, "five", "six"}
    +a = a .. 4
    +b = b:concat "seven"
    +print (a, b) --> 1234   1234fivesixseven
    +os.exit (0)
    +
+ +
+
+

Functions

+ Methods +
+
+ + std.strbuf.concat (x) +
+
+ Add a object to a buffer. + Elements are stringified lazily, so if add a table and then change + its contents, the contents of the buffer will be affected too. + + +

Parameters:

+
    +
  • x + object to add to buffer +
  • +
+ +

Returns:

+
    + + StrBuf + modified buffer +
+ + + +

Usage:

+
    +
    buf = buf:concat "append this" {" and", " this"}
    +
+ +
+
+

Metamethods

+ +
+
+ + std.strbuf:__concat (buffer, x) +
+
+ Support concatenation to StrBuf objects. + + +

Parameters:

+
    +
  • buffer + StrBuf + object +
  • +
  • x + a string, or object that can be coerced to a string +
  • +
+ +

Returns:

+
    + + StrBuf + modified buf +
+ + +

See also:

+ + +

Usage:

+
    +
    buf = buf .. x
    +
+ +
+
+ + std.strbuf:__tostring (buffer) +
+
+ Support fast conversion to Lua string. + + +

Parameters:

+
    +
  • buffer + StrBuf + object +
  • +
+ +

Returns:

+
    + + string + concatenation of buffer contents +
+ + +

See also:

+ + +

Usage:

+
    +
    str = tostring (buf)
    +
+ +
+
+ + +
+
+
+generated by LDoc 1.4.6 +Last updated 2018-09-16 19:20:25 +
+
+ + diff --git a/doc/classes/std.tree.html b/doc/classes/std.tree.html new file mode 100644 index 0000000..013c647 --- /dev/null +++ b/doc/classes/std.tree.html @@ -0,0 +1,544 @@ + + + + + stdlib 41.2.2 Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Class std.tree

+

Tree container prototype.

+

+ + +

Note that Functions listed below are only available from the Tree + prototype returned by requiring this module, because Container objects + cannot have object methods.

+ +

Prototype Chain

+ + +
+table
+ `-> Object
+      `-> Container
+           `-> Tree
+
+ +

+

See also:

+ + + +

Objects

+ + + + + +
std.tree.TreeTree prototype object.
+

Functions

+ + + + + + + + + + + + + + + + + + + + + + + + + +
std.tree.clone (t, nometa)Make a deep copy of a tree, including any metatables.
std.tree.ileaves (tr)Tree iterator which returns just numbered leaves, in order.
std.tree.inodes (tr)Tree iterator over numbered nodes, in order.
std.tree.leaves (t)Tree iterator which returns just leaves.
std.tree.merge (t, u)Destructively deep-merge one tree into another.
std.tree.nodes (tr)Tree iterator over all nodes.
+

Metamethods

+ + + + + + + + + +
std.tree.__index (tr, i)Deep retrieval.
std.tree.__newindex (tr, i[, v])Deep insertion.
+ +
+
+ + +

Objects

+ +
+
+ + std.tree.Tree +
+
+ Tree prototype object. + + +

Fields:

+
    +
  • _type + string + object name + (default "Tree") +
  • +
+ + + +

See also:

+ + +

Usage:

+
    +
    local std = require "std"
    +local Tree = std.tree {}
    +local tr = Tree {}
    +tr[{"branch1", 1}] = "leaf1"
    +tr[{"branch1", 2}] = "leaf2"
    +tr[{"branch2", 1}] = "leaf3"
    +print (tr[{"branch1"}])      --> Tree {leaf1, leaf2}
    +print (tr[{"branch1", 2}])   --> leaf2
    +print (tr[{"branch1", 3}])   --> nil
    +--> leaf1	leaf2	leaf3
    +for leaf in std.tree.leaves (tr) do
    +  io.write (leaf .. "\t")
    +end
    +
+ +
+
+

Functions

+ Methods +
+
+ + std.tree.clone (t, nometa) +
+
+ Make a deep copy of a tree, including any metatables. + + +

Parameters:

+
    +
  • t + table + tree or tree-like table +
  • +
  • nometa + boolean + if non-nil don't copy metatables +
  • +
+ +

Returns:

+
    + + Tree or table + a deep copy of tr +
+ + +

See also:

+ + +

Usage:

+
    +
    tr = {"one", {two=2}, {{"three"}, four=4}}
    +copy = clone (tr)
    +copy[2].two=5
    +assert (tr[2].two == 2)
    +
+ +
+
+ + std.tree.ileaves (tr) +
+
+ Tree iterator which returns just numbered leaves, in order. + + +

Parameters:

+
    +
  • tr + Tree or table + tree or tree-like table +
  • +
+ +

Returns:

+
    +
  1. + function + iterator function
  2. +
  3. + Tree or table + the tree tr
  4. +
+ + +

See also:

+ + +

Usage:

+
    +
    --> t = {"one", "three", "five"}
    +for leaf in ileaves {"one", {two=2}, {{"three"}, four=4}}, foo="bar", "five"}
    +do
    +  t[#t + 1] = leaf
    +end
    +
+ +
+
+ + std.tree.inodes (tr) +
+
+ Tree iterator over numbered nodes, in order.

+ +

The iterator function behaves like nodes, but only traverses the + array part of the nodes of tr, ignoring any others. + + +

Parameters:

+
    +
  • tr + Tree or table + tree or tree-like table to iterate over +
  • +
+ +

Returns:

+
    +
  1. + function + iterator function
  2. +
  3. + tree or table + the tree, tr
  4. +
+ + +

See also:

+ + + +
+
+ + std.tree.leaves (t) +
+
+ Tree iterator which returns just leaves. + + +

Parameters:

+
    +
  • t + table + tree or tree-like table +
  • +
+ +

Returns:

+
    +
  1. + function + iterator function
  2. +
  3. + table + t
  4. +
+ + +

See also:

+ + +

Usage:

+
    +
    for leaf in leaves {"one", {two=2}, {{"three"}, four=4}}, foo="bar", "five"}
    +do
    +  t[#t + 1] = leaf
    +end
    +--> t = {2, 4, "five", "foo", "one", "three"}
    +table.sort (t, lambda "=tostring(_1) < tostring(_2)")
    +
+ +
+
+ + std.tree.merge (t, u) +
+
+ Destructively deep-merge one tree into another. + + +

Parameters:

+
    +
  • t + table + destination tree +
  • +
  • u + table + table with nodes to merge +
  • +
+ +

Returns:

+
    + + table + t with nodes from u merged in +
+ + +

See also:

+ + +

Usage:

+
    +
    merge (dest, {{exists=1}, {{not = {present = { inside = "dest" }}}}})
    +
+ +
+
+ + std.tree.nodes (tr) +
+
+ Tree iterator over all nodes.

+ +

The returned iterator function performs a depth-first traversal of + tr, and at each node it returns {node-type, tree-path, tree-node} + where node-type is branch, join or leaf; tree-path is a + list of keys used to reach this node, and tree-node is the current + node.

+ +

Note that the tree-path reuses the same table on each iteration, so + you must table.clone a copy if you want to take a snap-shot of the + current state of the tree-path list before the next iteration + changes it. + + +

Parameters:

+
    +
  • tr + Tree or table + tree or tree-like table to iterate over +
  • +
+ +

Returns:

+
    +
  1. + function + iterator function
  2. +
  3. + Tree or table + the tree, tr
  4. +
+ + +

See also:

+ + +

Usage:

+
    +
    -- tree = +-- node1
    +--        |    +-- leaf1
    +--        |    '-- leaf2
    +--        '-- leaf 3
    +tree = Tree { Tree { "leaf1", "leaf2"}, "leaf3" }
    +for node_type, path, node in nodes (tree) do
    +  print (node_type, path, node)
    +end
    +--> "branch"   {}      {{"leaf1", "leaf2"}, "leaf3"}
    +--> "branch"   {1}     {"leaf1", "leaf"2")
    +--> "leaf"     {1,1}   "leaf1"
    +--> "leaf"     {1,2}   "leaf2"
    +--> "join"     {1}     {"leaf1", "leaf2"}
    +--> "leaf"     {2}     "leaf3"
    +--> "join"     {}      {{"leaf1", "leaf2"}, "leaf3"}
    +os.exit (0)
    +
+ +
+
+

Metamethods

+ +
+
+ + std.tree.__index (tr, i) +
+
+ Deep retrieval. + + +

Parameters:

+
    +
  • tr + Tree + a tree +
  • +
  • i + non-table, or list of keys {i1, ...i_n} +
  • +
+ +

Returns:

+
    + + tr[i1]...[i_n] if i is a key list, tr[i] otherwise +
+ + + +

Usage:

+
    +
    del_other_window = keymap[{"C-x", "4", KEY_DELETE}]
    +
+ +
+
+ + std.tree.__newindex (tr, i[, v]) +
+
+ Deep insertion. + + +

Parameters:

+
    +
  • tr + Tree + a tree +
  • +
  • i + non-table, or list of keys {i1, ...i_n} +
  • +
  • v + value + (optional) +
  • +
+ + + + +

Usage:

+
    +
    function bindkey (keylist, fn) keymap[keylist] = fn end
    +
+ +
+
+ + +
+
+
+generated by LDoc 1.4.6 +Last updated 2018-09-16 19:20:25 +
+
+ + diff --git a/doc/index.html b/doc/index.html new file mode 100644 index 0000000..27f1588 --- /dev/null +++ b/doc/index.html @@ -0,0 +1,158 @@ + + + + + stdlib 41.2.2 Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ + +

+

Standard Lua Libraries

+ +

This is a collection of Lua libraries for Lua 5.1 (including LuaJIT), +5.2, 5.3 and 5.4. Stdlib has no prerequisites beyond a standard Lua +system.

+ +

LICENSE

+ +

The libraries are copyright by their authors 2000-2018, and released under +the MIT license (the same license as Lua itself). There is no warranty.

+ + + +

Modules

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
stdLua Standard Libraries.
std.debugAdditions to the core debug module.
std.functionalFunctional programming.
std.ioAdditions to the core io module.
std.mathAdditions to the core math module.
std.operatorFunctional forms of Lua operators.
std.packageAdditions to the core package module.
std.strictChecks uses of undeclared global variables.
std.stringAdditions to the core string module.
std.tableExtensions to the core table module.
+

Classes

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
std.treeTree container prototype.
std.containerContainer prototype.
std.objectPrototype-based objects.
std.listTables as lists.
std.optparseParse and process command line options.
std.setSet container prototype.
std.strbufString buffers.
+ +
+
+
+generated by LDoc 1.4.6 +Last updated 2018-09-16 19:20:25 +
+
+ + diff --git a/doc/ldoc.css b/doc/ldoc.css new file mode 100644 index 0000000..52c4ad2 --- /dev/null +++ b/doc/ldoc.css @@ -0,0 +1,303 @@ +/* BEGIN RESET + +Copyright (c) 2010, Yahoo! Inc. All rights reserved. +Code licensed under the BSD License: +http://developer.yahoo.com/yui/license.html +version: 2.8.2r1 +*/ +html { + color: #000; + background: #FFF; +} +body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td { + margin: 0; + padding: 0; +} +table { + border-collapse: collapse; + border-spacing: 0; +} +fieldset,img { + border: 0; +} +address,caption,cite,code,dfn,em,strong,th,var,optgroup { + font-style: inherit; + font-weight: inherit; +} +del,ins { + text-decoration: none; +} +li { + margin-left: 20px; +} +caption,th { + text-align: left; +} +h1,h2,h3,h4,h5,h6 { + font-size: 100%; + font-weight: bold; +} +q:before,q:after { + content: ''; +} +abbr,acronym { + border: 0; + font-variant: normal; +} +sup { + vertical-align: baseline; +} +sub { + vertical-align: baseline; +} +legend { + color: #000; +} +input,button,textarea,select,optgroup,option { + font-family: inherit; + font-size: inherit; + font-style: inherit; + font-weight: inherit; +} +input,button,textarea,select {*font-size:100%; +} +/* END RESET */ + +body { + margin-left: 1em; + margin-right: 1em; + font-family: arial, helvetica, geneva, sans-serif; + background-color: #ffffff; margin: 0px; +} + +code, tt { font-family: monospace; font-size: 1.1em; } +span.parameter { font-family:monospace; } +span.parameter:after { content:":"; } +span.types:before { content:"("; } +span.types:after { content:")"; } +.type { font-weight: bold; font-style:italic } + +body, p, td, th { font-size: .95em; line-height: 1.2em;} + +p, ul { margin: 10px 0 0 0px;} + +strong { font-weight: bold;} + +em { font-style: italic;} + +h1 { + font-size: 1.5em; + margin: 20px 0 20px 0; +} +h2, h3, h4 { margin: 15px 0 10px 0; } +h2 { font-size: 1.25em; } +h3 { font-size: 1.15em; } +h4 { font-size: 1.06em; } + +a:link { font-weight: bold; color: #004080; text-decoration: none; } +a:visited { font-weight: bold; color: #006699; text-decoration: none; } +a:link:hover { text-decoration: underline; } + +hr { + color:#cccccc; + background: #00007f; + height: 1px; +} + +blockquote { margin-left: 3em; } + +ul { list-style-type: disc; } + +p.name { + font-family: "Andale Mono", monospace; + padding-top: 1em; +} + +pre { + background-color: rgb(245, 245, 245); + border: 1px solid #C0C0C0; /* silver */ + padding: 10px; + margin: 10px 0 10px 0; + overflow: auto; + font-family: "Andale Mono", monospace; +} + +pre.example { + font-size: .85em; +} + +table.index { border: 1px #00007f; } +table.index td { text-align: left; vertical-align: top; } + +#container { + margin-left: 1em; + margin-right: 1em; + background-color: #f0f0f0; +} + +#product { + text-align: center; + border-bottom: 1px solid #cccccc; + background-color: #ffffff; +} + +#product big { + font-size: 2em; +} + +#main { + background-color: #f0f0f0; + border-left: 2px solid #cccccc; +} + +#navigation { + float: left; + width: 14em; + vertical-align: top; + background-color: #f0f0f0; + overflow: visible; +} + +#navigation h2 { + background-color:#e7e7e7; + font-size:1.1em; + color:#000000; + text-align: left; + padding:0.2em; + border-top:1px solid #dddddd; + border-bottom:1px solid #dddddd; +} + +#navigation ul +{ + font-size:1em; + list-style-type: none; + margin: 1px 1px 10px 1px; +} + +#navigation li { + text-indent: -1em; + display: block; + margin: 3px 0px 0px 22px; +} + +#navigation li li a { + margin: 0px 3px 0px -1em; +} + +#content { + margin-left: 14em; + padding: 1em; + width: 700px; + border-left: 2px solid #cccccc; + border-right: 2px solid #cccccc; + background-color: #ffffff; +} + +#about { + clear: both; + padding: 5px; + border-top: 2px solid #cccccc; + background-color: #ffffff; +} + +@media print { + body { + font: 12pt "Times New Roman", "TimeNR", Times, serif; + } + a { font-weight: bold; color: #004080; text-decoration: underline; } + + #main { + background-color: #ffffff; + border-left: 0px; + } + + #container { + margin-left: 2%; + margin-right: 2%; + background-color: #ffffff; + } + + #content { + padding: 1em; + background-color: #ffffff; + } + + #navigation { + display: none; + } + pre.example { + font-family: "Andale Mono", monospace; + font-size: 10pt; + page-break-inside: avoid; + } +} + +table.module_list { + border-width: 1px; + border-style: solid; + border-color: #cccccc; + border-collapse: collapse; +} +table.module_list td { + border-width: 1px; + padding: 3px; + border-style: solid; + border-color: #cccccc; +} +table.module_list td.name { background-color: #f0f0f0; min-width: 200px; } +table.module_list td.summary { width: 100%; } + + +table.function_list { + border-width: 1px; + border-style: solid; + border-color: #cccccc; + border-collapse: collapse; +} +table.function_list td { + border-width: 1px; + padding: 3px; + border-style: solid; + border-color: #cccccc; +} +table.function_list td.name { background-color: #f0f0f0; min-width: 200px; } +table.function_list td.summary { width: 100%; } + +ul.nowrap { + overflow:auto; + white-space:nowrap; +} + +dl.table dt, dl.function dt {border-top: 1px solid #ccc; padding-top: 1em;} +dl.table dd, dl.function dd {padding-bottom: 1em; margin: 10px 0 0 20px;} +dl.table h3, dl.function h3 {font-size: .95em;} + +/* stop sublists from having initial vertical space */ +ul ul { margin-top: 0px; } +ol ul { margin-top: 0px; } +ol ol { margin-top: 0px; } +ul ol { margin-top: 0px; } + +/* make the target distinct; helps when we're navigating to a function */ +a:target + * { + background-color: #FF9; +} + + +/* styles for prettification of source */ +pre .comment { color: #558817; } +pre .constant { color: #a8660d; } +pre .escape { color: #844631; } +pre .keyword { color: #aa5050; font-weight: bold; } +pre .library { color: #0e7c6b; } +pre .marker { color: #512b1e; background: #fedc56; font-weight: bold; } +pre .string { color: #8080ff; } +pre .number { color: #f8660d; } +pre .operator { color: #2239a8; font-weight: bold; } +pre .preprocessor, pre .prepro { color: #a33243; } +pre .global { color: #800080; } +pre .user-keyword { color: #800080; } +pre .prompt { color: #558817; } +pre .url { color: #272fc2; text-decoration: underline; } + diff --git a/doc/modules/std.debug.html b/doc/modules/std.debug.html new file mode 100644 index 0000000..a41a045 --- /dev/null +++ b/doc/modules/std.debug.html @@ -0,0 +1,848 @@ + + + + + stdlib 41.2.2 Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Module std.debug

+

Additions to the core debug module.

+

The module table returned by std.debug also contains all of the entries + from the core debug table. An hygienic way to import this module, then, is + simply to override the core debug locally:

+ + +
+local debug = require "std.debug"
+
+ +

The behaviour of the functions in this module are controlled by the value + of the global _DEBUG. Not setting _DEBUG prior to requiring any of + stdlib's modules is equivalent to having _DEBUG = true.

+ +

The first line of Lua code in production quality projects that use stdlib + should be either:

+ + +
+_DEBUG = false
+
+ +

or alternatively, if you need to be careful not to damage the global + environment:

+ + +
+local init = require "std.debug_init"
+init._DEBUG = false
+
+ +

This mitigates almost all of the overhead of argument typechecking in + stdlib API functions.

+ + +

Functions

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DEPRECATED (version, name[, extramsg], fn)Provide a deprecated function definition according to _DEBUG.deprecate.
DEPRECATIONMSG (version, name[, extramsg], level)Format a deprecation warning message.
argcheck (name, i, expected, actual[, level=2])Check the type of an argument against expected types.
argerror (name, i[, extramsg[, level=1]])Raise a bad argument error.
argscheck (decl, inner)Wrap a function definition with argument type and arity checking.
debug ()Equivalent to calling debug.say (1, ...)
extramsg_mismatch (expected, actual[, index])Format a type mismatch error.
parsetypes (types)Compact permutation list into a list of valid types at each argument.
say ([n=1], ...)Print a debugging message to io.stderr.
trace (event)Trace function calls.
+

Tables

+ + + + + +
_DEBUGControl std.debug function behaviour.
+

Fields

+ + + + + + + + + + + + + + + + + + + + + +
extramsg_toomanyFormat a too many things error.
getfenvExtend debug.getfenv to unwrap functables correctly.
resulterrorRaise a bad result error.
setfenvExtend debug.setfenv to unwrap functables correctly.
typesplitSplit a typespec string into a table of normalized type names.
+ +
+
+ + +

Functions

+ Methods +
+
+ + DEPRECATED (version, name[, extramsg], fn) +
+
+ Provide a deprecated function definition according to _DEBUG.deprecate. + You can check whether your covered code uses deprecated functions by + setting _DEBUG.deprecate to true before loading any stdlib modules, + or silence deprecation warnings by setting _DEBUG.deprecate = false. + + +

Parameters:

+
    +
  • version + string + first deprecation release version +
  • +
  • name + string + function name for automatic warning message +
  • +
  • extramsg + string + additional warning text + (optional) +
  • +
  • fn + func + deprecated function +
  • +
+ +

Returns:

+
    + + a function to show the warning on first call, and hand off to fn +
+ + + +

Usage:

+
    +
    M.op = DEPRECATED ("41", "'std.functional.op'", std.operator)
    +
+ +
+
+ + DEPRECATIONMSG (version, name[, extramsg], level) +
+
+ Format a deprecation warning message. + + +

Parameters:

+
    +
  • version + string + first deprecation release version +
  • +
  • name + string + function name for automatic warning message +
  • +
  • extramsg + string + additional warning text + (optional) +
  • +
  • level + int + call stack level to blame for the error +
  • +
+ +

Returns:

+
    + + string + deprecation warning message, or empty string +
+ + + +

Usage:

+
    +
    io.stderr:write (DEPRECATIONMSG ("42", "multi-argument 'module.fname'", 2))
    +
+ +
+
+ + argcheck (name, i, expected, actual[, level=2]) +
+
+ Check the type of an argument against expected types. + Equivalent to luaL_argcheck in the Lua C API.

+ +

Call argerror if there is a type mismatch.

+ +

Argument actual must match one of the types from in expected, each + of which can be the name of a primitive Lua type, a stdlib object type, + or one of the special options below:

+ +
#table    accept any non-empty table
+any       accept any non-nil argument type
+file      accept an open file object
+function  accept a function, or object with a __call metamethod
+int       accept an integer valued number
+list      accept a table where all keys are a contiguous 1-based integer range
+#list     accept any non-empty list
+object    accept any std.Object derived type
+:foo      accept only the exact string ":foo", works for any :-prefixed string
+
+ +

The :foo format allows for type-checking of self-documenting + boolean-like constant string parameters predicated on nil versus + :option instead of false versus true. Or you could support + both:

+ +
argcheck ("table.copy", 2, "boolean|:nometa|nil", nometa)
+
+ +

A very common pattern is to have a list of possible types including + "nil" when the argument is optional. Rather than writing long-hand + as above, prepend a question mark to the list of types and omit the + explicit "nil" entry:

+ +
argcheck ("table.copy", 2, "?boolean|:nometa", predicate)
+
+ +

Normally, you should not need to use the level parameter, as the + default is to blame the caller of the function using argcheck in + error messages; which is almost certainly what you want. + + +

Parameters:

+
    +
  • name + string + function to blame in error message +
  • +
  • i + int + argument number to blame in error message +
  • +
  • expected + string + specification for acceptable argument types +
  • +
  • actual + argument passed +
  • +
  • level + int + call stack level to blame for the error + (default 2) +
  • +
+ + + + +

Usage:

+
    +
    local function case (with, branches)
    +  argcheck ("std.functional.case", 2, "#table", branches)
    +  ...
    +
+ +
+
+ + argerror (name, i[, extramsg[, level=1]]) +
+
+ Raise a bad argument error. + Equivalent to luaL_argerror in the Lua C API. This function does not + return. The level argument behaves just like the core error + function. + + +

Parameters:

+
    +
  • name + string + function to callout in error message +
  • +
  • i + int + argument number +
  • +
  • extramsg + string + additional text to append to message inside parentheses + (optional) +
  • +
  • level + int + call stack level to blame for the error + (default 1) +
  • +
+ + + +

See also:

+ + +

Usage:

+
    +
    local function slurp (file)
    +  local h, err = input_handle (file)
    +  if h == nil then argerror ("std.io.slurp", 1, err, 2) end
    +  ...
    +
+ +
+
+ + argscheck (decl, inner) +
+
+ +

Wrap a function definition with argument type and arity checking. + In addition to checking that each argument type matches the corresponding + element in the types table with argcheck, if the final element of + types ends with an ellipsis, remaining unchecked arguments are checked + against that type:

+ +
 format = argscheck ("string.format (string, ?any...)", string.format)
+
+ +

A colon in the function name indicates that the argument type list does + not have a type for self:

+ +
 format = argscheck ("string:format (?any...)", string.format)
+
+ +

If an argument can be omitted entirely, then put its type specification + in square brackets:

+ +
 insert = argscheck ("table.insert (table, [int], ?any)", table.insert)
+
+ +

Similarly return types can be checked with the same list syntax as + arguments:

+ +
 len = argscheck ("string.len (string) => int", string.len)
+
+ +

Additionally, variant return type lists can be listed like this:

+ +
 open = argscheck ("io.open (string, ?string) => file or nil, string",
+                   io.open)
+
+ + + + +

Parameters:

+
    +
  • decl + string + function type declaration string +
  • +
  • inner + func + function to wrap with argument checking +
  • +
+ + + + +

Usage:

+
    +
    local case = argscheck ("std.functional.case (?any, #table) => [any...]",
    +  function (with, branches)
    +    ...
    +end)
    +
+ +
+
+ + debug () +
+
+ Equivalent to calling debug.say (1, ...) + + + + + +

See also:

+ + +

Usage:

+
    +
    local debug = require "std.debug"
    +debug "oh noes!"
    +
+ +
+
+ + extramsg_mismatch (expected, actual[, index]) +
+
+ Format a type mismatch error. + + +

Parameters:

+
    +
  • expected + string + a pipe delimited list of matchable types +
  • +
  • actual + the actual argument to match with +
  • +
  • index + number + erroring container element index + (optional) +
  • +
+ +

Returns:

+
    + + string + formatted extramsg for this mismatch for argerror +
+ + +

See also:

+ + +

Usage:

+
    +
    if fmt ~= nil and type (fmt) ~= "string" then
    +  argerror ("format", 1, extramsg_mismatch ("?string", fmt))
    +end
    +
+ +
+
+ + parsetypes (types) +
+
+ Compact permutation list into a list of valid types at each argument. + Eliminate bracketed types by combining all valid types at each position + for all permutations of typelist. + + +

Parameters:

+
    +
  • types + list + a normalized list of type names +
  • +
+ +

Returns:

+
    + + list + valid types for each positional parameter +
+ + + + +
+
+ + say ([n=1], ...) +
+
+ Print a debugging message to io.stderr. + Display arguments passed through std.tostring and separated by tab + characters when _DEBUG is true and n is 1 or less; or _DEBUG.level + is a number greater than or equal to n. If _DEBUG is false or + nil, nothing is written. + + +

Parameters:

+
    +
  • n + int + debugging level, smaller is higher priority + (default 1) +
  • +
  • ... + objects to print (as for print) +
  • +
+ + + + +

Usage:

+
    +
    local _DEBUG = require "std.debug_init"._DEBUG
    +_DEBUG.level = 3
    +say (2, "_DEBUG table contents:", _DEBUG)
    +
+ +
+
+ + trace (event) +
+
+ Trace function calls. + Use as debug.sethook (trace, "cr"), which is done automatically + when _DEBUG.call is set. + Based on test/trace-calls.lua from the Lua distribution. + + +

Parameters:

+
    +
  • event + string + event causing the call +
  • +
+ + + + +

Usage:

+
    +
    _DEBUG = { call = true }
    +local debug = require "std.debug"
    +
+ +
+
+

Tables

+ +
+
+ + _DEBUG +
+
+ Control std.debug function behaviour. + To declare debugging state, set _DEBUG either to false to disable all + runtime debugging; to any "truthy" value (equivalent to enabling everything + except call, or as documented below. + + +

Fields:

+
    +
  • argcheck + boolean + honor argcheck and argscheck calls + (default true) +
  • +
  • call + boolean + do call trace debugging + (default false) +
  • +
  • deprecate + if false, deprecated APIs are defined, + and do not issue deprecation warnings when used; if nil issue a + deprecation warning each time a deprecated api is used; any other + value causes deprecated APIs not to be defined at all + (default nil) +
  • +
  • level + int + debugging level + (default 1) +
  • +
+ + + + +

Usage:

+
    +
    _DEBUG = { argcheck = false, level = 9 }
    +
+ +
+
+

Fields

+ +
+
+ + extramsg_toomany +
+
+ Format a too many things error. + + +
    +
  • bad + string + the thing there are too many of +
  • +
  • expected + int + maximum number of bad things expected +
  • +
  • actual + int + actual number of bad things that triggered the error +
  • +
+ + + +

See also:

+ + +

Usage:

+
    +
    if maxn (argt) > 7 then
    +  argerror ("sevenses", 8, extramsg_toomany ("argument", 7, maxn (argt)))
    +end
    +
+ +
+
+ + getfenv +
+
+ Extend debug.getfenv to unwrap functables correctly. + + +
    +
  • fn + int, function or functable + target function, or stack level +
  • +
+ + + + + +
+
+ + resulterror +
+
+ Raise a bad result error. + Like argerror for bad results. This function does not + return. The level argument behaves just like the core error + function. + + +
    +
  • name + string + function to callout in error message +
  • +
  • i + int + argument number +
  • +
  • extramsg + string + additional text to append to message inside parentheses + (optional) +
  • +
  • level + int + call stack level to blame for the error + (default 1) +
  • +
+ + + + +

Usage:

+
    +
    local function slurp (file)
    +  local h, err = input_handle (file)
    +  if h == nil then argerror ("std.io.slurp", 1, err, 2) end
    +  ...
    +
+ +
+
+ + setfenv +
+
+ Extend debug.setfenv to unwrap functables correctly. + + +
    +
  • fn + function or functable + target function +
  • +
  • env + table + new function environment +
  • +
+ + + + + +
+
+ + typesplit +
+
+ Split a typespec string into a table of normalized type names. + + +
    +
  • either + string or table + "?bool|:nometa" or {"boolean", ":nometa"} +
  • +
+ + + + + +
+
+ + +
+
+
+generated by LDoc 1.4.6 +Last updated 2018-09-16 19:20:25 +
+
+ + diff --git a/doc/modules/std.functional.html b/doc/modules/std.functional.html new file mode 100644 index 0000000..bb0c0c0 --- /dev/null +++ b/doc/modules/std.functional.html @@ -0,0 +1,1015 @@ + + + + + stdlib 41.2.2 Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Module std.functional

+

Functional programming.

+

A selection of higher-order functions to enable a functional style of + programming in Lua.

+ + +

Functions

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
bind (fn, argt)Partially apply a function.
callable (x)Identify callable types.
case (with, branches)A rudimentary case statement.
collect ([ifn=std.npairs], ...)Collect the results of an iterator.
compose (...)Compose functions.
cond (expr, branch, ...)A rudimentary condition-case statement.
curry (fn, n)Curry a function.
filter (pfn[, ifn=std.pairs], ...)Filter an iterator with a predicate.
foldl (fn[, d=t[1], t)Fold a binary function left associatively.
foldr (fn[, d=t[1], t)Fold a binary function right associatively.
id (...)Identity function.
lambda (s)Compile a lambda string into a Lua function.
map (fn[, ifn=std.pairs], ...)Map a function over an iterator.
map_with (fn, tt)Map a function over a table of argument lists.
memoize (fn[, normfn=std.tostring])Memoize a function, by wrapping it in a functable.
nop ()No operation.
reduce (fn, d[, ifn=std.pairs], ...)Fold a binary function into an iterator.
zip (tt)Zip a table of tables.
zip_with (fn, tt)Zip a list of tables together with a function.
+

Types

+ + + + + + + + + +
normalize (...)Signature of a memoize argument normalization callback function.
predicate (...)Signature of a filter predicate callback function.
+ +
+
+ + +

Functions

+ Methods +
+
+ + bind (fn, argt) +
+
+ Partially apply a function. + + +

Parameters:

+
    +
  • fn + func + function to apply partially +
  • +
  • argt + table + table of fn arguments to bind +
  • +
+ +

Returns:

+
    + + function with argt arguments already bound +
+ + + +

Usage:

+
    +
    cube = bind (std.operator.pow, {[2] = 3})
    +
+ +
+
+ + callable (x) +
+
+ Identify callable types. + + +

Parameters:

+
    +
  • x + an object or primitive +
  • +
+ +

Returns:

+
    + + true if x can be called, otherwise false +
+ + + +

Usage:

+
    +
    if callable (functable) then functable (args) end
    +
+ +
+
+ + case (with, branches) +
+
+ A rudimentary case statement. + Match with against keys in branches table. + + +

Parameters:

+
    +
  • with + expression to match +
  • +
  • branches + table + map possible matches to functions +
  • +
+ +

Returns:

+
    + + the value associated with a matching key, or the first non-key + value if no key matches. Function or functable valued matches are + called using with as the sole argument, and the result of that call + returned; otherwise the matching value associated with the matching + key is returned directly; or else nil if there is no match and no + default. +
+ + +

See also:

+ + +

Usage:

+
    +
    return case (type (object), {
    +  table  = "table",
    +  string = function ()  return "string" end,
    +           function (s) error ("unhandled type: " .. s) end,
    +})
    +
+ +
+
+ + collect ([ifn=std.npairs], ...) +
+
+ Collect the results of an iterator. + + +

Parameters:

+
    +
  • ifn + func + iterator function + (default std.npairs) +
  • +
  • ... + ifn arguments +
  • +
+ +

Returns:

+
    + + table + of results from running ifn on args +
+ + +

See also:

+ + +

Usage:

+
    +
    --> {"a", "b", "c"}
    +collect {"a", "b", "c", x=1, y=2, z=5}
    +
+ +
+
+ + compose (...) +
+
+ Compose functions. + + +

Parameters:

+
    +
  • ... + func + functions to compose +
  • +
+ +

Returns:

+
    + + function + composition of fnN .. fn1: note that this is the + reverse of what you might expect, but means that code like:

    + +
     functional.compose (function (x) return f (x) end,
    +                     function (x) return g (x) end))
    +
    + +

    can be read from top to bottom. +

+ + + +

Usage:

+
    +
    vpairs = compose (table.invert, ipairs)
    +for v, i in vpairs {"a", "b", "c"} do process (v, i) end
    +
+ +
+
+ + cond (expr, branch, ...) +
+
+ A rudimentary condition-case statement. + If expr is "truthy" return branch if given, otherwise expr + itself. If the return value is a function or functable, then call it + with expr as the sole argument and return the result; otherwise + return it explicitly. If expr is "falsey", then recurse with the + first two arguments stripped. + + +

Parameters:

+
    +
  • expr + a Lua expression +
  • +
  • branch + a function, functable or value to use if expr is + "truthy" +
  • +
  • ... + additional arguments to retry if expr is "falsey" +
  • +
+ + + +

See also:

+ + +

Usage:

+
    +
    -- recursively calculate the nth triangular number
    +function triangle (n)
    +  return cond (
    +    n <= 0, 0,
    +    n == 1, 1,
    +            function () return n + triangle (n - 1) end)
    +end
    +
+ +
+
+ + curry (fn, n) +
+
+ Curry a function. + + +

Parameters:

+
    +
  • fn + func + function to curry +
  • +
  • n + int + number of arguments +
  • +
+ +

Returns:

+
    + + function + curried version of fn +
+ + + +

Usage:

+
    +
    add = curry (function (x, y) return x + y end, 2)
    +incr, decr = add (1), add (-1)
    +
+ +
+
+ + filter (pfn[, ifn=std.pairs], ...) +
+
+ Filter an iterator with a predicate. + + +

Parameters:

+
    +
  • pfn + predicate + predicate function +
  • +
  • ifn + func + iterator function + (default std.pairs) +
  • +
  • ... + iterator arguments +
  • +
+ +

Returns:

+
    + + table + elements e for which pfn (e) is not "falsey". +
+ + +

See also:

+ + +

Usage:

+
    +
    --> {2, 4}
    +filter (lambda '|e|e%2==0', std.elems, {1, 2, 3, 4})
    +
+ +
+
+ + foldl (fn[, d=t[1], t) +
+
+ Fold a binary function left associatively. + If parameter d is omitted, the first element of t is used, + and t treated as if it had been passed without that element. + + +

Parameters:

+
    +
  • fn + func + binary function +
  • +
  • d + initial left-most argument + (default t[1) +
  • +
  • t + table + a table +
  • +
+ +

Returns:

+
    + + result +
+ + +

See also:

+ + +

Usage:

+
    +
    foldl (std.operator.quot, {10000, 100, 10}) == (10000 / 100) / 10
    +
+ +
+
+ + foldr (fn[, d=t[1], t) +
+
+ Fold a binary function right associatively. + If parameter d is omitted, the last element of t is used, + and t treated as if it had been passed without that element. + + +

Parameters:

+
    +
  • fn + func + binary function +
  • +
  • d + initial right-most argument + (default t[1) +
  • +
  • t + table + a table +
  • +
+ +

Returns:

+
    + + result +
+ + +

See also:

+ + +

Usage:

+
    +
    foldr (std.operator.quot, {10000, 100, 10}) == 10000 / (100 / 10)
    +
+ +
+
+ + id (...) +
+
+ Identity function. + + +

Parameters:

+
    +
  • ... + arguments +
  • +
+ +

Returns:

+
    + + arguments +
+ + + + +
+
+ + lambda (s) +
+
+ Compile a lambda string into a Lua function.

+ +

A valid lambda string takes one of the following forms:

+ +
    +
  1. '=expression': equivalent to function (...) return expression end
  2. +
  3. '|args|expression': equivalent to function (args) return expression end
  4. +
+ +

The first form (starting with '=') automatically assigns the first + nine arguments to parameters '_1' through '_9' for use within the + expression body. The parameter '_1' is aliased to '_', and if the + first non-whitespace of the whole expression is '_', then the + leading '=' can be omitted.

+ +

The results are memoized, so recompiling a previously compiled + lambda string is extremely fast. + + +

Parameters:

+
    +
  • s + string + a lambda string +
  • +
+ +

Returns:

+
    + + functable + compiled lambda string, can be called like a function +
+ + + +

Usage:

+
    +
    -- The following are equivalent:
    +lambda '= _1 < _2'
    +lambda '|a,b| a<b'
    +
+ +
+
+ + map (fn[, ifn=std.pairs], ...) +
+
+ Map a function over an iterator. + + +

Parameters:

+
    +
  • fn + func + map function +
  • +
  • ifn + func + iterator function + (default std.pairs) +
  • +
  • ... + iterator arguments +
  • +
+ +

Returns:

+
    + + table + results +
+ + +

See also:

+ + +

Usage:

+
    +
    --> {1, 4, 9, 16}
    +map (lambda '=_1*_1', std.ielems, {1, 2, 3, 4})
    +
+ +
+
+ + map_with (fn, tt) +
+
+ Map a function over a table of argument lists. + + +

Parameters:

+
    +
  • fn + func + map function +
  • +
  • tt + table + a table of fn argument lists +
  • +
+ +

Returns:

+
    + + table + new table of fn results +
+ + +

See also:

+ + +

Usage:

+
    +
    --> {"123", "45"}, {a="123", b="45"}
    +conc = bind (map_with, {lambda '|...|table.concat {...}'})
    +conc {{1, 2, 3}, {4, 5}}, conc {a={1, 2, 3, x="y"}, b={4, 5, z=6}}
    +
+ +
+
+ + memoize (fn[, normfn=std.tostring]) +
+
+ Memoize a function, by wrapping it in a functable.

+ +

To ensure that memoize always returns the same results for the same + arguments, it passes arguments to fn. You can specify a more + sophisticated function if memoize should handle complicated argument + equivalencies. + + +

Parameters:

+
    +
  • fn + func + pure function: a function with no side effects +
  • +
  • normfn + normalize + function to normalize arguments + (default std.tostring) +
  • +
+ +

Returns:

+
    + + functable + memoized function +
+ + + +

Usage:

+
    +
    local fast = memoize (function (...) --[[ slow code ]] end)
    +
+ +
+
+ + nop () +
+
+ No operation. + This function ignores all arguments, and returns no values. + + + + + +

See also:

+ + +

Usage:

+
    +
    if unsupported then vtable["memrmem"] = nop end
    +
+ +
+
+ + reduce (fn, d[, ifn=std.pairs], ...) +
+
+ Fold a binary function into an iterator. + + +

Parameters:

+
    +
  • fn + func + reduce function +
  • +
  • d + initial first argument +
  • +
  • ifn + func + iterator function + (default std.pairs) +
  • +
  • ... + iterator arguments +
  • +
+ +

Returns:

+
    + + result +
+ + +

See also:

+ + +

Usage:

+
    +
    --> 2 ^ 3 ^ 4 ==> 4096
    +reduce (std.operator.pow, 2, std.ielems, {3, 4})
    +
+ +
+
+ + zip (tt) +
+
+ Zip a table of tables. + Make a new table, with lists of elements at the same index in the + original table. This function is effectively its own inverse. + + +

Parameters:

+
    +
  • tt + table + a table of tables +
  • +
+ +

Returns:

+
    + + table + new table with lists of elements of the same key + from tt +
+ + +

See also:

+ + +

Usage:

+
    +
    --> {{1, 3, 5}, {2, 4}}, {a={x=1, y=3, z=5}, b={x=2, y=4}}
    +zip {{1, 2}, {3, 4}, {5}}, zip {x={a=1, b=2}, y={a=3, b=4}, z={a=5}}
    +
+ +
+
+ + zip_with (fn, tt) +
+
+ Zip a list of tables together with a function. + + +

Parameters:

+
    +
  • fn + function + function +
  • +
  • tt + table + table of tables +
  • +
+ +

Returns:

+
    + + table + a new table of results from calls to fn with arguments + made from all elements the same key in the original tables; effectively + the "columns" in a simple list + of lists. +
+ + +

See also:

+ + +

Usage:

+
    +
    --> {"135", "24"}, {a="1", b="25"}
    +conc = bind (zip_with, {lambda '|...|table.concat {...}'})
    +conc {{1, 2}, {3, 4}, {5}}, conc {{a=1, b=2}, x={a=3, b=4}, {b=5}}
    +
+ +
+
+

Types

+ +
+
+ + normalize (...) +
+
+ Signature of a memoize argument normalization callback function. + + +

Parameters:

+
    +
  • ... + arguments +
  • +
+ +

Returns:

+
    + + string + normalized arguments +
+ + + +

Usage:

+
    +
    local normalize = function (name, value, props) return name end
    +local intern = std.functional.memoize (mksymbol, normalize)
    +
+ +
+
+ + predicate (...) +
+
+ Signature of a filter predicate callback function. + + +

Parameters:

+
    +
  • ... + arguments +
  • +
+ +

Returns:

+
    + + boolean + "truthy" if the predicate condition succeeds, + "falsey" otherwise +
+ + + +

Usage:

+
    +
    local predicate = lambda '|k,v|type(v)=="string"'
    +local strvalues = filter (predicate, std.pairs, {name="Roberto", id=12345})
    +
+ +
+
+ + +
+
+
+generated by LDoc 1.4.6 +Last updated 2018-09-16 19:20:25 +
+
+ + diff --git a/doc/modules/std.html b/doc/modules/std.html new file mode 100644 index 0000000..4242564 --- /dev/null +++ b/doc/modules/std.html @@ -0,0 +1,843 @@ + + + + + stdlib 41.2.2 Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Module std

+

Lua Standard Libraries.

+

This module contains a selection of improved Lua core functions, among + others.

+ +

Also, after requiring this module, simply referencing symbols in the + submodule hierarchy will load the necessary modules on demand.

+ +

By default there are no changes to any global symbols, or monkey + patching of core module tables and metatables. However, sometimes it's + still convenient to do that: For example, when using stdlib from the + REPL, or in a prototype where you want to throw caution to the wind and + compatibility with other modules be damned. In that case, you can give + stdlib permission to scribble all over your namespaces by using the + various monkey_patch calls in the library.

+ + +

Functions

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
assert (expect[, f=""[, ...]])Enhance core assert to also allow formatted arguments.
barrel ([namespace=_G])A barrel of monkey_patches.
elems (t)An iterator over all elements of a sequence.
eval (s)Evaluate a string as Lua code.
getmetamethod (x, n)Return named metamethod, if any, otherwise nil.
ielems (t)An iterator over the integer keyed elements of a sequence.
ipairs (t)An iterator over elements of a sequence, until the first nil value.
ireverse (t)Return a new table with element order reversed.
monkey_patch ([namespace=_G])Overwrite core methods and metamethods with std enhanced versions.
npairs (t)Ordered iterator for integer keyed values.
pairs (t)Enhance core pairs to respect __pairs even in Lua 5.1.
require (module[, min[, too_big[, pattern]]])Enhance core require to assert version number compatibility.
ripairs (t)An iterator like ipairs, but in reverse.
rnpairs (t)An iterator like npairs, but in reverse.
tostring (x)Enhance core tostring to render table contents as a string.
+

Tables

+ + + + + +
stdModule table.
+

Metamethods

+ + + + + +
__index (name)Lazy loading of stdlib modules.
+ +
+
+ + +

Functions

+ Methods +
+
+ + assert (expect[, f=""[, ...]]) +
+
+ Enhance core assert to also allow formatted arguments. + + +

Parameters:

+
    +
  • expect + expression, expected to be truthy +
  • +
  • f + string + format string + (default "") +
  • +
  • ... + arguments to format + (optional) +
  • +
+ +

Returns:

+
    + + value of expect, if truthy +
+ + + +

Usage:

+
    +
    std.assert (expected ~= nil, "100% unexpected!")
    +std.assert (expected ~= nil, "%s unexpected!", expected)
    +
+ +
+
+ + barrel ([namespace=_G]) +
+
+ A barrel of monkey_patches.

+ +

Apply all monkey_patch functions. Additionally, for backwards + compatibility only, write a selection of sub-module functions into + the given namespace. + + +

Parameters:

+
    +
  • namespace + table + where to install global functions + (default _G) +
  • +
+ +

Returns:

+
    + + table + module table +
+ + + +

Usage:

+
    +
    local std = require "std".barrel ()
    +
+ +
+
+ + elems (t) +
+
+ An iterator over all elements of a sequence. + If t has a __pairs metamethod, use that to iterate. + + +

Parameters:

+
    +
  • t + table + a table +
  • +
+ +

Returns:

+
    +
  1. + function + iterator function
  2. +
  3. + table + t, the table being iterated over
  4. +
  5. + key, the previous iteration key
  6. +
+ + +

See also:

+ + +

Usage:

+
    +
    for value in std.elems {a = 1, b = 2, c = 5} do process (value) end
    +
+ +
+
+ + eval (s) +
+
+ Evaluate a string as Lua code. + + +

Parameters:

+
    +
  • s + string + string of Lua code +
  • +
+ +

Returns:

+
    + + result of evaluating s +
+ + + +

Usage:

+
    +
    std.eval "math.min (2, 10)"
    +
+ +
+
+ + getmetamethod (x, n) +
+
+ Return named metamethod, if any, otherwise nil. + + +

Parameters:

+
    +
  • x + item to act on +
  • +
  • n + string + name of metamethod to lookup +
  • +
+ +

Returns:

+
    + + function or nil + metamethod function, or nil if no metamethod +
+ + + +

Usage:

+
    +
    lookup = std.getmetamethod (require "std.object", "__index")
    +
+ +
+
+ + ielems (t) +
+
+ An iterator over the integer keyed elements of a sequence. + If t has a __len metamethod, iterate up to the index it returns. + + +

Parameters:

+
    +
  • t + table + a table +
  • +
+ +

Returns:

+
    +
  1. + function + iterator function
  2. +
  3. + table + t, the table being iterated over
  4. +
  5. + int + index, the previous iteration index
  6. +
+ + +

See also:

+ + +

Usage:

+
    +
    for v in std.ielems {"a", "b", "c"} do process (v) end
    +
+ +
+
+ + ipairs (t) +
+
+ An iterator over elements of a sequence, until the first nil value.

+ +

Like Lua 5.1 and 5.3, but unlike Lua 5.2 (which looks for and uses the + __ipairs metamethod), this iterator returns successive key-value + pairs with integer keys starting at 1, up to the first nil valued + pair. + + +

Parameters:

+
    +
  • t + table + a table +
  • +
+ +

Returns:

+
    +
  1. + function + iterator function
  2. +
  3. + table + t, the table being iterated over
  4. +
  5. + int + index, the previous iteration index
  6. +
+ + +

See also:

+ + +

Usage:

+
    +
    -- length of sequence
    +args = {"first", "second", nil, "last"}
    +--> 1=first
    +--> 2=second
    +for i, v in std.ipairs (args) do
    +  print (string.format ("%d=%s", i, v))
    +end
    +
+ +
+
+ + ireverse (t) +
+
+ Return a new table with element order reversed. + Apart from the order of the elments returned, this function follows + the same rules as ipairs for determining first and last elements. + + +

Parameters:

+
    +
  • t + table + a table +
  • +
+ +

Returns:

+
    + + table + a new table with integer keyed elements in reverse + order with respect to t +
+ + +

See also:

+ + +

Usage:

+
    +
    local rielems = std.functional.compose (std.ireverse, std.ielems)
    +for e in rielems (l) do process (e) end
    +
+ +
+
+ + monkey_patch ([namespace=_G]) +
+
+ Overwrite core methods and metamethods with std enhanced versions.

+ +

Write all functions from this module, except std.barrel and + std.monkey_patch, into the given namespace. + + +

Parameters:

+
    +
  • namespace + table + where to install global functions + (default _G) +
  • +
+ +

Returns:

+
    + + table + the module table +
+ + + +

Usage:

+
    +
    local std = require "std".monkey_patch ()
    +
+ +
+
+ + npairs (t) +
+
+ Ordered iterator for integer keyed values. + Like ipairs, but does not stop until the largest integer key. + + +

Parameters:

+
    +
  • t + table + a table +
  • +
+ +

Returns:

+
    +
  1. + function + iterator function
  2. +
  3. + table + t
  4. +
+ + +

See also:

+ + +

Usage:

+
    +
    for i,v in npairs {"one", nil, "three"} do ... end
    +
+ +
+
+ + pairs (t) +
+
+ Enhance core pairs to respect __pairs even in Lua 5.1. + + +

Parameters:

+
    +
  • t + table + a table +
  • +
+ +

Returns:

+
    +
  1. + function + iterator function
  2. +
  3. + table + t, the table being iterated over
  4. +
  5. + key, the previous iteration key
  6. +
+ + +

See also:

+ + +

Usage:

+
    +
    for k, v in pairs {"a", b = "c", foo = 42} do process (k, v) end
    +
+ +
+
+ + require (module[, min[, too_big[, pattern]]]) +
+
+ Enhance core require to assert version number compatibility. + By default match against the last substring of (dot-delimited) + digits in the module version string. + + +

Parameters:

+
    +
  • module + string + module to require +
  • +
  • min + string + lowest acceptable version + (optional) +
  • +
  • too_big + string + lowest version that is too big + (optional) +
  • +
  • pattern + string + to match version in module.version or + module._VERSION (default: "([%.%d]+)%D*$") + (optional) +
  • +
+ + + + +

Usage:

+
    +
    -- posix.version == "posix library for Lua 5.2 / 32"
    +posix = require ("posix", "29")
    +
+ +
+
+ + ripairs (t) +
+
+ An iterator like ipairs, but in reverse. + Apart from the order of the elments returned, this function follows + the same rules as ipairs for determining first and last elements. + + +

Parameters:

+
    +
  • t + table + any table +
  • +
+ +

Returns:

+
    +
  1. + function + iterator function
  2. +
  3. + table + t
  4. +
  5. + number + #t + 1
  6. +
+ + +

See also:

+ + +

Usage:

+
    +
    for i, v = ripairs (t) do ... end
    +
+ +
+
+ + rnpairs (t) +
+
+ An iterator like npairs, but in reverse. + Apart from the order of the elments returned, this function follows + the same rules as npairs for determining first and last elements. + + +

Parameters:

+
    +
  • t + table + a table +
  • +
+ +

Returns:

+
    +
  1. + function + iterator function
  2. +
  3. + table + t
  4. +
+ + +

See also:

+ + +

Usage:

+
    +
    for i,v in rnpairs {"one", nil, "three"} do ... end
    +
+ +
+
+ + tostring (x) +
+
+ Enhance core tostring to render table contents as a string. + + +

Parameters:

+
    +
  • x + object to convert to string +
  • +
+ +

Returns:

+
    + + string + compact string rendering of x +
+ + + +

Usage:

+
    +
    -- {1=baz,foo=bar}
    +print (std.tostring {foo="bar","baz"})
    +
+ +
+
+

Tables

+ +
+
+ + std +
+
+ Module table.

+ +

In addition to the functions documented on this page, and a version + field, references to other submodule functions will be loaded on + demand. + + +

Fields:

+
    +
  • version + release version string +
  • +
+ + + + + +
+
+

Metamethods

+ +
+
+ + __index (name) +
+
+ Lazy loading of stdlib modules. + Don't load everything on initial startup, wait until first attempt + to access a submodule, and then load it on demand. + + +

Parameters:

+
    +
  • name + string + submodule name +
  • +
+ +

Returns:

+
    + + table or nil + the submodule that was loaded to satisfy the missing + name, otherwise nil if nothing was found +
+ + + +

Usage:

+
    +
    local std = require "std"
    +local prototype = std.object.prototype
    +
+ +
+
+ + +
+
+
+generated by LDoc 1.4.6 +Last updated 2018-09-16 19:20:25 +
+
+ + diff --git a/doc/modules/std.io.html b/doc/modules/std.io.html new file mode 100644 index 0000000..40339e2 --- /dev/null +++ b/doc/modules/std.io.html @@ -0,0 +1,614 @@ + + + + + stdlib 41.2.2 Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Module std.io

+

Additions to the core io module.

+

+ + +

The module table returned by std.io also contains all of the entries from + the core io module table. An hygienic way to import this module, then, + is simply to override core io locally:

+ + +
+local io = require "std.io"
+
+ +

+ + +

Functions

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
catdir (...)Concatenate directory names into a path.
catfile (...)Concatenate one or more directories and a filename into a path.
die (msg, ...)Die with error.
dirname (path)Remove the last dirsep delimited element from a path.
monkey_patch ([namespace=_G])Overwrite core io methods with std enhanced versions.
process_files (fn)Process files specified on the command-line.
readlines ([file=io.input()])Read a file or file handle into a list of lines.
shell (c)Perform a shell command and return its output.
slurp ([file=io.input()])Slurp a file handle.
splitdir (path)Split a directory path into components.
warn (msg, ...)Give warning with the name of program and file (if any).
writelines ([h=io.output()], ...)Write values adding a newline after each.
+

Types

+ + + + + +
fileprocessor (filename, i)Signature of process_files callback function.
+ +
+
+ + +

Functions

+ Methods +
+
+ + catdir (...) +
+
+ Concatenate directory names into a path. + + +

Parameters:

+
    +
  • ... + string + path components +
  • +
+ +

Returns:

+
    + + path without trailing separator +
+ + +

See also:

+ + +

Usage:

+
    +
    dirpath = catdir ("", "absolute", "directory")
    +
+ +
+
+ + catfile (...) +
+
+ Concatenate one or more directories and a filename into a path. + + +

Parameters:

+
    +
  • ... + string + path components +
  • +
+ +

Returns:

+
    + + string + path +
+ + +

See also:

+ + +

Usage:

+
    +
    filepath = catfile ("relative", "path", "filename")
    +
+ +
+
+ + die (msg, ...) +
+
+ Die with error. + This function uses the same rules to build a message prefix + as warn. + + +

Parameters:

+
    +
  • msg + string + format string +
  • +
  • ... + additional arguments to plug format string specifiers +
  • +
+ + + +

See also:

+ + +

Usage:

+
    +
    die ("oh noes! (%s)", tostring (obj))
    +
+ +
+
+ + dirname (path) +
+
+ Remove the last dirsep delimited element from a path. + + +

Parameters:

+
    +
  • path + string + file path +
  • +
+ +

Returns:

+
    + + string + a new path with the last dirsep and following + truncated +
+ + + +

Usage:

+
    +
    dir = dirname "/base/subdir/filename"
    +
+ +
+
+ + monkey_patch ([namespace=_G]) +
+
+ Overwrite core io methods with std enhanced versions.

+ +

Also adds readlines and writelines metamethods to core file objects. + + +

Parameters:

+
    +
  • namespace + table + where to install global functions + (default _G) +
  • +
+ +

Returns:

+
    + + table + the std.io module table +
+ + + +

Usage:

+
    +
    local io = require "std.io".monkey_patch ()
    +
+ +
+
+ + process_files (fn) +
+
+ Process files specified on the command-line. + Each filename is made the default input source with io.input, and + then the filename and argument number are passed to the callback + function. In list of filenames, - means io.stdin. If no + filenames were given, behave as if a single - was passed. + + +

Parameters:

+
    +
  • fn + fileprocessor + function called for each file argument +
  • +
+ + + + +

Usage:

+
    +
    #! /usr/bin/env lua
    +-- minimal cat command
    +local io = require "std.io"
    +io.process_files (function () io.write (io.slurp ()) end)
    +
+ +
+
+ + readlines ([file=io.input()]) +
+
+ Read a file or file handle into a list of lines. + The lines in the returned list are not \n terminated. + + +

Parameters:

+
    +
  • file + file or string + file handle or name; + if file is a file handle, that file is closed after reading + (default io.input()) +
  • +
+ +

Returns:

+
    + + list + lines +
+ + + +

Usage:

+
    +
    list = readlines "/etc/passwd"
    +
+ +
+
+ + shell (c) +
+
+ Perform a shell command and return its output. + + +

Parameters:

+ + +

Returns:

+
    + + string + output, or nil if error +
+ + +

See also:

+ + +

Usage:

+
    +
    users = shell [[cat /etc/passwd | awk -F: '{print $1;}']]
    +
+ +
+
+ + slurp ([file=io.input()]) +
+
+ Slurp a file handle. + + +

Parameters:

+
    +
  • file + file or string + file handle or name; + if file is a file handle, that file is closed after reading + (default io.input()) +
  • +
+ +

Returns:

+
    + + contents of file or handle, or nil if error +
+ + +

See also:

+ + +

Usage:

+
    +
    contents = slurp (filename)
    +
+ +
+
+ + splitdir (path) +
+
+ Split a directory path into components. + Empty components are retained: the root directory becomes {"", ""}. + + +

Parameters:

+
    +
  • path + path +
  • +
+ +

Returns:

+
    + + list of path components +
+ + +

See also:

+ + +

Usage:

+
    +
    dir_components = splitdir (filepath)
    +
+ +
+
+ + warn (msg, ...) +
+
+ Give warning with the name of program and file (if any). + If there is a global prog table, prefix the message with + prog.name or prog.file, and prog.line if any. Otherwise + if there is a global opts table, prefix the message with + opts.program and opts.line if any. std.optparse:parse + returns an opts table that provides the required program + field, as long as you assign it back to _G.opts. + + +

Parameters:

+
    +
  • msg + string + format string +
  • +
  • ... + additional arguments to plug format string specifiers +
  • +
+ + + +

See also:

+ + +

Usage:

+
    +
    local OptionParser = require "std.optparse"
    +local parser = OptionParser "eg 0\nUsage: eg\n"
    +_G.arg, _G.opts = parser:parse (_G.arg)
    +if not _G.opts.keep_going then
    +  require "std.io".warn "oh noes!"
    +end
    +
+ +
+
+ + writelines ([h=io.output()], ...) +
+
+ Write values adding a newline after each. + + +

Parameters:

+
    +
  • h + file + open writable file handle; + the file is not closed after writing + (default io.output()) +
  • +
  • ... + string or number + values to write (as for write) +
  • +
+ + + + +

Usage:

+
    +
    writelines (io.stdout, "first line", "next line")
    +
+ +
+
+

Types

+ +
+
+ + fileprocessor (filename, i) +
+
+ Signature of process_files callback function. + + +

Parameters:

+
    +
  • filename + string + filename +
  • +
  • i + int + argument number of filename +
  • +
+ + + + +

Usage:

+
    +
    local fileprocessor = function (filename, i)
    +  io.write (tostring (i) .. ":\n===\n" .. io.slurp (filename) .. "\n")
    +end
    +io.process_files (fileprocessor)
    +
+ +
+
+ + +
+
+
+generated by LDoc 1.4.6 +Last updated 2018-09-16 19:20:25 +
+
+ + diff --git a/doc/modules/std.math.html b/doc/modules/std.math.html new file mode 100644 index 0000000..6448b9f --- /dev/null +++ b/doc/modules/std.math.html @@ -0,0 +1,222 @@ + + + + + stdlib 41.2.2 Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Module std.math

+

Additions to the core math module.

+

+ + +

The module table returned by std.math also contains all of the entries from + the core math table. An hygienic way to import this module, then, is simply + to override the core math locally:

+ + +
+local math = require "std.math"
+
+ +

+ + +

Functions

+ + + + + + + + + + + + + +
floor (n[, p=0])Extend math.floor to take the number of decimal places.
monkey_patch ([namespace=_G])Overwrite core math methods with std enhanced versions.
round (n[, p=0])Round a number to a given number of decimal places
+ +
+
+ + +

Functions

+ Methods +
+
+ + floor (n[, p=0]) +
+
+ Extend math.floor to take the number of decimal places. + + +

Parameters:

+
    +
  • n + number + number +
  • +
  • p + int + number of decimal places to truncate to + (default 0) +
  • +
+ +

Returns:

+
    + + number + n truncated to p decimal places +
+ + + +

Usage:

+
    +
    tenths = floor (magnitude, 1)
    +
+ +
+
+ + monkey_patch ([namespace=_G]) +
+
+ Overwrite core math methods with std enhanced versions. + + +

Parameters:

+
    +
  • namespace + table + where to install global functions + (default _G) +
  • +
+ +

Returns:

+
    + + table + the module table +
+ + + +

Usage:

+
    +
    require "std.math".monkey_patch ()
    +
+ +
+
+ + round (n[, p=0]) +
+
+ Round a number to a given number of decimal places + + +

Parameters:

+
    +
  • n + number + number +
  • +
  • p + int + number of decimal places to round to + (default 0) +
  • +
+ +

Returns:

+
    + + number + n rounded to p decimal places +
+ + + +

Usage:

+
    +
    roughly = round (exactly, 2)
    +
+ +
+
+ + +
+
+
+generated by LDoc 1.4.6 +Last updated 2018-09-16 19:20:25 +
+
+ + diff --git a/doc/modules/std.operator.html b/doc/modules/std.operator.html new file mode 100644 index 0000000..bf6fec0 --- /dev/null +++ b/doc/modules/std.operator.html @@ -0,0 +1,742 @@ + + + + + stdlib 41.2.2 Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Module std.operator

+

Functional forms of Lua operators.

+

+ +

+ + +

Functions

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
concat (a, b)Stringify and concatenate arguments.
conj (a, b)Return the logical conjunction of the arguments.
diff (a, b)Return the difference of the arguments.
disj (a, b)Return the logical disjunction of the arguments.
eq (a, b)Return the equality of the arguments.
get (t, k)Dereference a table.
gt (a, b)Return whether the arguments are in descending order.
gte (a, b)Return whether the arguments are not in ascending order.
lt (a, b)Return whether the arguments are in ascending order.
lte (a, b)Return whether the arguments are not in descending order.
mod (a, b)Return the modulus of the arguments.
neg (a)Return the logical negation of the arguments.
neq (a, b)Return the inequality of the arguments.
pow (a, b)Return the exponent of the arguments.
prod (a, b)Return the product of the arguments.
quot (a, b)Return the quotient of the arguments.
set (t, k, v)Set a table element, honoring metamethods.
sum (a, b)Return the sum of the arguments.
+ +
+
+ + +

Functions

+ Methods +
+
+ + concat (a, b) +
+
+ Stringify and concatenate arguments. + + +

Parameters:

+
    +
  • a + an argument +
  • +
  • b + another argument +
  • +
+ +

Returns:

+
    + + concatenation of stringified arguments. +
+ + + +

Usage:

+
    +
    --> "=> 1000010010"
    +functional.foldl (concat, "=> ", {10000, 100, 10})
    +
+ +
+
+ + conj (a, b) +
+
+ Return the logical conjunction of the arguments. + + +

Parameters:

+
    +
  • a + an argument +
  • +
  • b + another argument +
  • +
+ +

Returns:

+
    + + logical a and b +
+ + + +

Usage:

+
    +
    --> true
    +functional.foldl (conj, {true, 1, "false"})
    +
+ +
+
+ + diff (a, b) +
+
+ Return the difference of the arguments. + + +

Parameters:

+
    +
  • a + an argument +
  • +
  • b + another argument +
  • +
+ +

Returns:

+
    + + the difference between a and b +
+ + + +

Usage:

+
    +
    --> 890
    +functional.foldl (diff, {10000, 100, 10})
    +
+ +
+
+ + disj (a, b) +
+
+ Return the logical disjunction of the arguments. + + +

Parameters:

+
    +
  • a + an argument +
  • +
  • b + another argument +
  • +
+ +

Returns:

+
    + + logical a or b +
+ + + +

Usage:

+
    +
    --> true
    +functional.foldl (disj, {true, 1, false})
    +
+ +
+
+ + eq (a, b) +
+
+ Return the equality of the arguments. + + +

Parameters:

+
    +
  • a + an argument +
  • +
  • b + another argument +
  • +
+ +

Returns:

+
    + + true if a is b, otherwise false +
+ + + + +
+
+ + get (t, k) +
+
+ Dereference a table. + + +

Parameters:

+
    +
  • t + table + a table +
  • +
  • k + a key to lookup in t +
  • +
+ +

Returns:

+
    + + value stored at t[k] if any, otherwise nil +
+ + + +

Usage:

+
    +
    --> 4
    +functional.foldl (get, {1, {{2, 3, 4}, 5}}, {2, 1, 3})
    +
+ +
+
+ + gt (a, b) +
+
+ Return whether the arguments are in descending order. + + +

Parameters:

+
    +
  • a + an argument +
  • +
  • b + another argument +
  • +
+ +

Returns:

+
    + + true if a is greater then b, otherwise false +
+ + + + +
+
+ + gte (a, b) +
+
+ Return whether the arguments are not in ascending order. + + +

Parameters:

+
    +
  • a + an argument +
  • +
  • b + another argument +
  • +
+ +

Returns:

+
    + + true if a is not greater then b, otherwise false +
+ + + + +
+
+ + lt (a, b) +
+
+ Return whether the arguments are in ascending order. + + +

Parameters:

+
    +
  • a + an argument +
  • +
  • b + another argument +
  • +
+ +

Returns:

+
    + + true if a is less then b, otherwise false +
+ + + + +
+
+ + lte (a, b) +
+
+ Return whether the arguments are not in descending order. + + +

Parameters:

+
    +
  • a + an argument +
  • +
  • b + another argument +
  • +
+ +

Returns:

+
    + + true if a is not greater then b, otherwise false +
+ + + + +
+
+ + mod (a, b) +
+
+ Return the modulus of the arguments. + + +

Parameters:

+
    +
  • a + an argument +
  • +
  • b + another argument +
  • +
+ +

Returns:

+
    + + the modulus of a and b +
+ + + +

Usage:

+
    +
    --> 3
    +functional.foldl (mod, {65536, 100, 11})
    +
+ +
+
+ + neg (a) +
+
+ Return the logical negation of the arguments. + + +

Parameters:

+
    +
  • a + an argument +
  • +
+ +

Returns:

+
    + + not a +
+ + + +

Usage:

+
    +
    --> {true, false, false, false}
    +functional.bind (functional.map, {std.ielems, neg}) {false, true, 1, 0}
    +
+ +
+
+ + neq (a, b) +
+
+ Return the inequality of the arguments. + + +

Parameters:

+
    +
  • a + an argument +
  • +
  • b + another argument +
  • +
+ +

Returns:

+
    + + false if a is b, otherwise true +
+ + + +

Usage:

+
    +
    --> true
    +local f = require "std.functional"
    +table.empty (f.filter (f.bind (neq, {6}), std.ielems, {6, 6, 6})
    +
+ +
+
+ + pow (a, b) +
+
+ Return the exponent of the arguments. + + +

Parameters:

+
    +
  • a + an argument +
  • +
  • b + another argument +
  • +
+ +

Returns:

+
    + + the a to the power of b +
+ + + +

Usage:

+
    +
    --> 4096
    +functional.foldl (pow, {2, 3, 4})
    +
+ +
+
+ + prod (a, b) +
+
+ Return the product of the arguments. + + +

Parameters:

+
    +
  • a + an argument +
  • +
  • b + another argument +
  • +
+ +

Returns:

+
    + + the product of a and b +
+ + + +

Usage:

+
    +
    --> 10000000
    +functional.foldl (prod, {10000, 100, 10})
    +
+ +
+
+ + quot (a, b) +
+
+ Return the quotient of the arguments. + + +

Parameters:

+
    +
  • a + an argument +
  • +
  • b + another argument +
  • +
+ +

Returns:

+
    + + the quotient a and b +
+ + + +

Usage:

+
    +
    --> 1000
    +functional.foldr (quot, {10000, 100, 10})
    +
+ +
+
+ + set (t, k, v) +
+
+ Set a table element, honoring metamethods. + + +

Parameters:

+
    +
  • t + table + a table +
  • +
  • k + a key to lookup in t +
  • +
  • v + a value to set for k +
  • +
+ +

Returns:

+
    + + table + t +
+ + + +

Usage:

+
    +
    -- destructive table merge:
    +--> {"one", bar="baz", two=5}
    +functional.reduce (set, {"foo", bar="baz"}, {"one", two=5})
    +
+ +
+
+ + sum (a, b) +
+
+ Return the sum of the arguments. + + +

Parameters:

+
    +
  • a + an argument +
  • +
  • b + another argument +
  • +
+ +

Returns:

+
    + + the sum of the a and b +
+ + + +

Usage:

+
    +
    --> 10110
    +functional.foldl (sum, {10000, 100, 10})
    +
+ +
+
+ + +
+
+
+generated by LDoc 1.4.6 +Last updated 2018-09-16 19:20:25 +
+
+ + diff --git a/doc/modules/std.package.html b/doc/modules/std.package.html new file mode 100644 index 0000000..bdc11c6 --- /dev/null +++ b/doc/modules/std.package.html @@ -0,0 +1,417 @@ + + + + + stdlib 41.2.2 Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Module std.package

+

Additions to the core package module.

+

+ + +

The module table returned by std.package also contains all of the entries + from the core package table. An hygienic way to import this module, then, is + simply to override core package locally:

+ + +
+local package = require "std.package"
+
+ +

+ + +

Functions

+ + + + + + + + + + + + + + + + + + + + + +
find (pathstrings, patt[, init=1[, plain=false]])Look for a path segment match of patt in pathstrings.
insert (pathstrings[, pos=n+1], value)Insert a new element into a package.path like string of paths.
mappath (pathstrings, callback, ...)Call a function with each element of a path string.
normalize (...)Normalize a path list.
remove (pathstrings[, pos=n])Remove any element from a package.path like string of paths.
+

Tables

+ + + + + +
packageMake named constants for package.config + (undocumented in 5.1; see luaconf.h for C equivalents).
+

Types

+ + + + + +
mappathcb (element, ...)Function signature of a callback for mappath.
+ +
+
+ + +

Functions

+ Methods +
+
+ + find (pathstrings, patt[, init=1[, plain=false]]) +
+
+ Look for a path segment match of patt in pathstrings. + + +

Parameters:

+
    +
  • pathstrings + string + pathsep delimited path elements +
  • +
  • patt + string + a Lua pattern to search for in pathstrings +
  • +
  • init + int + element (not byte index!) to start search at. + Negative numbers begin counting backwards from the last element + (default 1) +
  • +
  • plain + bool + unless false, treat patt as a plain + string, not a pattern. Note that if plain is given, then init + must be given as well. + (default false) +
  • +
+ +

Returns:

+
    + + the matching element number (not byte index!) and full text + of the matching element, if any; otherwise nil +
+ + + +

Usage:

+
    +
    i, s = find (package.path, "^[^" .. package.dirsep .. "/]")
    +
+ +
+
+ + insert (pathstrings[, pos=n+1], value) +
+
+ Insert a new element into a package.path like string of paths. + + +

Parameters:

+
    +
  • pathstrings + string + a package.path like string +
  • +
  • pos + int + element index at which to insert value, where n is + the number of elements prior to insertion + (default n+1) +
  • +
  • value + string + new path element to insert +
  • +
+ +

Returns:

+
    + + string + a new string with the new element inserted +
+ + + +

Usage:

+
    +
    package.path = insert (package.path, 1, install_dir .. "/?.lua")
    +
+ +
+
+ + mappath (pathstrings, callback, ...) +
+
+ Call a function with each element of a path string. + + +

Parameters:

+
    +
  • pathstrings + string + a package.path like string +
  • +
  • callback + mappathcb + function to call for each element +
  • +
  • ... + additional arguments passed to callback +
  • +
+ +

Returns:

+
    + + nil, or first non-nil returned by callback +
+ + + +

Usage:

+
    +
    mappath (package.path, searcherfn, transformfn)
    +
+ +
+
+ + normalize (...) +
+
+ Normalize a path list. + Removing redundant . and .. directories, and keep only the first + instance of duplicate elements. Each argument can contain any number + of pathsep delimited elements; wherein characters are subject to + / and ? normalization, converting / to dirsep and ? to + path_mark (unless immediately preceded by a % character). + + +

Parameters:

+
    +
  • ... + path elements +
  • +
+ +

Returns:

+
    + + string + a single normalized pathsep delimited paths string +
+ + + +

Usage:

+
    +
    package.path = normalize (user_paths, sys_paths, package.path)
    +
+ +
+
+ + remove (pathstrings[, pos=n]) +
+
+ Remove any element from a package.path like string of paths. + + +

Parameters:

+
    +
  • pathstrings + string + a package.path like string +
  • +
  • pos + int + element index from which to remove an item, where n + is the number of elements prior to removal + (default n) +
  • +
+ +

Returns:

+
    + + string + a new string with given element removed +
+ + + +

Usage:

+
    +
    package.path = remove (package.path)
    +
+ +
+
+

Tables

+ +
+
+ + package +
+
+ Make named constants for package.config + (undocumented in 5.1; see luaconf.h for C equivalents). + + +

Fields:

+
    +
  • dirsep + string + directory separator +
  • +
  • pathsep + string + path separator +
  • +
  • path_mark + string + string that marks substitution points in a path template +
  • +
  • execdir + string + (Windows only) replaced by the executable's directory in a path +
  • +
  • igmark + string + Mark to ignore all before it when building luaopen_ function name. +
  • +
+ + + + + +
+
+

Types

+ +
+
+ + mappathcb (element, ...) +
+
+ Function signature of a callback for mappath. + + +

Parameters:

+
    +
  • element + string + an element from a pathsep delimited string of + paths +
  • +
  • ... + additional arguments propagated from mappath +
  • +
+ +

Returns:

+
    + + non-nil to break, otherwise continue with the next element +
+ + + + +
+
+ + +
+
+
+generated by LDoc 1.4.6 +Last updated 2018-09-16 19:20:25 +
+
+ + diff --git a/doc/modules/std.strict.html b/doc/modules/std.strict.html new file mode 100644 index 0000000..73b72c1 --- /dev/null +++ b/doc/modules/std.strict.html @@ -0,0 +1,161 @@ + + + + + stdlib 41.2.2 Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Module std.strict

+

Checks uses of undeclared global variables.

+

All global variables must be 'declared' through a regular + assignment (even assigning nil will do) in a top-level + chunk before being used anywhere or assigned to inside a function.

+ +

To use this module, just require it near the start of your program.

+ +

From Lua distribution (etc/strict.lua).

+ + +

Functions

+ + + + + + + + + +
__index (t, n)Detect dereference of undeclared global.
__newindex (t, n, v)Detect assignment to undeclared global.
+ +
+
+ + +

Functions

+ Methods +
+
+ + __index (t, n) +
+
+ Detect dereference of undeclared global. + + +

Parameters:

+
    +
  • t + table + _G +
  • +
  • n + string + name of the variable being dereferenced +
  • +
+ + + + + +
+
+ + __newindex (t, n, v) +
+
+ Detect assignment to undeclared global. + + +

Parameters:

+
    +
  • t + table + _G +
  • +
  • n + string + name of the variable being declared +
  • +
  • v + initial value of the variable +
  • +
+ + + + + +
+
+ + +
+
+
+generated by LDoc 1.4.6 +Last updated 2018-09-16 19:20:25 +
+
+ + diff --git a/doc/modules/std.string.html b/doc/modules/std.string.html new file mode 100644 index 0000000..02917b1 --- /dev/null +++ b/doc/modules/std.string.html @@ -0,0 +1,1208 @@ + + + + + stdlib 41.2.2 Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Module std.string

+

Additions to the core string module.

+

+ + +

The module table returned by std.string also contains all of the entries + from the core string table. An hygienic way to import this module, then, is + simply to override the core string locally:

+ + +
+local string = require "std.string"
+
+ +

+ + +

Functions

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
caps (s)Capitalise each word in a string.
chomp (s)Remove any final newline from a string.
escape_pattern (s)Escape a string to be used as a pattern.
escape_shell (s)Escape a string to be used as a shell token.
finds (s, pattern[, init=1[, plain]])Repeatedly string.find until target string is exhausted.
format (f[, ...])Extend to work better with one argument.
ltrim (s[, r="%s+"])Remove leading matter from a string.
monkey_patch ([namespace=_G])Overwrite core string methods with std enhanced versions.
numbertosi (n)Write a number using SI suffixes.
ordinal_suffix (n)Return the English suffix for an ordinal.
pad (s, w[, p=" "])Justify a string.
prettytostring (x[, indent="\t"[, spacing=""]])Pretty-print a table, or other object.
render (x, open, close, elem, pair, sep[, roots])Turn tables into strings with recursion detection.
rtrim (s[, r="%s+"])Remove trailing matter from a string.
split (s[, sep="%s+"])Split a string at a given separator.
tfind (s, pattern[, init=1[, plain]])Do string.find, returning a table of captures.
trim (s[, r="%s+"])Remove leading and trailing matter from a string.
wrap (s[, w=78[, ind=0[, ind1=ind]]])Wrap a string into a paragraph.
+

Fields

+ + + + + + + + + + + + + +
__concatString concatenation operation.
__indexString subscript operation.
pickleConvert a value to a string.
+

Types

+ + + + + + + + + + + + + + + + + + + + + +
closetablecb (t)Signature of render close table callback.
elementcb (x)Signature of render element callback.
opentablecb (t)Signature of render open table callback.
paircb (t, key, value, keystr, valuestr)Signature of render pair callback.
separatorcb (t, pk, pv, fk, fv)Signature of render separator callback.
+ +
+
+ + +

Functions

+ Methods +
+
+ + caps (s) +
+
+ Capitalise each word in a string. + + +

Parameters:

+
    +
  • s + string + any string +
  • +
+ +

Returns:

+
    + + string + s with each word capitalized +
+ + + +

Usage:

+
    +
    userfullname = caps (input_string)
    +
+ +
+
+ + chomp (s) +
+
+ Remove any final newline from a string. + + +

Parameters:

+
    +
  • s + string + any string +
  • +
+ +

Returns:

+
    + + string + s with any single trailing newline removed +
+ + + +

Usage:

+
    +
    line = chomp (line)
    +
+ +
+
+ + escape_pattern (s) +
+
+ Escape a string to be used as a pattern. + + +

Parameters:

+
    +
  • s + string + any string +
  • +
+ +

Returns:

+
    + + string + s with active pattern characters escaped +
+ + + +

Usage:

+
    +
    substr = inputstr:match (escape_pattern (literal))
    +
+ +
+
+ + escape_shell (s) +
+
+ Escape a string to be used as a shell token. + Quotes spaces, parentheses, brackets, quotes, apostrophes and + whitespace. + + +

Parameters:

+
    +
  • s + string + any string +
  • +
+ +

Returns:

+
    + + string + s with active shell characters escaped +
+ + + +

Usage:

+
    +
    os.execute ("echo " .. escape_shell (outputstr))
    +
+ +
+
+ + finds (s, pattern[, init=1[, plain]]) +
+
+ Repeatedly string.find until target string is exhausted. + + +

Parameters:

+
    +
  • s + string + target string +
  • +
  • pattern + string + pattern to match in s +
  • +
  • init + int + start position + (default 1) +
  • +
  • plain + bool + inhibit magic characters + (optional) +
  • +
+ +

Returns:

+
    + + list of {from, to; capt = {captures}} +
+ + +

See also:

+ + +

Usage:

+
    +
    for t in std.elems (finds ("the target string", "%S+")) do
    +  print (tostring (t.capt))
    +end
    +
+ +
+
+ + format (f[, ...]) +
+
+ Extend to work better with one argument. + If only one argument is passed, no formatting is attempted. + + +

Parameters:

+
    +
  • f + string + format string +
  • +
  • ... + arguments to format + (optional) +
  • +
+ +

Returns:

+
    + + formatted string +
+ + + +

Usage:

+
    +
    print (format "100% stdlib!")
    +
+ +
+
+ + ltrim (s[, r="%s+"]) +
+
+ Remove leading matter from a string. + + +

Parameters:

+
    +
  • s + string + any string +
  • +
  • r + string + leading pattern + (default "%s+") +
  • +
+ +

Returns:

+
    + + string + s with leading r stripped +
+ + + +

Usage:

+
    +
    print ("got: " .. ltrim (userinput))
    +
+ +
+
+ + monkey_patch ([namespace=_G]) +
+
+ Overwrite core string methods with std enhanced versions.

+ +

Also adds auto-stringification to .. operator on core strings, and + integer indexing of strings with [] dereferencing. + + +

Parameters:

+
    +
  • namespace + table + where to install global functions + (default _G) +
  • +
+ +

Returns:

+
    + + table + the module table +
+ + + +

Usage:

+
    +
    local string = require "std.string".monkey_patch ()
    +
+ +
+
+ + numbertosi (n) +
+
+ Write a number using SI suffixes. + The number is always written to 3 s.f. + + +

Parameters:

+
    +
  • n + number or string + any numeric value +
  • +
+ +

Returns:

+
    + + string + n simplifed using largest available SI suffix. +
+ + + +

Usage:

+
    +
    print (numbertosi (bitspersecond) .. "bps")
    +
+ +
+
+ + ordinal_suffix (n) +
+
+ Return the English suffix for an ordinal. + + +

Parameters:

+
    +
  • n + int or string + any integer value +
  • +
+ +

Returns:

+
    + + string + English suffix for n +
+ + + +

Usage:

+
    +
    local now = os.date "*t"
    +print ("%d%s day of the week", now.day, ordinal_suffix (now.day))
    +
+ +
+
+ + pad (s, w[, p=" "]) +
+
+ Justify a string. + When the string is longer than w, it is truncated (left or right + according to the sign of w). + + +

Parameters:

+
    +
  • s + string + a string to justify +
  • +
  • w + int + width to justify to (-ve means right-justify; +ve means + left-justify) +
  • +
  • p + string + string to pad with + (default " ") +
  • +
+ +

Returns:

+
    + + string + s justified to w characters wide +
+ + + +

Usage:

+
    +
    print (pad (trim (outputstr, 78)) .. "\n")
    +
+ +
+
+ + prettytostring (x[, indent="\t"[, spacing=""]]) +
+
+ Pretty-print a table, or other object. + + +

Parameters:

+
    +
  • x + object to convert to string +
  • +
  • indent + string + indent between levels + (default "\t") +
  • +
  • spacing + string + space before every line + (default "") +
  • +
+ +

Returns:

+
    + + string + pretty string rendering of x +
+ + + +

Usage:

+
    +
    print (prettytostring (std, "  "))
    +
+ +
+
+ + render (x, open, close, elem, pair, sep[, roots]) +
+
+ Turn tables into strings with recursion detection. + N.B. Functions calling render should not recurse, or recursion + detection will not work. + + +

Parameters:

+
    +
  • x + object to convert to string +
  • +
  • open + opentablecb + open table rendering function +
  • +
  • close + closetablecb + close table rendering function +
  • +
  • elem + elementcb + element rendering function +
  • +
  • pair + paircb + pair rendering function +
  • +
  • sep + separatorcb + separator rendering function +
  • +
  • roots + table + accumulates table references to detect recursion + (optional) +
  • +
+ +

Returns:

+
    + + string representation of x +
+ + + +

Usage:

+
    +
    function tostring (x)
    +  return render (x, lambda '="{"', lambda '="}"', tostring,
    +                 lambda '=_4.."=".._5', lambda '= _4 and "," or ""',
    +                 lambda '=","')
    +end
    +
+ +
+
+ + rtrim (s[, r="%s+"]) +
+
+ Remove trailing matter from a string. + + +

Parameters:

+
    +
  • s + string + any string +
  • +
  • r + string + trailing pattern + (default "%s+") +
  • +
+ +

Returns:

+
    + + string + s with trailing r stripped +
+ + + +

Usage:

+
    +
    print ("got: " .. rtrim (userinput))
    +
+ +
+
+ + split (s[, sep="%s+"]) +
+
+ Split a string at a given separator. + Separator is a Lua pattern, so you have to escape active characters, + ^$()%.[]*+-? with a % prefix to match a literal character in s. + + +

Parameters:

+
    +
  • s + string + to split +
  • +
  • sep + string + separator pattern + (default "%s+") +
  • +
+ +

Returns:

+
    + + list of strings +
+ + + +

Usage:

+
    +
    words = split "a very short sentence"
    +
+ +
+
+ + tfind (s, pattern[, init=1[, plain]]) +
+
+ Do string.find, returning a table of captures. + + +

Parameters:

+
    +
  • s + string + target string +
  • +
  • pattern + string + pattern to match in s +
  • +
  • init + int + start position + (default 1) +
  • +
  • plain + bool + inhibit magic characters + (optional) +
  • +
+ +

Returns:

+
    +
  1. + int + start of match
  2. +
  3. + int + end of match
  4. +
  5. + table + list of captured strings
  6. +
+ + +

See also:

+ + +

Usage:

+
    +
    b, e, captures = tfind ("the target string", "%s", 10)
    +
+ +
+
+ + trim (s[, r="%s+"]) +
+
+ Remove leading and trailing matter from a string. + + +

Parameters:

+
    +
  • s + string + any string +
  • +
  • r + string + trailing pattern + (default "%s+") +
  • +
+ +

Returns:

+
    + + string + s with leading and trailing r stripped +
+ + + +

Usage:

+
    +
    print ("got: " .. trim (userinput))
    +
+ +
+
+ + wrap (s[, w=78[, ind=0[, ind1=ind]]]) +
+
+ Wrap a string into a paragraph. + + +

Parameters:

+
    +
  • s + string + a paragraph of text +
  • +
  • w + int + width to wrap to + (default 78) +
  • +
  • ind + int + indent + (default 0) +
  • +
  • ind1 + int + indent of first line + (default ind) +
  • +
+ +

Returns:

+
    + + string + s wrapped to w columns +
+ + + +

Usage:

+
    +
    print (wrap (copyright, 72, 4))
    +
+ +
+
+

Fields

+ +
+
+ + __concat +
+
+ String concatenation operation. + + +
    +
  • s + string + initial string +
  • +
  • o + object to stringify and concatenate +
  • +
+ + + + +

Usage:

+
    +
    local string = require "std.string".monkey_patch ()
    +concatenated = "foo" .. {"bar"}
    +
+ +
+
+ + __index +
+
+ String subscript operation. + + +
    +
  • s + string + string +
  • +
  • i + int or string + index or method name +
  • +
+ + + + +

Usage:

+
    +
    getmetatable ("").__index = require "std.string".__index
    +third = ("12345")[3]
    +
+ +
+
+ + pickle +
+
+ Convert a value to a string. + The string can be passed to functional.eval to retrieve the value. + + +
    +
  • x + object to pickle +
  • +
+ + + +

See also:

+ + +

Usage:

+
    +
    function slow_identity (x) return functional.eval (pickle (x)) end
    +
+ +
+
+

Types

+ +
+
+ + closetablecb (t) +
+
+ Signature of render close table callback. + + +

Parameters:

+
    +
  • t + table + table just rendered +
  • +
+ +

Returns:

+
    + + string + close table rendering +
+ + +

See also:

+ + +

Usage:

+
    +
    function close (t) return "}" end
    +
+ +
+
+ + elementcb (x) +
+
+ Signature of render element callback. + + +

Parameters:

+
    +
  • x + element to render +
  • +
+ +

Returns:

+
    + + string + element rendering +
+ + +

See also:

+ + +

Usage:

+
    +
    function element (e) return require "std".tostring (e) end
    +
+ +
+
+ + opentablecb (t) +
+
+ Signature of render open table callback. + + +

Parameters:

+
    +
  • t + table + table about to be rendered +
  • +
+ +

Returns:

+
    + + string + open table rendering +
+ + +

See also:

+ + +

Usage:

+
    +
    function open (t) return "{" end
    +
+ +
+
+ + paircb (t, key, value, keystr, valuestr) +
+
+ Signature of render pair callback. + Trying to re-render key or value here will break recursion + detection, use strkey and strvalue pre-rendered values instead. + + +

Parameters:

+
    +
  • t + table + table containing pair being rendered +
  • +
  • key + key part of key being rendered +
  • +
  • value + value part of key being rendered +
  • +
  • keystr + string + prerendered key +
  • +
  • valuestr + string + prerendered value +
  • +
+ +

Returns:

+
    + + string + pair rendering +
+ + +

See also:

+ + +

Usage:

+
    +
    function pair (_, _, _, key, value) return key .. "=" .. value end
    +
+ +
+
+ + separatorcb (t, pk, pv, fk, fv) +
+
+ Signature of render separator callback. + + +

Parameters:

+
    +
  • t + table + table currently being rendered +
  • +
  • pk + t key preceding separator, or nil for first key +
  • +
  • pv + t value preceding separator, or nil for first value +
  • +
  • fk + t key following separator, or nil for last key +
  • +
  • fv + t value following separator, or nil for last value +
  • +
+ +

Returns:

+
    + + string + separator rendering +
+ + + +

Usage:

+
    +
    function separator (_, _, _, fk) return fk and "," or "" end
    +
+ +
+
+ + +
+
+
+generated by LDoc 1.4.6 +Last updated 2018-09-16 19:20:25 +
+
+ + diff --git a/doc/modules/std.table.html b/doc/modules/std.table.html new file mode 100644 index 0000000..e4e2be4 --- /dev/null +++ b/doc/modules/std.table.html @@ -0,0 +1,1158 @@ + + + + + stdlib 41.2.2 Reference + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Module std.table

+

Extensions to the core table module.

+

+ + +

The module table returned by std.table also contains all of the entries from + the core table module. An hygienic way to import this module, then, is simply + to override the core table locally:

+ + +
+local table = require "std.table"
+
+ +

+ + +

Functions

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
clone (t[, map={}[, nometa]])Make a shallow copy of a table, including any metatable.
clone_select (t[, keys={}[, nometa]])Make a partial clone of a table.
depair (ls)Turn a list of pairs into a table.
empty (t)Return whether table is empty.
enpair (t)Turn a table into a list of pairs.
flatten (t)Flatten a nested table into a list.
insert (t[, pos=len (t)], v)Enhance core table.insert to return its result.
invert (t)Invert a table.
keys (t)Make the list of keys in table.
len (t)Equivalent to # operation, but respecting __len even on Lua 5.1.
maxn (t)Largest integer key in a table.
merge (t, u[, map={}[, nometa]])Destructively merge another table's fields into another.
merge_select (t, u[, keys={}[, nometa]])Destructively merge another table's named fields into table.
monkey_patch ([namespace=_G])Overwrite core table methods with std enhanced versions.
new ([x=nil[, t={}]])Make a table with a default value for unset keys.
okeys (t)Make an ordered list of keys in table.
pack (...)Turn a tuple into a list.
project (fkey, tt)Project a list of fields from a list of tables.
remove (t[, pos=len (t)])Enhance core table.remove to respect __len when pos is omitted.
shape (dims, t)Shape a table according to a list of dimensions.
size (t)Find the number of elements in a table.
sort (t[, c=std.operator.lt])Enhance core table.sort to return its result.
unpack (t[, i=1[, j=table.maxn(t)]])Enhance core table.unpack to always unpack up to maxn (t).
values (t)Make the list of values of a table.
+

Types

+ + + + + +
comparator (a, b)Signature of a sort comparator function.
+ +
+
+ + +

Functions

+ Methods +
+
+ + clone (t[, map={}[, nometa]]) +
+
+ Make a shallow copy of a table, including any metatable.

+ +

To make deep copies, use tree.clone. + + +

Parameters:

+
    +
  • t + table + source table +
  • +
  • map + table + table of {old_key=new_key, ...} + (default {}) +
  • +
  • nometa + bool + if non-nil don't copy metatable + (optional) +
  • +
+ +

Returns:

+
    + + copy of t, also sharing t's metatable unless nometa + is true, and with keys renamed according to map +
+ + +

See also:

+ + +

Usage:

+
    +
    shallowcopy = clone (original, {rename_this = "to_this"}, ":nometa")
    +
+ +
+
+ + clone_select (t[, keys={}[, nometa]]) +
+
+ Make a partial clone of a table.

+ +

Like clone, but does not copy any fields by default. + + +

Parameters:

+
    +
  • t + table + source table +
  • +
  • keys + table + list of keys to copy + (default {}) +
  • +
  • nometa + bool + if non-nil don't copy metatable + (optional) +
  • +
+ +

Returns:

+
    + + table + copy of fields in selection from t, also sharing t's + metatable unless nometa +
+ + +

See also:

+ + +

Usage:

+
    +
    partialcopy = clone_select (original, {"this", "and_this"}, true)
    +
+ +
+
+ + depair (ls) +
+
+ Turn a list of pairs into a table. + + +

Parameters:

+
    +
  • ls + table + list of lists +
  • +
+ +

Returns:

+
    + + table + a flat table with keys and values from ls +
+ + +

See also:

+ + +

Usage:

+
    +
    --> {a=1, b=2, c=3}
    +depair {{"a", 1}, {"b", 2}, {"c", 3}}
    +
+ +
+
+ + empty (t) +
+
+ Return whether table is empty. + + +

Parameters:

+
    +
  • t + table + any table +
  • +
+ +

Returns:

+
    + + boolean + true if t is empty, otherwise false +
+ + + +

Usage:

+
    +
    if empty (t) then error "ohnoes" end
    +
+ +
+
+ + enpair (t) +
+
+ Turn a table into a list of pairs. + + +

Parameters:

+
    +
  • t + table + a table {i1=v1, ..., in=vn} +
  • +
+ +

Returns:

+
    + + table + a new list of pairs containing {{i1, v1}, ..., {in, vn}} +
+ + +

See also:

+ + +

Usage:

+
    +
    --> {{1, "a"}, {2, "b"}, {3, "c"}}
    +enpair {"a", "b", "c"}
    +
+ +
+
+ + flatten (t) +
+
+ Flatten a nested table into a list. + + +

Parameters:

+
    +
  • t + table + a table +
  • +
+ +

Returns:

+
    + + table + a list of all non-table elements of t +
+ + + +

Usage:

+
    +
    --> {1, 2, 3, 4, 5}
    +flatten {{1, {{2}, 3}, 4}, 5}
    +
+ +
+
+ + insert (t[, pos=len (t)], v) +
+
+ Enhance core table.insert to return its result. + If pos is not given, respect __len metamethod when calculating + default append. Also, diagnose out of bounds pos arguments + consistently on any supported version of Lua. + + +

Parameters:

+
    +
  • t + table + a table +
  • +
  • pos + int + index at which to insert new element + (default len (t)) +
  • +
  • v + value to insert into t +
  • +
+ +

Returns:

+
    + + table + t +
+ + + +

Usage:

+
    +
    --> {1, "x", 2, 3, "y"}
    +insert (insert ({1, 2, 3}, 2, "x"), "y")
    +
+ +
+
+ + invert (t) +
+
+ Invert a table. + + +

Parameters:

+
    +
  • t + table + a table with {k=v, ...} +
  • +
+ +

Returns:

+
    + + table + inverted table {v=k, ...} +
+ + + +

Usage:

+
    +
    --> {a=1, b=2, c=3}
    +invert {"a", "b", "c"}
    +
+ +
+
+ + keys (t) +
+
+ Make the list of keys in table. + + +

Parameters:

+
    +
  • t + table + a table +
  • +
+ +

Returns:

+
    + + table + list of keys from t +
+ + +

See also:

+ + +

Usage:

+
    +
    globals = keys (_G)
    +
+ +
+
+ + len (t) +
+
+ Equivalent to # operation, but respecting __len even on Lua 5.1. + + +

Parameters:

+
    +
  • t + table + a table +
  • +
+ +

Returns:

+
    + + int + length of list part of t +
+ + + +

Usage:

+
    +
    for i = 1, len (t) do process (t[i]) end
    +
+ +
+
+ + maxn (t) +
+
+ Largest integer key in a table. + + +

Parameters:

+
    +
  • t + table + a table +
  • +
+ +

Returns:

+
    + + int + largest integer key in t +
+ + + +

Usage:

+
    +
    --> 42
    +maxn {"a", b="c", 99, [42]="x", "x", [5]=67}
    +
+ +
+
+ + merge (t, u[, map={}[, nometa]]) +
+
+ Destructively merge another table's fields into another. + + +

Parameters:

+
    +
  • t + table + destination table +
  • +
  • u + table + table with fields to merge +
  • +
  • map + table + table of {old_key=new_key, ...} + (default {}) +
  • +
  • nometa + bool + if true or ":nometa" don't copy metatable + (optional) +
  • +
+ +

Returns:

+
    + + table + t with fields from u merged in +
+ + +

See also:

+ + +

Usage:

+
    +
    merge (_G, require "std.debug", {say = "log"}, ":nometa")
    +
+ +
+
+ + merge_select (t, u[, keys={}[, nometa]]) +
+
+ Destructively merge another table's named fields into table.

+ +

Like merge, but does not merge any fields by default. + + +

Parameters:

+
    +
  • t + table + destination table +
  • +
  • u + table + table with fields to merge +
  • +
  • keys + table + list of keys to copy + (default {}) +
  • +
  • nometa + bool + if true or ":nometa" don't copy metatable + (optional) +
  • +
+ +

Returns:

+
    + + table + copy of fields in selection from t, also sharing t's + metatable unless nometa +
+ + +

See also:

+ + +

Usage:

+
    +
    merge_select (_G, require "std.debug", {"say"}, false)
    +
+ +
+
+ + monkey_patch ([namespace=_G]) +
+
+ Overwrite core table methods with std enhanced versions. + + +

Parameters:

+
    +
  • namespace + table + where to install global functions + (default _G) +
  • +
+ +

Returns:

+
    + + table + the module table +
+ + + +

Usage:

+
    +
    local table = require "std.table".monkey_patch ()
    +
+ +
+
+ + new ([x=nil[, t={}]]) +
+
+ Make a table with a default value for unset keys. + + +

Parameters:

+
    +
  • x + default entry value + (default nil) +
  • +
  • t + table + initial table + (default {}) +
  • +
+ +

Returns:

+
    + + table + table whose unset elements are x +
+ + + +

Usage:

+
    +
    t = new (0)
    +
+ +
+
+ + okeys (t) +
+
+ Make an ordered list of keys in table. + + +

Parameters:

+
    +
  • t + table + a table +
  • +
+ +

Returns:

+
    + + table + ordered list of keys from t +
+ + +

See also:

+ + +

Usage:

+
    +
    globals = keys (_G)
    +
+ +
+
+ + pack (...) +
+
+ Turn a tuple into a list. + + +

Parameters:

+
    +
  • ... + tuple +
  • +
+ +

Returns:

+
    + + list +
+ + + +

Usage:

+
    +
    --> {1, 2, "ax"}
    +pack (("ax1"):find "(%D+)")
    +
+ +
+
+ + project (fkey, tt) +
+
+ Project a list of fields from a list of tables. + + +

Parameters:

+
    +
  • fkey + field to project +
  • +
  • tt + table + a list of tables +
  • +
+ +

Returns:

+
    + + table + list of fkey fields from tt +
+ + + +

Usage:

+
    +
    --> {1, 3, "yy"}
    +project ("xx", {{"a", xx=1, yy="z"}, {"b", yy=2}, {"c", xx=3}, {xx="yy"})
    +
+ +
+
+ + remove (t[, pos=len (t)]) +
+
+ Enhance core table.remove to respect __len when pos is omitted. + Also, diagnose out of bounds pos arguments consistently on any supported + version of Lua. + + +

Parameters:

+
    +
  • t + table + a table +
  • +
  • pos + int + index from which to remove an element + (default len (t)) +
  • +
+ +

Returns:

+
    + + removed value, or else nil +
+ + + +

Usage:

+
    +
    --> {1, 2, 5}
    +t = {1, 2, "x", 5}
    +remove (t, 3) == "x" and t
    +
+ +
+
+ + shape (dims, t) +
+
+ Shape a table according to a list of dimensions.

+ +

Dimensions are given outermost first and items from the original + list are distributed breadth first; there may be one 0 indicating + an indefinite number. Hence, {0} is a flat list, + {1} is a singleton, {2, 0} is a list of + two lists, and {0, 2} is a list of pairs.

+ +

Algorithm: turn shape into all positive numbers, calculating + the zero if necessary and making sure there is at most one; + recursively walk the shape, adding empty tables until the bottom + level is reached at which point add table items instead, using a + counter to walk the flattened original list. + + +

Parameters:

+
    +
  • dims + table + table of dimensions {d1, ..., dn} +
  • +
  • t + table + a table of elements +
  • +
+ +

Returns:

+
    + + reshaped list +
+ + + +

Usage:

+
    +
    --> {{"a", "b"}, {"c", "d"}, {"e", "f"}}
    +shape ({3, 2}, {"a", "b", "c", "d", "e", "f"})
    +
+ +
+
+ + size (t) +
+
+ Find the number of elements in a table. + + +

Parameters:

+
    +
  • t + table + any table +
  • +
+ +

Returns:

+
    + + int + number of non-nil values in t +
+ + + +

Usage:

+
    +
    --> 3
    +size {foo = true, bar = true, baz = false}
    +
+ +
+
+ + sort (t[, c=std.operator.lt]) +
+
+ Enhance core table.sort to return its result. + + +

Parameters:

+
    +
  • t + table + unsorted table +
  • +
  • c + comparator + ordering function callback + (default std.operator.lt) +
  • +
+ +

Returns:

+
    + + t with keys sorted accordind to c +
+ + + +

Usage:

+
    +
    table.concat (sort (object))
    +
+ +
+
+ + unpack (t[, i=1[, j=table.maxn(t)]]) +
+
+ Enhance core table.unpack to always unpack up to maxn (t). + + +

Parameters:

+
    +
  • t + table + table to act on +
  • +
  • i + int + first index to unpack + (default 1) +
  • +
  • j + int + last index to unpack + (default table.maxn(t)) +
  • +
+ +

Returns:

+
    + + ... values of numeric indices of t +
+ + + +

Usage:

+
    +
    return unpack (results_table)
    +
+ +
+
+ + values (t) +
+
+ Make the list of values of a table. + + +

Parameters:

+
    +
  • t + table + any table +
  • +
+ +

Returns:

+
    + + table + list of values in t +
+ + +

See also:

+ + +

Usage:

+
    +
    --> {"a", "c", 42}
    +values {"a", b="c", [-1]=42}
    +
+ +
+
+

Types

+ +
+
+ + comparator (a, b) +
+
+ Signature of a sort comparator function. + + +

Parameters:

+
    +
  • a + any object +
  • +
  • b + any object +
  • +
+ +

Returns:

+
    + + boolean + true if a sorts before b, otherwise false +
+ + +

See also:

+ + +

Usage:

+
    +
    local reversor = function (a, b) return a > b end
    +sort (t, reversor)
    +
+ +
+
+ + +
+
+
+generated by LDoc 1.4.6 +Last updated 2018-09-16 19:20:25 +
+
+ + diff --git a/lib/std.lua b/lib/std.lua new file mode 100644 index 0000000..31b1311 --- /dev/null +++ b/lib/std.lua @@ -0,0 +1,304 @@ +--[[-- + Lua Standard Libraries. + + This module contains a selection of improved Lua core functions, among + others. + + Also, after requiring this module, simply referencing symbols in the + submodule hierarchy will load the necessary modules on demand. + + By default there are no changes to any global symbols, or monkey + patching of core module tables and metatables. However, sometimes it's + still convenient to do that: For example, when using stdlib from the + REPL, or in a prototype where you want to throw caution to the wind and + compatibility with other modules be damned. In that case, you can give + `stdlib` permission to scribble all over your namespaces by using the + various `monkey_patch` calls in the library. + + @todo Write a style guide (indenting/wrapping, capitalisation, + function and variable names); library functions should call + error, not die; OO vs non-OO (a thorny problem). + @todo pre-compile. + @module std +]] + + +local base = require "std.base" + +local M, monkeys + + +local function monkey_patch (namespace) + base.copy (namespace or _G, monkeys) + return M +end + + +local function barrel (namespace) + namespace = namespace or _G + + -- Older releases installed the following into _G by default. + for _, name in pairs { + "functional.bind", "functional.collect", "functional.compose", + "functional.curry", "functional.filter", "functional.id", + "functional.map", + + "io.die", "io.warn", + + "string.pickle", "string.prettytostring", "string.render", + + "table.pack", + + "tree.ileaves", "tree.inodes", "tree.leaves", "tree.nodes", + } do + local module, method = name:match "^(.*)%.(.-)$" + namespace[method] = M[module][method] + end + + -- Support old api names, for backwards compatibility. + namespace.fold = M.functional.fold + namespace.metamethod = M.getmetamethod + namespace.op = M.operator + namespace.require_version = M.require + + require "std.io".monkey_patch (namespace) + require "std.math".monkey_patch (namespace) + require "std.string".monkey_patch (namespace) + require "std.table".monkey_patch (namespace) + + return monkey_patch (namespace) +end + + + +--- Module table. +-- +-- In addition to the functions documented on this page, and a `version` +-- field, references to other submodule functions will be loaded on +-- demand. +-- @table std +-- @field version release version string + +local function X (decl, fn) + return require "std.debug".argscheck ("std." .. decl, fn) +end + +M = { + --- Enhance core `assert` to also allow formatted arguments. + -- @function assert + -- @param expect expression, expected to be *truthy* + -- @string[opt=""] f format string + -- @param[opt] ... arguments to format + -- @return value of *expect*, if *truthy* + -- @usage + -- std.assert (expected ~= nil, "100% unexpected!") + -- std.assert (expected ~= nil, "%s unexpected!", expected) + assert = X ("assert (?any, ?string, [any...])", base.assert), + + --- A [barrel of monkey_patches](http://dictionary.reference.com/browse/barrel+of+monkeys). + -- + -- Apply **all** `monkey_patch` functions. Additionally, for backwards + -- compatibility only, write a selection of sub-module functions into + -- the given namespace. + -- @function barrel + -- @tparam[opt=_G] table namespace where to install global functions + -- @treturn table module table + -- @usage local std = require "std".barrel () + barrel = X ("barrel (?table)", barrel), + + --- An iterator over all elements of a sequence. + -- If *t* has a `__pairs` metamethod, use that to iterate. + -- @function elems + -- @tparam table t a table + -- @treturn function iterator function + -- @treturn table *t*, the table being iterated over + -- @return *key*, the previous iteration key + -- @see ielems + -- @see pairs + -- @usage + -- for value in std.elems {a = 1, b = 2, c = 5} do process (value) end + elems = X ("elems (table)", base.elems), + + --- Evaluate a string as Lua code. + -- @function eval + -- @string s string of Lua code + -- @return result of evaluating `s` + -- @usage std.eval "math.min (2, 10)" + eval = X ("eval (string)", base.eval), + + --- An iterator over the integer keyed elements of a sequence. + -- If *t* has a `__len` metamethod, iterate up to the index it returns. + -- @function ielems + -- @tparam table t a table + -- @treturn function iterator function + -- @treturn table *t*, the table being iterated over + -- @treturn int *index*, the previous iteration index + -- @see elems + -- @see ipairs + -- @usage + -- for v in std.ielems {"a", "b", "c"} do process (v) end + ielems = X ("ielems (table)", base.ielems), + + --- An iterator over elements of a sequence, until the first `nil` value. + -- + -- Like Lua 5.1 and 5.3, but unlike Lua 5.2 (which looks for and uses the + -- `__ipairs` metamethod), this iterator returns successive key-value + -- pairs with integer keys starting at 1, up to the first `nil` valued + -- pair. + -- @function ipairs + -- @tparam table t a table + -- @treturn function iterator function + -- @treturn table *t*, the table being iterated over + -- @treturn int *index*, the previous iteration index + -- @see ielems + -- @see npairs + -- @see pairs + -- @usage + -- -- length of sequence + -- args = {"first", "second", nil, "last"} + -- --> 1=first + -- --> 2=second + -- for i, v in std.ipairs (args) do + -- print (string.format ("%d=%s", i, v)) + -- end + ipairs = X ("ipairs (table)", base.ipairs), + + --- Return a new table with element order reversed. + -- Apart from the order of the elments returned, this function follows + -- the same rules as @{ipairs} for determining first and last elements. + -- @function ireverse + -- @tparam table t a table + -- @treturn table a new table with integer keyed elements in reverse + -- order with respect to *t* + -- @see ielems + -- @see ipairs + -- @usage + -- local rielems = std.functional.compose (std.ireverse, std.ielems) + -- for e in rielems (l) do process (e) end + ireverse = X ("ireverse (table)", base.ireverse), + + --- Return named metamethod, if any, otherwise `nil`. + -- @function getmetamethod + -- @param x item to act on + -- @string n name of metamethod to lookup + -- @treturn function|nil metamethod function, or `nil` if no metamethod + -- @usage lookup = std.getmetamethod (require "std.object", "__index") + getmetamethod = X ("getmetamethod (?any, string)", base.getmetamethod), + + --- Overwrite core methods and metamethods with `std` enhanced versions. + -- + -- Write all functions from this module, except `std.barrel` and + -- `std.monkey_patch`, into the given namespace. + -- @function monkey_patch + -- @tparam[opt=_G] table namespace where to install global functions + -- @treturn table the module table + -- @usage local std = require "std".monkey_patch () + monkey_patch = X ("monkey_patch (?table)", monkey_patch), + + --- Ordered iterator for integer keyed values. + -- Like ipairs, but does not stop until the largest integer key. + -- @function npairs + -- @tparam table t a table + -- @treturn function iterator function + -- @treturn table t + -- @see ipairs + -- @see rnpairs + -- @usage + -- for i,v in npairs {"one", nil, "three"} do ... end + npairs = X ("npairs (table)", base.npairs), + + --- Enhance core `pairs` to respect `__pairs` even in Lua 5.1. + -- @function pairs + -- @tparam table t a table + -- @treturn function iterator function + -- @treturn table *t*, the table being iterated over + -- @return *key*, the previous iteration key + -- @see elems + -- @see ipairs + -- @usage + -- for k, v in pairs {"a", b = "c", foo = 42} do process (k, v) end + pairs = X ("pairs (table)", base.pairs), + + --- Enhance core `require` to assert version number compatibility. + -- By default match against the last substring of (dot-delimited) + -- digits in the module version string. + -- @function require + -- @string module module to require + -- @string[opt] min lowest acceptable version + -- @string[opt] too_big lowest version that is too big + -- @string[opt] pattern to match version in `module.version` or + -- `module._VERSION` (default: `"([%.%d]+)%D*$"`) + -- @usage + -- -- posix.version == "posix library for Lua 5.2 / 32" + -- posix = require ("posix", "29") + require = X ("require (string, ?string, ?string, ?string)", base.require), + + --- An iterator like ipairs, but in reverse. + -- Apart from the order of the elments returned, this function follows + -- the same rules as @{ipairs} for determining first and last elements. + -- @function ripairs + -- @tparam table t any table + -- @treturn function iterator function + -- @treturn table *t* + -- @treturn number `#t + 1` + -- @see ipairs + -- @see rnpairs + -- @usage for i, v = ripairs (t) do ... end + ripairs = X ("ripairs (table)", base.ripairs), + + --- An iterator like npairs, but in reverse. + -- Apart from the order of the elments returned, this function follows + -- the same rules as @{npairs} for determining first and last elements. + -- @function rnpairs + -- @tparam table t a table + -- @treturn function iterator function + -- @treturn table t + -- @see npairs + -- @see ripairs + -- @usage + -- for i,v in rnpairs {"one", nil, "three"} do ... end + rnpairs = X ("rnpairs (table)", base.rnpairs), + + --- Enhance core `tostring` to render table contents as a string. + -- @function tostring + -- @param x object to convert to string + -- @treturn string compact string rendering of *x* + -- @usage + -- -- {1=baz,foo=bar} + -- print (std.tostring {foo="bar","baz"}) + tostring = X ("tostring (?any)", base.tostring), + + version = "General Lua libraries / 41.2.2", +} + + +monkeys = base.copy ({}, M) + +-- Don't monkey_patch these apis into _G! +for _, api in ipairs {"barrel", "monkey_patch", "version"} do + monkeys[api] = nil +end + + +--- Metamethods +-- @section Metamethods + +return setmetatable (M, { + --- Lazy loading of stdlib modules. + -- Don't load everything on initial startup, wait until first attempt + -- to access a submodule, and then load it on demand. + -- @function __index + -- @string name submodule name + -- @treturn table|nil the submodule that was loaded to satisfy the missing + -- `name`, otherwise `nil` if nothing was found + -- @usage + -- local std = require "std" + -- local prototype = std.object.prototype + __index = function (self, name) + local ok, t = pcall (require, "std." .. name) + if ok then + rawset (self, name, t) + return t + end + end, +}) diff --git a/lib/std.lua.in b/lib/std.lua.in new file mode 100644 index 0000000..925a750 --- /dev/null +++ b/lib/std.lua.in @@ -0,0 +1,304 @@ +--[[-- + Lua Standard Libraries. + + This module contains a selection of improved Lua core functions, among + others. + + Also, after requiring this module, simply referencing symbols in the + submodule hierarchy will load the necessary modules on demand. + + By default there are no changes to any global symbols, or monkey + patching of core module tables and metatables. However, sometimes it's + still convenient to do that: For example, when using stdlib from the + REPL, or in a prototype where you want to throw caution to the wind and + compatibility with other modules be damned. In that case, you can give + `stdlib` permission to scribble all over your namespaces by using the + various `monkey_patch` calls in the library. + + @todo Write a style guide (indenting/wrapping, capitalisation, + function and variable names); library functions should call + error, not die; OO vs non-OO (a thorny problem). + @todo pre-compile. + @module std +]] + + +local base = require "std.base" + +local M, monkeys + + +local function monkey_patch (namespace) + base.copy (namespace or _G, monkeys) + return M +end + + +local function barrel (namespace) + namespace = namespace or _G + + -- Older releases installed the following into _G by default. + for _, name in pairs { + "functional.bind", "functional.collect", "functional.compose", + "functional.curry", "functional.filter", "functional.id", + "functional.map", + + "io.die", "io.warn", + + "string.pickle", "string.prettytostring", "string.render", + + "table.pack", + + "tree.ileaves", "tree.inodes", "tree.leaves", "tree.nodes", + } do + local module, method = name:match "^(.*)%.(.-)$" + namespace[method] = M[module][method] + end + + -- Support old api names, for backwards compatibility. + namespace.fold = M.functional.fold + namespace.metamethod = M.getmetamethod + namespace.op = M.operator + namespace.require_version = M.require + + require "std.io".monkey_patch (namespace) + require "std.math".monkey_patch (namespace) + require "std.string".monkey_patch (namespace) + require "std.table".monkey_patch (namespace) + + return monkey_patch (namespace) +end + + + +--- Module table. +-- +-- In addition to the functions documented on this page, and a `version` +-- field, references to other submodule functions will be loaded on +-- demand. +-- @table std +-- @field version release version string + +local function X (decl, fn) + return require "std.debug".argscheck ("std." .. decl, fn) +end + +M = { + --- Enhance core `assert` to also allow formatted arguments. + -- @function assert + -- @param expect expression, expected to be *truthy* + -- @string[opt=""] f format string + -- @param[opt] ... arguments to format + -- @return value of *expect*, if *truthy* + -- @usage + -- std.assert (expected ~= nil, "100% unexpected!") + -- std.assert (expected ~= nil, "%s unexpected!", expected) + assert = X ("assert (?any, ?string, [any...])", base.assert), + + --- A [barrel of monkey_patches](http://dictionary.reference.com/browse/barrel+of+monkeys). + -- + -- Apply **all** `monkey_patch` functions. Additionally, for backwards + -- compatibility only, write a selection of sub-module functions into + -- the given namespace. + -- @function barrel + -- @tparam[opt=_G] table namespace where to install global functions + -- @treturn table module table + -- @usage local std = require "std".barrel () + barrel = X ("barrel (?table)", barrel), + + --- An iterator over all elements of a sequence. + -- If *t* has a `__pairs` metamethod, use that to iterate. + -- @function elems + -- @tparam table t a table + -- @treturn function iterator function + -- @treturn table *t*, the table being iterated over + -- @return *key*, the previous iteration key + -- @see ielems + -- @see pairs + -- @usage + -- for value in std.elems {a = 1, b = 2, c = 5} do process (value) end + elems = X ("elems (table)", base.elems), + + --- Evaluate a string as Lua code. + -- @function eval + -- @string s string of Lua code + -- @return result of evaluating `s` + -- @usage std.eval "math.min (2, 10)" + eval = X ("eval (string)", base.eval), + + --- An iterator over the integer keyed elements of a sequence. + -- If *t* has a `__len` metamethod, iterate up to the index it returns. + -- @function ielems + -- @tparam table t a table + -- @treturn function iterator function + -- @treturn table *t*, the table being iterated over + -- @treturn int *index*, the previous iteration index + -- @see elems + -- @see ipairs + -- @usage + -- for v in std.ielems {"a", "b", "c"} do process (v) end + ielems = X ("ielems (table)", base.ielems), + + --- An iterator over elements of a sequence, until the first `nil` value. + -- + -- Like Lua 5.1 and 5.3, but unlike Lua 5.2 (which looks for and uses the + -- `__ipairs` metamethod), this iterator returns successive key-value + -- pairs with integer keys starting at 1, up to the first `nil` valued + -- pair. + -- @function ipairs + -- @tparam table t a table + -- @treturn function iterator function + -- @treturn table *t*, the table being iterated over + -- @treturn int *index*, the previous iteration index + -- @see ielems + -- @see npairs + -- @see pairs + -- @usage + -- -- length of sequence + -- args = {"first", "second", nil, "last"} + -- --> 1=first + -- --> 2=second + -- for i, v in std.ipairs (args) do + -- print (string.format ("%d=%s", i, v)) + -- end + ipairs = X ("ipairs (table)", base.ipairs), + + --- Return a new table with element order reversed. + -- Apart from the order of the elments returned, this function follows + -- the same rules as @{ipairs} for determining first and last elements. + -- @function ireverse + -- @tparam table t a table + -- @treturn table a new table with integer keyed elements in reverse + -- order with respect to *t* + -- @see ielems + -- @see ipairs + -- @usage + -- local rielems = std.functional.compose (std.ireverse, std.ielems) + -- for e in rielems (l) do process (e) end + ireverse = X ("ireverse (table)", base.ireverse), + + --- Return named metamethod, if any, otherwise `nil`. + -- @function getmetamethod + -- @param x item to act on + -- @string n name of metamethod to lookup + -- @treturn function|nil metamethod function, or `nil` if no metamethod + -- @usage lookup = std.getmetamethod (require "std.object", "__index") + getmetamethod = X ("getmetamethod (?any, string)", base.getmetamethod), + + --- Overwrite core methods and metamethods with `std` enhanced versions. + -- + -- Write all functions from this module, except `std.barrel` and + -- `std.monkey_patch`, into the given namespace. + -- @function monkey_patch + -- @tparam[opt=_G] table namespace where to install global functions + -- @treturn table the module table + -- @usage local std = require "std".monkey_patch () + monkey_patch = X ("monkey_patch (?table)", monkey_patch), + + --- Ordered iterator for integer keyed values. + -- Like ipairs, but does not stop until the largest integer key. + -- @function npairs + -- @tparam table t a table + -- @treturn function iterator function + -- @treturn table t + -- @see ipairs + -- @see rnpairs + -- @usage + -- for i,v in npairs {"one", nil, "three"} do ... end + npairs = X ("npairs (table)", base.npairs), + + --- Enhance core `pairs` to respect `__pairs` even in Lua 5.1. + -- @function pairs + -- @tparam table t a table + -- @treturn function iterator function + -- @treturn table *t*, the table being iterated over + -- @return *key*, the previous iteration key + -- @see elems + -- @see ipairs + -- @usage + -- for k, v in pairs {"a", b = "c", foo = 42} do process (k, v) end + pairs = X ("pairs (table)", base.pairs), + + --- Enhance core `require` to assert version number compatibility. + -- By default match against the last substring of (dot-delimited) + -- digits in the module version string. + -- @function require + -- @string module module to require + -- @string[opt] min lowest acceptable version + -- @string[opt] too_big lowest version that is too big + -- @string[opt] pattern to match version in `module.version` or + -- `module._VERSION` (default: `"([%.%d]+)%D*$"`) + -- @usage + -- -- posix.version == "posix library for Lua 5.2 / 32" + -- posix = require ("posix", "29") + require = X ("require (string, ?string, ?string, ?string)", base.require), + + --- An iterator like ipairs, but in reverse. + -- Apart from the order of the elments returned, this function follows + -- the same rules as @{ipairs} for determining first and last elements. + -- @function ripairs + -- @tparam table t any table + -- @treturn function iterator function + -- @treturn table *t* + -- @treturn number `#t + 1` + -- @see ipairs + -- @see rnpairs + -- @usage for i, v = ripairs (t) do ... end + ripairs = X ("ripairs (table)", base.ripairs), + + --- An iterator like npairs, but in reverse. + -- Apart from the order of the elments returned, this function follows + -- the same rules as @{npairs} for determining first and last elements. + -- @function rnpairs + -- @tparam table t a table + -- @treturn function iterator function + -- @treturn table t + -- @see npairs + -- @see ripairs + -- @usage + -- for i,v in rnpairs {"one", nil, "three"} do ... end + rnpairs = X ("rnpairs (table)", base.rnpairs), + + --- Enhance core `tostring` to render table contents as a string. + -- @function tostring + -- @param x object to convert to string + -- @treturn string compact string rendering of *x* + -- @usage + -- -- {1=baz,foo=bar} + -- print (std.tostring {foo="bar","baz"}) + tostring = X ("tostring (?any)", base.tostring), + + version = "General Lua libraries / @VERSION@", +} + + +monkeys = base.copy ({}, M) + +-- Don't monkey_patch these apis into _G! +for _, api in ipairs {"barrel", "monkey_patch", "version"} do + monkeys[api] = nil +end + + +--- Metamethods +-- @section Metamethods + +return setmetatable (M, { + --- Lazy loading of stdlib modules. + -- Don't load everything on initial startup, wait until first attempt + -- to access a submodule, and then load it on demand. + -- @function __index + -- @string name submodule name + -- @treturn table|nil the submodule that was loaded to satisfy the missing + -- `name`, otherwise `nil` if nothing was found + -- @usage + -- local std = require "std" + -- local prototype = std.object.prototype + __index = function (self, name) + local ok, t = pcall (require, "std." .. name) + if ok then + rawset (self, name, t) + return t + end + end, +}) diff --git a/lib/std/base.lua b/lib/std/base.lua new file mode 100644 index 0000000..5f2139f --- /dev/null +++ b/lib/std/base.lua @@ -0,0 +1,499 @@ +--[[-- + Prevent dependency loops with key function implementations. + + A few key functions are used in several stdlib modules; we implement those + functions in this internal module to prevent dependency loops in the first + instance, and to minimise coupling between modules where the use of one of + these functions might otherwise load a whole selection of other supporting + modules unnecessarily. + + Although the implementations are here for logistical reasons, we re-export + them from their respective logical modules so that the api is not affected + as far as client code is concerned. The functions in this file do not make + use of `argcheck` or similar, because we know that they are only called by + other stdlib functions which have already performed the necessary checking + and neither do we want to slow everything down by recheckng those argument + types here. + + This implies that when re-exporting from another module when argument type + checking is in force, we must export a wrapper function that can check the + user's arguments fully at the API boundary. + + @module std.base +]] + + +local dirsep = string.match (package.config, "^(%S+)\n") +local loadstring = rawget (_G, "loadstring") or load + + +local function raise (bad, to, name, i, extramsg, level) + level = level or 1 + local s = string.format ("bad %s #%d %s '%s'", bad, i, to, name) + if extramsg ~= nil then + s = s .. " (" .. extramsg .. ")" + end + error (s, level + 1) +end + + +local function argerror (name, i, extramsg, level) + level = level or 1 + raise ("argument", "to", name, i, extramsg, level + 1) +end + + +local function assert (expect, fmt, arg1, ...) + local msg = (arg1 ~= nil) and string.format (fmt, arg1, ...) or fmt or "" + return expect or error (msg, 2) +end + + +local function getmetamethod (x, n) + local _, m = pcall (function (x) + return getmetatable (x)[n] + end, + x) + if type (m) ~= "function" then + m = nil + end + return m +end + + +local function callable (x) + if type (x) == "function" then return x end + return getmetamethod (x, "__call") +end + + +local function catfile (...) + return table.concat ({...}, dirsep) +end + + +-- Lua < 5.2 doesn't call `__len` automatically! +local function len (t) + local m = getmetamethod (t, "__len") + return m and m (t) or #t +end + + +-- Iterate over keys 1..n, where n is the key before the first nil +-- valued ordinal key (like Lua 5.3). +local function ipairs (l) + return function (l, n) + n = n + 1 + if l[n] ~= nil then + return n, l[n] + end + end, l, 0 +end + + +local _pairs = pairs + +local maxn = table.maxn or function (t) + local n = 0 + for k in _pairs (t) do + if type (k) == "number" and k > n then n = k end + end + return n +end + + +local _unpack = table.unpack or unpack + +local function unpack (t, i, j) + return _unpack (t, i or 1, j or maxn (t)) +end + + +local function compare (l, m) + local lenl, lenm = len (l), len (m) + for i = 1, math.min (lenl, lenm) do + local li, mi = tonumber (l[i]), tonumber (m[i]) + if li == nil or mi == nil then + li, mi = l[i], m[i] + end + if li < mi then + return -1 + elseif li > mi then + return 1 + end + end + if lenl < lenm then + return -1 + elseif lenl > lenm then + return 1 + end + return 0 +end + + +local _pairs = pairs + +-- Respect __pairs metamethod, even in Lua 5.1. +local function pairs (t) + return (getmetamethod (t, "__pairs") or _pairs) (t) +end + + +local function copy (dest, src) + if src == nil then dest, src = {}, dest end + for k, v in pairs (src) do dest[k] = v end + return dest +end + + +--- Iterator adaptor for discarding first value from core iterator function. +-- @func factory iterator to be wrapped +-- @param ... *factory* arguments +-- @treturn function iterator that discards first returned value of +-- factory iterator +-- @return invariant state from *factory* +-- @return `true` +-- @usage +-- for v in wrapiterator (ipairs {"a", "b", "c"}) do process (v) end +local function wrapiterator (factory, ...) + -- Capture wrapped ctrl variable into an upvalue... + local fn, istate, ctrl = factory (...) + -- Wrap the returned iterator fn to maintain wrapped ctrl. + return function (state, _) + local v + ctrl, v = fn (state, ctrl) + if ctrl then return v end + end, istate, true -- wrapped initial state, and wrapper ctrl +end + + +local function elems (t) + return wrapiterator (pairs, t) +end + + +local function escape_pattern (s) + return (s:gsub ("[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0")) +end + + +local function eval (s) + return loadstring ("return " .. s)() +end + + +local function ielems (l) + return wrapiterator (ipairs, l) +end + + +local _insert = table.insert + +local function insert (t, pos, v) + if v == nil then pos, v = len (t) + 1, pos end + if pos < 1 or pos > len (t) + 1 then + argerror ("std.table.insert", 2, "position " .. pos .. " out of bounds", 2) + end + _insert (t, pos, v) + return t +end + + +local function invert (t) + local i = {} + for k, v in pairs (t) do + i[v] = k + end + return i +end + + +-- Be careful to reverse only the valid sequence part of a table. +local function ireverse (t) + local oob = 1 + while t[oob] ~= nil do + oob = oob + 1 + end + + local r = {} + for i = 1, oob - 1 do r[oob - i] = t[i] end + return r +end + + +-- Sort numbers first then asciibetically +local function keysort (a, b) + if type (a) == "number" then + return type (b) ~= "number" or a < b + else + return type (b) ~= "number" and tostring (a) < tostring (b) + end +end + + +local function okeys (t) + local r = {} + for k in pairs (t) do r[#r + 1] = k end + table.sort (r, keysort) + return r +end + + +local function last (t) return t[len (t)] end + + +local function leaves (it, tr) + local function visit (n) + if type (n) == "table" then + for _, v in it (n) do + visit (v) + end + else + coroutine.yield (n) + end + end + return coroutine.wrap (visit), tr +end + + +local function merge (dest, src) + for k, v in pairs (src) do dest[k] = dest[k] or v end + return dest +end + + +local function npairs (t) + local i, n = 0, maxn (t) + return function (t) + i = i + 1 + if i <= n then return i, t[i] end + end, + t, i +end + + +local function collect (ifn, ...) + local argt, r = {...}, {} + if not callable (ifn) then + ifn, argt = npairs, {ifn, ...} + end + + -- How many return values from ifn? + local arity = 1 + for e, v in ifn (unpack (argt)) do + if v then arity, r = 2, {} break end + -- Build an arity-1 result table on first pass... + r[#r + 1] = e + end + + if arity == 2 then + -- ...oops, it was arity-2 all along, start again! + for k, v in ifn (unpack (argt)) do + r[k] = v + end + end + + return r +end + + +local function prototype (o) + return (getmetatable (o) or {})._type or io.type (o) or type (o) +end + + +local function reduce (fn, d, ifn, ...) + local argt = {...} + if not callable (ifn) then + ifn, argt = pairs, {ifn, ...} + end + + local nextfn, state, k = ifn (unpack (argt)) + local t = {nextfn (state, k)} -- table of iteration 1 + + local r = d -- initialise accumulator + while t[1] ~= nil do -- until iterator returns nil + k = t[1] + r = fn (r, unpack (t)) -- pass all iterator results to fn + t = {nextfn (state, k)} -- maintain loop invariant + end + return r +end + + +-- Write pretty-printing based on: +-- +-- John Hughes's and Simon Peyton Jones's Pretty Printer Combinators +-- +-- Based on "The Design of a Pretty-printing Library in Advanced +-- Functional Programming", Johan Jeuring and Erik Meijer (eds), LNCS 925 +-- http://www.cs.chalmers.se/~rjmh/Papers/pretty.ps +-- Heavily modified by Simon Peyton Jones, Dec 96 + +local function render (x, opencb, closecb, elemcb, paircb, sepcb, roots) + roots = roots or {} + local function stop_roots (x) + return roots[x] or render (x, opencb, closecb, elemcb, paircb, sepcb, copy (roots)) + end + + if type (x) ~= "table" or getmetamethod (x, "__tostring") then + return elemcb (x) + else + local buf, k_, v_ = { opencb (x) } -- pre-buffer table open + roots[x] = elemcb (x) -- initialise recursion protection + + for _, k in ipairs (okeys (x)) do -- for ordered table members + local v = x[k] + buf[#buf + 1] = sepcb (x, k_, v_, k, v) -- | buffer separator + buf[#buf + 1] = paircb (x, k, v, stop_roots (k), stop_roots (v)) + -- | buffer key/value pair + k_, v_ = k, v + end + buf[#buf + 1] = sepcb (x, k_, v_) -- buffer trailing separator + buf[#buf + 1] = closecb (x) -- buffer table close + + return table.concat (buf) -- stringify buffer + end +end + + +local function ripairs (t) + local oob = 1 + while t[oob] ~= nil do + oob = oob + 1 + end + + return function (t, n) + n = n - 1 + if n > 0 then + return n, t[n] + end + end, t, oob +end + + +local function rnpairs (t) + local oob = maxn (t) + 1 + + return function (t, n) + n = n - 1 + if n > 0 then + return n, t[n] + end + end, t, oob +end + + +local function split (s, sep) + local r, patt = {} + if sep == "" then + patt = "(.)" + insert (r, "") + else + patt = "(.-)" .. (sep or "%s+") + end + local b, lens = 0, len (s) + while b <= lens do + local e, n, m = string.find (s, patt, b + 1) + insert (r, m or s:sub (b + 1, lens)) + b = n or lens + 1 + end + return r +end + + +local function vcompare (a, b) + return compare (split (a, "%."), split (b, "%.")) +end + + +local _require = require + +local function require (module, min, too_big, pattern) + local m = _require (module) + local v = tostring (m.version or m._VERSION or ""):match (pattern or "([%.%d]+)%D*$") + if min then + assert (vcompare (v, min) >= 0, "require '" .. module .. + "' with at least version " .. min .. ", but found version " .. v) + end + if too_big then + assert (vcompare (v, too_big) < 0, "require '" .. module .. + "' with version less than " .. too_big .. ", but found version " .. v) + end + return m +end + + +local _tostring = _G.tostring + +local function tostring (x) + return render (x, + function () return "{" end, + function () return "}" end, + _tostring, + function (_, _, _, is, vs) return is .."=".. vs end, + function (_, i, _, k) return i and k and "," or "" end) +end + + + +return { + copy = copy, + keysort = keysort, + merge = merge, + okeys = okeys, + raise = raise, + + -- std.lua -- + assert = assert, + eval = eval, + elems = elems, + ielems = ielems, + ipairs = ipairs, + ireverse = ireverse, + npairs = npairs, + pairs = pairs, + ripairs = ripairs, + rnpairs = rnpairs, + require = require, + tostring = tostring, + + -- debug.lua -- + argerror = argerror, + + -- functional.lua -- + callable = callable, + collect = collect, + nop = function () end, + reduce = reduce, + + -- io.lua -- + catfile = catfile, + + -- list.lua -- + compare = compare, + + -- object.lua -- + prototype = prototype, + + -- package.lua -- + dirsep = dirsep, + + -- string.lua -- + escape_pattern = escape_pattern, + render = render, + split = split, + + -- table.lua -- + getmetamethod = getmetamethod, + insert = insert, + invert = invert, + last = last, + len = len, + maxn = maxn, + unpack = unpack, + + -- tree.lua -- + leaves = leaves, + +} diff --git a/lib/std/container.lua b/lib/std/container.lua new file mode 100644 index 0000000..3136d4e --- /dev/null +++ b/lib/std/container.lua @@ -0,0 +1,306 @@ +--[[-- + Container prototype. + + A container is a @{std.object} with no methods. It's functionality is + instead defined by its *meta*methods. + + Where an Object uses the `__index` metatable entry to hold object + methods, a Container stores its contents using `__index`, preventing + it from having methods in there too. + + Although there are no actual methods, Containers are free to use + metamethods (`__index`, `__sub`, etc) and, like Objects, can supply + module functions by listing them in `_functions`. Also, since a + @{std.container} is a @{std.object}, it can be passed to the + @{std.object} module functions, or anywhere else a @{std.object} is + expected. + + When making your own prototypes, derive from @{std.container} if you want + to access the contents of your objects with the `[]` operator, or from + @{std.object} if you want to access the functionality of your objects with + named object methods. + + Prototype Chain + --------------- + + table + `-> Object + `-> Container + + @classmod std.container +]] + + +local _DEBUG = require "std.debug_init"._DEBUG + +local base = require "std.base" +local debug = require "std.debug" + +local ipairs, pairs, okeys = base.ipairs, base.pairs, base.okeys +local insert, len, maxn = base.insert, base.len, base.maxn +local okeys, prototype, tostring = base.okeys, base.prototype, base.tostring +local argcheck = debug.argcheck + + + +--[[ ================= ]]-- +--[[ Helper Functions. ]]-- +--[[ ================= ]]-- + + +-- Instantiate a new object based on *proto*. +-- +-- This is equivalent to: +-- +-- table.merge (table.clone (proto), t or {}) +-- +-- But, not typechecking arguments or checking for metatables, is +-- slightly faster. +-- @tparam table proto base object to copy from +-- @tparam[opt={}] table t additional fields to merge in +-- @treturn table a new table with fields from proto and t merged in. +local function instantiate (proto, t) + local obj = {} + local k, v = next (proto) + while k do + obj[k] = v + k, v = next (proto, k) + end + + t = t or {} + k, v = next (t) + while k do + obj[k] = v + k, v = next (t, k) + end + return obj +end + + +local ModuleFunction = { + __tostring = function (self) return tostring (self.call) end, + __call = function (self, ...) return self.call (...) end, +} + + +--- Mark a function not to be copied into clones. +-- +-- It responds to `type` with `table`, but otherwise behaves like a +-- regular function. Marking uncopied module functions in-situ like this +-- (as opposed to doing book keeping in the metatable) means that we +-- don't have to create a new metatable with the book keeping removed for +-- cloned objects, we can just share our existing metatable directly. +-- @func fn a function +-- @treturn functable a callable functable for `fn` +local function modulefunction (fn) + if getmetatable (fn) == ModuleFunction then + -- Don't double wrap! + return fn + else + return setmetatable ({_type = "modulefunction", call = fn}, ModuleFunction) + end +end + + + +--[[ ================= ]]-- +--[[ Container Object. ]]-- +--[[ ================= ]]-- + + +local function mapfields (obj, src, map) + local mt = getmetatable (obj) or {} + + -- Map key pairs. + -- Copy all pairs when `map == nil`, but discard unmapped src keys + -- when map is provided (i.e. if `map == {}`, copy nothing). + if map == nil or next (map) then + map = map or {} + local k, v = next (src) + while k do + local key, dst = map[k] or k, obj + local kind = type (key) + if kind == "string" and key:sub (1, 1) == "_" then + mt[key] = v + elseif next (map) and kind == "number" and len (dst) + 1 < key then + -- When map is given, but has fewer entries than src, stop copying + -- fields when map is exhausted. + break + else + dst[key] = v + end + k, v = next (src, k) + end + end + + -- Quicker to remove this after copying fields than test for it + -- it on every iteration above. + mt._functions = nil + + -- Inject module functions. + local t = src._functions or {} + local k, v = next (t) + while (k) do + obj[k] = modulefunction (v) + k, v = next (t, k) + end + + -- Only set non-empty metatable. + if next (mt) then + setmetatable (obj, mt) + end + return obj +end + + +local function __call (self, x, ...) + local mt = getmetatable (self) + local obj_mt = mt + local obj = {} + + -- This is the slowest part of cloning for any objects that have + -- a lot of fields to test and copy. If you need to clone a lot of + -- objects from a prototype with several module functions, it's much + -- faster to clone objects from each other than the prototype! + local k, v = next (self) + while (k) do + if type (v) ~= "table" or v._type ~= "modulefunction" then + obj[k] = v + end + k, v = next (self, k) + end + + if type (mt._init) == "function" then + obj = mt._init (obj, x, ...) + else + obj = (self.mapfields or mapfields) (obj, x, mt._init) + end + + -- If a metatable was set, then merge our fields and use it. + if next (getmetatable (obj) or {}) then + obj_mt = instantiate (mt, getmetatable (obj)) + + -- Merge object methods. + if type (obj_mt.__index) == "table" and + type ((mt or {}).__index) == "table" + then + obj_mt.__index = instantiate (mt.__index, obj_mt.__index) + end + end + + return setmetatable (obj, obj_mt) +end + + +local function X (decl, fn) + return debug.argscheck ("std.container." .. decl, fn) +end + +local M = { + mapfields = X ("mapfields (table, table|object, ?table)", mapfields), +} + + +if _DEBUG.argcheck then + + local argerror, extramsg_toomany = debug.argerror, debug.extramsg_toomany + + M.__call = function (self, x, ...) + local mt = getmetatable (self) + + -- A function initialised object can be passed arguments of any + -- type, so only argcheck non-function initialised objects. + if type (mt._init) ~= "function" then + local name, argt = mt._type, {...} + -- Don't count `self` as an argument for error messages, because + -- it just refers back to the object being called: `Container {"x"}. + argcheck (name, 1, "table", x) + if next (argt) then + argerror (name, 2, extramsg_toomany ("argument", 1, 1 + maxn (argt)), 2) + end + end + + return __call (self, x, ...) + end + +else + + M.__call = __call + +end + + +function M.__tostring (self) + local n, k_ = 1, nil + local buf = { prototype (self), " {" } -- pre-buffer object open + for _, k in ipairs (okeys (self)) do -- for ordered public members + local v = self[k] + + if k_ ~= nil then -- | buffer separator + if k ~= n and type (k_) == "number" and k_ == n - 1 then + -- `;` separates `v` elements from `k=v` elements + buf[#buf + 1] = "; " + elseif k ~= nil then + -- `,` separator everywhere else + buf[#buf + 1] = ", " + end + end + + if type (k) == "number" and k == n then -- | buffer key/value pair + -- render initial array-like elements as just `v` + buf[#buf + 1] = tostring (v) + n = n + 1 + else + -- render remaining elements as `k=v` + buf[#buf + 1] = tostring (k) .. "=" .. tostring (v) + end + + k_ = k -- maintain loop invariant: k_ is previous key + end + buf[#buf + 1] = "}" -- buffer object close + + return table.concat (buf) -- stringify buffer +end + + +--- Container prototype. +-- +-- Container also inherits all the fields and methods from +-- @{std.object.Object}. +-- @object Container +-- @string[opt="Container"] _type object name +-- @see std.object +-- @see std.object.__call +-- @usage +-- local std = require "std" +-- local Container = std.container {} +-- +-- local Graph = Container { +-- _type = "Graph", +-- _functions = { +-- nodes = function (graph) +-- local n = 0 +-- for _ in std.pairs (graph) do n = n + 1 end +-- return n +-- end, +-- }, +-- } +-- local g = Graph { "node1", "node2" } +-- --> 2 +-- print (Graph.nodes (g)) + +return setmetatable ({ + + -- Normally, these are set and wrapped automatically during cloning. + -- But, we have to bootstrap the first object, so in this one instance + -- it has to be done manually. + + mapfields = modulefunction (M.mapfields), + prototype = modulefunction (prototype), +}, { + _type = "Container", + + __call = M.__call, + __tostring = M.__tostring, + __pairs = M.__pairs, +}) diff --git a/lib/std/debug.lua b/lib/std/debug.lua new file mode 100644 index 0000000..9657846 --- /dev/null +++ b/lib/std/debug.lua @@ -0,0 +1,902 @@ +--[[-- + Additions to the core debug module. + + The module table returned by `std.debug` also contains all of the entries + from the core debug table. An hygienic way to import this module, then, is + simply to override the core `debug` locally: + + local debug = require "std.debug" + + The behaviour of the functions in this module are controlled by the value + of the global `_DEBUG`. Not setting `_DEBUG` prior to requiring **any** of + stdlib's modules is equivalent to having `_DEBUG = true`. + + The first line of Lua code in production quality projects that use stdlib + should be either: + + _DEBUG = false + + or alternatively, if you need to be careful not to damage the global + environment: + + local init = require "std.debug_init" + init._DEBUG = false + + This mitigates almost all of the overhead of argument typechecking in + stdlib API functions. + + @module std.debug +]] + + +local debug_init = require "std.debug_init" +local base = require "std.base" + +local _DEBUG = debug_init._DEBUG +local argerror, raise = base.argerror, base.raise +local prototype, unpack = base.prototype, base.unpack +local copy, split, tostring = base.copy, base.split, base.tostring +local insert, last, len, maxn = base.insert, base.last, base.len, base.maxn +local ipairs, pairs = base.ipairs, base.pairs + + +local M + + +-- Return a deprecation message if _DEBUG.deprecate is `nil`, otherwise "". +local function DEPRECATIONMSG (version, name, extramsg, level) + if level == nil then level, extramsg = extramsg, nil end + extramsg = extramsg or "and will be removed entirely in a future release" + + local _, where = pcall (function () error ("", level + 3) end) + if _DEBUG.deprecate == nil then + return (where .. string.format ("%s was deprecated in release %s, %s.\n", + name, tostring (version), extramsg)) + end + + return "" +end + + +-- Define deprecated functions when _DEBUG.deprecate is not "truthy", +-- and write `DEPRECATIONMSG` output to stderr. +local function DEPRECATED (version, name, extramsg, fn) + if fn == nil then fn, extramsg = extramsg, nil end + + if not _DEBUG.deprecate then + return function (...) + io.stderr:write (DEPRECATIONMSG (version, name, extramsg, 2)) + return fn (...) + end + end +end + + +local _setfenv = debug.setfenv + +local function setfenv (fn, env) + -- Unwrap functable: + if type (fn) == "table" then + fn = fn.call or (getmetatable (fn) or {}).__call + end + + if _setfenv then + return _setfenv (fn, env) + + else + -- From http://lua-users.org/lists/lua-l/2010-06/msg00313.html + local name + local up = 0 + repeat + up = up + 1 + name = debug.getupvalue (fn, up) + until name == '_ENV' or name == nil + if name then + debug.upvaluejoin (fn, up, function () return name end, 1) + debug.setupvalue (fn, up, env) + end + + return fn + end +end + + +local _getfenv = rawget (_G, "getfenv") + +local getfenv = function (fn) + fn = fn or 1 + + -- Unwrap functable: + if type (fn) == "table" then + fn = fn.call or (getmetatable (fn) or {}).__call + end + + if _getfenv then + if type (fn) == "number" then fn = fn + 1 end + + -- Stack frame count is critical here, so ensure we don't optimise one + -- away in LuaJIT... + return _getfenv (fn), nil + + else + if type (fn) == "number" then + fn = debug.getinfo (fn + 1, "f").func + end + + local name, env + local up = 0 + repeat + up = up + 1 + name, env = debug.getupvalue (fn, up) + until name == '_ENV' or name == nil + return env + end +end + + +local function resulterror (name, i, extramsg, level) + level = level or 1 + raise ("result", "from", name, i, extramsg, level + 1) +end + + +local function extramsg_toomany (bad, expected, actual) + local s = "no more than %d %s%s expected, got %d" + return s:format (expected, bad, expected == 1 and "" or "s", actual) +end + + +--- Strip trailing ellipsis from final argument if any, storing maximum +-- number of values that can be matched directly in `t.maxvalues`. +-- @tparam table t table to act on +-- @string v element added to *t*, to match against ... suffix +-- @treturn table *t* with ellipsis stripped and maxvalues field set +local function markdots (t, v) + return (v:gsub ("%.%.%.$", function () t.dots = true return "" end)) +end + + +--- Calculate permutations of type lists with and without [optionals]. +-- @tparam table t a list of expected types by argument position +-- @treturn table set of possible type lists +local function permute (t) + if t[#t] then t[#t] = t[#t]:gsub ("%]%.%.%.$", "...]") end + + local p = {{}} + for i, v in ipairs (t) do + local optional = v:match "%[(.+)%]" + + if optional == nil then + -- Append non-optional type-spec to each permutation. + for b = 1, #p do + insert (p[b], markdots (p[b], v)) + end + else + -- Duplicate all existing permutations, and add optional type-spec + -- to the unduplicated permutations. + local o = #p + for b = 1, o do + p[b + o] = copy (p[b]) + insert (p[b], markdots (p[b], optional)) + end + end + end + return p +end + + +local function typesplit (types) + if type (types) == "string" then + types = split (types:gsub ("%s+or%s+", "|"), "%s*|%s*") + end + local r, seen, add_nil = {}, {}, false + for _, v in ipairs (types) do + local m = v:match "^%?(.+)$" + if m then + add_nil, v = true, m + end + if not seen[v] then + r[#r + 1] = v + seen[v] = true + end + end + if add_nil then + r[#r + 1] = "nil" + end + return r +end + + +local function projectuniq (fkey, tt) + -- project + local t = {} + for _, u in ipairs (tt) do + t[#t + 1] = u[fkey] + end + + -- split and remove duplicates + local r, s = {}, {} + for _, e in ipairs (t) do + for _, v in ipairs (typesplit (e)) do + if s[v] == nil then + r[#r + 1], s[v] = v, true + end + end + end + return r +end + + +local function parsetypes (types) + local r, permutations = {}, permute (types) + for i = 1, #permutations[1] do + r[i] = projectuniq (i, permutations) + end + r.dots = permutations[1].dots + return r +end + + +--- Concatenate a table of strings using ", " and " or " delimiters. +-- @tparam table alternatives a table of strings +-- @treturn string string of elements from alternatives delimited by ", " +-- and " or " +local function concat (alternatives) + if len (alternatives) > 1 then + local t = copy (alternatives) + local top = table.remove (t) + t[#t] = t[#t] .. " or " .. top + alternatives = t + end + return table.concat (alternatives, ", ") +end + + +local function extramsg_mismatch (expectedtypes, actual, index) + local actualtype = prototype (actual) + + -- Tidy up actual type for display. + if actualtype == "nil" then + actualtype = "no value" + elseif actualtype == "string" and actual:sub (1, 1) == ":" then + actualtype = actual + elseif type (actual) == "table" and next (actual) == nil then + local matchstr = "," .. table.concat (expectedtypes, ",") .. "," + if actualtype == "table" and matchstr == ",#list," then + actualtype = "empty list" + elseif actualtype == "table" or matchstr:match ",#" then + actualtype = "empty " .. actualtype + end + end + + if index then + actualtype = actualtype .. " at index " .. tostring (index) + end + + -- Tidy up expected types for display. + local expectedstr = expectedtypes + if type (expectedtypes) == "table" then + local t = {} + for i, v in ipairs (expectedtypes) do + if v == "func" then + t[i] = "function" + elseif v == "bool" then + t[i] = "boolean" + elseif v == "any" then + t[i] = "any value" + elseif v == "file" then + t[i] = "FILE*" + elseif not index then + t[i] = v:match "(%S+) of %S+" or v + else + t[i] = v + end + end + expectedstr = (concat (t) .. " expected"): + gsub ("#table", "non-empty table"): + gsub ("#list", "non-empty list"): + gsub ("(%S+ of [^,%s]-)s? ", "%1s "): + gsub ("(%S+ of [^,%s]-)s?,", "%1s,"): + gsub ("(s, [^,%s]-)s? ", "%1s "): + gsub ("(s, [^,%s]-)s?,", "%1s,"): + gsub ("(of .-)s? or ([^,%s]-)s? ", "%1s or %2s ") + end + + return expectedstr .. ", got " .. actualtype +end + + +local argcheck, argscheck -- forward declarations + +if _DEBUG.argcheck then + + --- Return index of the first mismatch between types and values, or `nil`. + -- @tparam table typelist a list of expected types + -- @tparam table valuelist a table of arguments to compare + -- @treturn int|nil position of first mismatch in *typelist* + local function match (typelist, valuelist) + local n = #typelist + for i = 1, n do -- normal parameters + local ok = pcall (argcheck, "pcall", i, typelist[i], valuelist[i]) + if not ok then return i end + end + for i = n + 1, maxn (valuelist) do -- additional values against final type + local ok = pcall (argcheck, "pcall", i, typelist[n], valuelist[i]) + if not ok then return i end + end + end + + + --- Compare *check* against type of *actual* + -- @string check extended type name expected + -- @param actual object being typechecked + -- @treturn boolean `true` if *actual* is of type *check*, otherwise + -- `false` + local function checktype (check, actual) + if check == "any" and actual ~= nil then + return true + elseif check == "file" and io.type (actual) == "file" then + return true + end + + local actualtype = type (actual) + if check == actualtype then + return true + elseif check == "bool" and actualtype == "boolean" then + return true + elseif check == "#table" then + if actualtype == "table" and next (actual) then + return true + end + elseif check == "function" or check == "func" then + if actualtype == "function" or + (getmetatable (actual) or {}).__call ~= nil + then + return true + end + elseif check == "int" then + if actualtype == "number" and actual == math.floor (actual) then + return true + end + elseif type (check) == "string" and check:sub (1, 1) == ":" then + if check == actual then + return true + end + end + + actualtype = prototype (actual) + if check == actualtype then + return true + elseif check == "list" or check == "#list" then + if actualtype == "table" or actualtype == "List" then + local len, count = len (actual), 0 + local i = next (actual) + repeat + if i ~= nil then count = count + 1 end + i = next (actual, i) + until i == nil or count > len + if count == len and (check == "list" or count > 0) then + return true + end + end + elseif check == "object" then + if actualtype ~= "table" and type (actual) == "table" then + return true + end + end + + return false + end + + + local function empty (t) return not next (t) end + + -- Pattern to normalize: [types...] to [types]... + local last_pat = "^%[([^%]%.]+)%]?(%.*)%]?" + + --- Diagnose mismatches between *valuelist* and type *permutations*. + -- @tparam table valuelist list of actual values to be checked + -- @tparam table argt table of precalculated values and handler functiens + local function diagnose (valuelist, argt) + local permutations = argt.permutations + + local bestmismatch, t = 0 + for i, typelist in ipairs (permutations) do + local mismatch = match (typelist, valuelist) + if mismatch == nil then + bestmismatch, t = nil, nil + break -- every *valuelist* matched types from this *typelist* + elseif mismatch > bestmismatch then + bestmismatch, t = mismatch, permutations[i] + end + end + + if bestmismatch ~= nil then + -- Report an error for all possible types at bestmismatch index. + local i, expected = bestmismatch + if t.dots and i > #t then + expected = typesplit (t[#t]) + else + expected = projectuniq (i, permutations) + end + + -- This relies on the `permute()` algorithm leaving the longest + -- possible permutation (with dots if necessary) at permutations[1]. + local typelist = permutations[1] + + -- For "container of things", check all elements are a thing too. + if typelist[i] then + local check, contents = typelist[i]:match "^(%S+) of (%S-)s?$" + if contents and type (valuelist[i]) == "table" then + for k, v in pairs (valuelist[i]) do + if not checktype (contents, v) then + argt.badtype (i, extramsg_mismatch (expected, v, k), 3) + end + end + end + end + + -- Otherwise the argument type itself was mismatched. + if t.dots or #t >= maxn (valuelist) then + argt.badtype (i, extramsg_mismatch (expected, valuelist[i]), 3) + end + end + + local n, t = maxn (valuelist), t or permutations[1] + if t and t.dots == nil and n > #t then + argt.badtype (#t + 1, extramsg_toomany (argt.bad, #t, n), 3) + end + end + + + function argcheck (name, i, expected, actual, level) + level = level or 2 + expected = typesplit (expected) + + -- Check actual has one of the types from expected + local ok = false + for _, expect in ipairs (expected) do + local check, contents = expect:match "^(%S+) of (%S-)s?$" + check = check or expect + + -- Does the type of actual check out? + ok = checktype (check, actual) + + -- For "table of things", check all elements are a thing too. + if ok and contents and type (actual) == "table" then + for k, v in pairs (actual) do + if not checktype (contents, v) then + argerror (name, i, extramsg_mismatch (expected, v, k), level + 1) + end + end + end + if ok then break end + end + + if not ok then + argerror (name, i, extramsg_mismatch (expected, actual), level + 1) + end + end + + + -- Pattern to extract: fname ([types]?[, types]*) + local args_pat = "^%s*([%w_][%.%:%d%w_]*)%s*%(%s*(.*)%s*%)" + + function argscheck (decl, inner) + -- Parse "fname (argtype, argtype, argtype...)". + local fname, argtypes = decl:match (args_pat) + if argtypes == "" then + argtypes = {} + elseif argtypes then + argtypes = split (argtypes, "%s*,%s*") + else + fname = decl:match "^%s*([%w_][%.%:%d%w_]*)" + end + + -- Precalculate vtables once to make multiple calls faster. + local input, output = { + bad = "argument", + badtype = function (i, extramsg, level) + level = level or 1 + argerror (fname, i, extramsg, level + 1) + end, + permutations = permute (argtypes), + } + + -- Parse "... => returntype, returntype, returntype...". + local returntypes = decl:match "=>%s*(.+)%s*$" + if returntypes then + local i, permutations = 0, {} + for _, group in ipairs (split (returntypes, "%s+or%s+")) do + returntypes = split (group, ",%s*") + for _, t in ipairs (permute (returntypes)) do + i = i + 1 + permutations[i] = t + end + end + + -- Ensure the longest permutation is first in the list. + table.sort (permutations, function (a, b) return #a > #b end) + + output = { + bad = "result", + badtype = function (i, extramsg, level) + level = level or 1 + resulterror (fname, i, extramsg, level + 1) + end, + permutations = permutations, + } + end + + return function (...) + local argt = {...} + + -- Don't check type of self if fname has a ':' in it. + if fname:find (":") then table.remove (argt, 1) end + + -- Diagnose bad inputs. + diagnose (argt, input) + + -- Propagate outer environment to inner function. + local x = math.max -- ??? getfenv(1) fails if we remove this ??? + setfenv (inner, getfenv (1)) + + -- Execute. + local results = {inner (...)} + + -- Diagnose bad outputs. + if returntypes then + diagnose (results, output) + end + + return unpack (results, 1, maxn (results)) + end + end + +else + + -- Turn off argument checking if _DEBUG is false, or a table containing + -- a false valued `argcheck` field. + + argcheck = base.nop + argscheck = function (decl, inner) return inner end + +end + + +local function say (n, ...) + local level, argt = n, {...} + if type (n) ~= "number" then + level, argt = 1, {n, ...} + end + if _DEBUG.level ~= math.huge and + ((type (_DEBUG.level) == "number" and _DEBUG.level >= level) or level <= 1) + then + local t = {} + for k, v in pairs (argt) do t[k] = tostring (v) end + io.stderr:write (table.concat (t, "\t") .. "\n") + end +end + + +local level = 0 + +local function trace (event) + local t = debug.getinfo (3) + local s = " >>> " + for i = 1, level do s = s .. " " end + if t ~= nil and t.currentline >= 0 then + s = s .. t.short_src .. ":" .. t.currentline .. " " + end + t = debug.getinfo (2) + if event == "call" then + level = level + 1 + else + level = math.max (level - 1, 0) + end + if t.what == "main" then + if event == "call" then + s = s .. "begin " .. t.short_src + else + s = s .. "end " .. t.short_src + end + elseif t.what == "Lua" then + s = s .. event .. " " .. (t.name or "(Lua)") .. " <" .. + t.linedefined .. ":" .. t.short_src .. ">" + else + s = s .. event .. " " .. (t.name or "(C)") .. " [" .. t.what .. "]" + end + io.stderr:write (s .. "\n") +end + +-- Set hooks according to _DEBUG +if type (_DEBUG) == "table" and _DEBUG.call then + debug.sethook (trace, "cr") +end + + + +M = { + --- Provide a deprecated function definition according to _DEBUG.deprecate. + -- You can check whether your covered code uses deprecated functions by + -- setting `_DEBUG.deprecate` to `true` before loading any stdlib modules, + -- or silence deprecation warnings by setting `_DEBUG.deprecate = false`. + -- @function DEPRECATED + -- @string version first deprecation release version + -- @string name function name for automatic warning message + -- @string[opt] extramsg additional warning text + -- @func fn deprecated function + -- @return a function to show the warning on first call, and hand off to *fn* + -- @usage + -- M.op = DEPRECATED ("41", "'std.functional.op'", std.operator) + DEPRECATED = DEPRECATED, + + --- Format a deprecation warning message. + -- @function DEPRECATIONMSG + -- @string version first deprecation release version + -- @string name function name for automatic warning message + -- @string[opt] extramsg additional warning text + -- @int level call stack level to blame for the error + -- @treturn string deprecation warning message, or empty string + -- @usage + -- io.stderr:write (DEPRECATIONMSG ("42", "multi-argument 'module.fname'", 2)) + DEPRECATIONMSG = DEPRECATIONMSG, + + --- Check the type of an argument against expected types. + -- Equivalent to luaL_argcheck in the Lua C API. + -- + -- Call `argerror` if there is a type mismatch. + -- + -- Argument `actual` must match one of the types from in `expected`, each + -- of which can be the name of a primitive Lua type, a stdlib object type, + -- or one of the special options below: + -- + -- #table accept any non-empty table + -- any accept any non-nil argument type + -- file accept an open file object + -- function accept a function, or object with a __call metamethod + -- int accept an integer valued number + -- list accept a table where all keys are a contiguous 1-based integer range + -- #list accept any non-empty list + -- object accept any std.Object derived type + -- :foo accept only the exact string ":foo", works for any :-prefixed string + -- + -- The `:foo` format allows for type-checking of self-documenting + -- boolean-like constant string parameters predicated on `nil` versus + -- `:option` instead of `false` versus `true`. Or you could support + -- both: + -- + -- argcheck ("table.copy", 2, "boolean|:nometa|nil", nometa) + -- + -- A very common pattern is to have a list of possible types including + -- "nil" when the argument is optional. Rather than writing long-hand + -- as above, prepend a question mark to the list of types and omit the + -- explicit "nil" entry: + -- + -- argcheck ("table.copy", 2, "?boolean|:nometa", predicate) + -- + -- Normally, you should not need to use the `level` parameter, as the + -- default is to blame the caller of the function using `argcheck` in + -- error messages; which is almost certainly what you want. + -- @function argcheck + -- @string name function to blame in error message + -- @int i argument number to blame in error message + -- @string expected specification for acceptable argument types + -- @param actual argument passed + -- @int[opt=2] level call stack level to blame for the error + -- @usage + -- local function case (with, branches) + -- argcheck ("std.functional.case", 2, "#table", branches) + -- ... + argcheck = argcheck, + + --- Raise a bad argument error. + -- Equivalent to luaL_argerror in the Lua C API. This function does not + -- return. The `level` argument behaves just like the core `error` + -- function. + -- @function argerror + -- @string name function to callout in error message + -- @int i argument number + -- @string[opt] extramsg additional text to append to message inside parentheses + -- @int[opt=1] level call stack level to blame for the error + -- @see resulterror + -- @see extramsg_mismatch + -- @usage + -- local function slurp (file) + -- local h, err = input_handle (file) + -- if h == nil then argerror ("std.io.slurp", 1, err, 2) end + -- ... + argerror = argerror, + + --- Wrap a function definition with argument type and arity checking. + -- In addition to checking that each argument type matches the corresponding + -- element in the *types* table with `argcheck`, if the final element of + -- *types* ends with an ellipsis, remaining unchecked arguments are checked + -- against that type: + -- + -- format = argscheck ("string.format (string, ?any...)", string.format) + -- + -- A colon in the function name indicates that the argument type list does + -- not have a type for `self`: + -- + -- format = argscheck ("string:format (?any...)", string.format) + -- + -- If an argument can be omitted entirely, then put its type specification + -- in square brackets: + -- + -- insert = argscheck ("table.insert (table, [int], ?any)", table.insert) + -- + -- Similarly return types can be checked with the same list syntax as + -- arguments: + -- + -- len = argscheck ("string.len (string) => int", string.len) + -- + -- Additionally, variant return type lists can be listed like this: + -- + -- open = argscheck ("io.open (string, ?string) => file or nil, string", + -- io.open) + -- + -- @function argscheck + -- @string decl function type declaration string + -- @func inner function to wrap with argument checking + -- @usage + -- local case = argscheck ("std.functional.case (?any, #table) => [any...]", + -- function (with, branches) + -- ... + -- end) + argscheck = argscheck, + + --- Format a type mismatch error. + -- @function extramsg_mismatch + -- @string expected a pipe delimited list of matchable types + -- @param actual the actual argument to match with + -- @number[opt] index erroring container element index + -- @treturn string formatted *extramsg* for this mismatch for @{argerror} + -- @see argerror + -- @see resulterror + -- @usage + -- if fmt ~= nil and type (fmt) ~= "string" then + -- argerror ("format", 1, extramsg_mismatch ("?string", fmt)) + -- end + extramsg_mismatch = function (expected, actual, index) + return extramsg_mismatch (typesplit (expected), actual, index) + end, + + --- Format a too many things error. + -- @string bad the thing there are too many of + -- @int expected maximum number of *bad* things expected + -- @int actual actual number of *bad* things that triggered the error + -- @see argerror + -- @see resulterror + -- @see extramsg_mismatch + -- @usage + -- if maxn (argt) > 7 then + -- argerror ("sevenses", 8, extramsg_toomany ("argument", 7, maxn (argt))) + -- end + extramsg_toomany = extramsg_toomany, + + --- Extend `debug.getfenv` to unwrap functables correctly. + -- @tparam int|function|functable fn target function, or stack level + -- @treturn table environment of *fn* + getfenv = getfenv, + + --- Compact permutation list into a list of valid types at each argument. + -- Eliminate bracketed types by combining all valid types at each position + -- for all permutations of *typelist*. + -- @function parsetypes + -- @tparam list types a normalized list of type names + -- @treturn list valid types for each positional parameter + parsetypes = parsetypes, + + --- Raise a bad result error. + -- Like @{argerror} for bad results. This function does not + -- return. The `level` argument behaves just like the core `error` + -- function. + -- @string name function to callout in error message + -- @int i argument number + -- @string[opt] extramsg additional text to append to message inside parentheses + -- @int[opt=1] level call stack level to blame for the error + -- @usage + -- local function slurp (file) + -- local h, err = input_handle (file) + -- if h == nil then argerror ("std.io.slurp", 1, err, 2) end + -- ... + resulterror = resulterror, + + --- Extend `debug.setfenv` to unwrap functables correctly. + -- @tparam function|functable fn target function + -- @tparam table env new function environment + -- @treturn function *fn* + setfenv = setfenv, + + --- Print a debugging message to `io.stderr`. + -- Display arguments passed through `std.tostring` and separated by tab + -- characters when `_DEBUG` is `true` and *n* is 1 or less; or `_DEBUG.level` + -- is a number greater than or equal to *n*. If `_DEBUG` is false or + -- nil, nothing is written. + -- @function say + -- @int[opt=1] n debugging level, smaller is higher priority + -- @param ... objects to print (as for print) + -- @usage + -- local _DEBUG = require "std.debug_init"._DEBUG + -- _DEBUG.level = 3 + -- say (2, "_DEBUG table contents:", _DEBUG) + say = say, + + --- Trace function calls. + -- Use as debug.sethook (trace, "cr"), which is done automatically + -- when `_DEBUG.call` is set. + -- Based on test/trace-calls.lua from the Lua distribution. + -- @function trace + -- @string event event causing the call + -- @usage + -- _DEBUG = { call = true } + -- local debug = require "std.debug" + trace = trace, + + --- Split a typespec string into a table of normalized type names. + -- @tparam string|table either `"?bool|:nometa"` or `{"boolean", ":nometa"}` + -- @treturn table a new list with duplicates removed and leading "?"s + -- replaced by a "nil" element + typesplit = typesplit, + + + -- Private: + _setdebug = function (t) + for k, v in pairs (t) do + if v == "nil" then v = nil end + _DEBUG[k] = v + end + end, +} + + +for k, v in pairs (debug) do + M[k] = M[k] or v +end + +--- Equivalent to calling `debug.say (1, ...)` +-- @function debug +-- @see say +-- @usage +-- local debug = require "std.debug" +-- debug "oh noes!" +local metatable = { + __call = function (self, ...) + M.say (1, ...) + end, +} + + + +--[[ =========== ]]-- +--[[ Deprecated. ]]-- +--[[ =========== ]]-- + + +M.toomanyargmsg = DEPRECATED ("41.2.0", "debug.toomanyargmsg", + "use 'debug.extramsg_toomany' instead", + function (name, expect, actual) + local s = "bad argument #%d to '%s' (no more than %d argument%s expected, got %d)" + return s:format (expect + 1, name, expect, expect == 1 and "" or "s", actual) + end) + + +return setmetatable (M, metatable) + + + +--- Control std.debug function behaviour. +-- To declare debugging state, set _DEBUG either to `false` to disable all +-- runtime debugging; to any "truthy" value (equivalent to enabling everything +-- except *call*, or as documented below. +-- @class table +-- @name _DEBUG +-- @tfield[opt=true] boolean argcheck honor argcheck and argscheck calls +-- @tfield[opt=false] boolean call do call trace debugging +-- @field[opt=nil] deprecate if `false`, deprecated APIs are defined, +-- and do not issue deprecation warnings when used; if `nil` issue a +-- deprecation warning each time a deprecated api is used; any other +-- value causes deprecated APIs not to be defined at all +-- @tfield[opt=1] int level debugging level +-- @usage _DEBUG = { argcheck = false, level = 9 } diff --git a/lib/std/debug_init/init.lua b/lib/std/debug_init/init.lua new file mode 100644 index 0000000..ccdcb69 --- /dev/null +++ b/lib/std/debug_init/init.lua @@ -0,0 +1,47 @@ +-- Debugging is on by default +local M = {} + +-- Use rawget to satisfy std.strict. +local _DEBUG = rawget (_G, "_DEBUG") + +-- User specified fields. +if type (_DEBUG) == "table" then + M._DEBUG = _DEBUG + +-- Turn everything off. +elseif _DEBUG == false then + M._DEBUG = { + argcheck = false, + call = false, + deprecate = false, + level = math.huge, + } + +-- Turn everything on (except _DEBUG.call must be set explicitly). +elseif _DEBUG == true then + M._DEBUG = { + argcheck = true, + call = false, + deprecate = true, + } + +else + M._DEBUG = {} +end + + +local function setdefault (field, value) + if M._DEBUG[field] == nil then + M._DEBUG[field] = value + end +end + + +-- Default settings if otherwise unspecified. +setdefault ("argcheck", true) +setdefault ("call", false) +setdefault ("deprecate", nil) +setdefault ("level", 1) + + +return M diff --git a/lib/std/functional.lua b/lib/std/functional.lua new file mode 100644 index 0000000..b20a8bc --- /dev/null +++ b/lib/std/functional.lua @@ -0,0 +1,620 @@ +--[[-- + Functional programming. + + A selection of higher-order functions to enable a functional style of + programming in Lua. + + @module std.functional +]] + + +local base = require "std.base" +local debug = require "std.debug" + +local ielems, ipairs, ireverse, npairs, pairs = + base.ielems, base.ipairs, base.ireverse, base.npairs, base.pairs +local callable, copy, len, reduce, unpack = + base.callable, base.copy, base.len, base.reduce, base.unpack +local loadstring = loadstring or load + + +local function bind (fn, ...) + local bound = {...} + if type (bound[1]) == "table" and bound[2] == nil then + bound = bound[1] + else + io.stderr:write (debug.DEPRECATIONMSG ("39", + "multi-argument 'std.functional.bind'", + "use a table of arguments as the second parameter instead", 2)) + end + + return function (...) + local argt, i = copy (bound), 1 + for _, v in npairs {...} do + while argt[i] ~= nil do i = i + 1 end + argt[i], i = v, i + 1 + end + return fn (unpack (argt)) + end +end + + +local function case (with, branches) + local match = branches[with] or branches[1] + if callable (match) then + return match (with) + end + return match +end + + +local function compose (...) + local fns = {...} + + return function (...) + local argt = {...} + for _, fn in npairs (fns) do + argt = {fn (unpack (argt))} + end + return unpack (argt) + end +end + + +local function cond (expr, branch, ...) + if branch == nil and select ("#", ...) == 0 then + expr, branch = true, expr + end + if expr then + if callable (branch) then + return branch (expr) + end + return branch + end + return cond (...) +end + + +local function curry (fn, n) + if n <= 1 then + return fn + else + return function (x) + return curry (bind (fn, x), n - 1) + end + end +end + + +local function filter (pfn, ifn, ...) + local argt, r = {...}, {} + if not callable (ifn) then + ifn, argt = pairs, {ifn, ...} + end + + local nextfn, state, k = ifn (unpack (argt)) + + local t = {nextfn (state, k)} -- table of iteration 1 + local arity = #t -- How many return values from ifn? + + if arity == 1 then + local v = t[1] + while v ~= nil do -- until iterator returns nil + if pfn (unpack (t)) then -- pass all iterator results to p + r[#r + 1] = v + end + + t = {nextfn (state, v)} -- maintain loop invariant + v = t[1] + + if #t > 1 then -- unless we discover arity is not 1 after all + arity, r = #t, {} break + end + end + end + + if arity > 1 then + -- No need to start over here, because either: + -- (i) arity was never 1, and the original value of t is correct + -- (ii) arity used to be 1, but we only consumed nil values, so the + -- current t with arity > 1 is the correct next value to use + while t[1] ~= nil do + local k = t[1] + if pfn (unpack (t)) then r[k] = t[2] end + t = {nextfn (state, k)} + end + end + + return r +end + + +local function foldl (fn, d, t) + if t == nil then + local tail = {} + for i = 2, len (d) do tail[#tail + 1] = d[i] end + d, t = d[1], tail + end + return reduce (fn, d, ielems, t) +end + + +local function foldr (fn, d, t) + if t == nil then + local u, last = {}, len (d) + for i = 1, last - 1 do u[#u + 1] = d[i] end + d, t = d[last], u + end + return reduce (function (x, y) return fn (y, x) end, d, ielems, ireverse (t)) +end + + +local function id (...) + return ... +end + + +local function memoize (fn, normalize) + if normalize == nil then + normalize = function (...) return base.tostring {...} end + end + + return setmetatable ({}, { + __call = function (self, ...) + local k = normalize (...) + local t = self[k] + if t == nil then + t = {fn (...)} + self[k] = t + end + return unpack (t) + end + }) +end + + +local lambda = memoize (function (s) + local expr + + -- Support "|args|expression" format. + local args, body = s:match "^%s*|%s*([^|]*)|%s*(.+)%s*$" + if args and body then + expr = "return function (" .. args .. ") return " .. body .. " end" + end + + -- Support "expression" format. + if not expr then + body = s:match "^%s*(_.*)%s*$" or s:match "^=%s*(.+)%s*$" + if body then + expr = [[ + return function (...) + local unpack = table.unpack or unpack + local _1,_2,_3,_4,_5,_6,_7,_8,_9 = unpack {...} + local _ = _1 + return ]] .. body .. [[ + end + ]] + end + end + + local ok, fn + if expr then + ok, fn = pcall (loadstring (expr)) + end + + -- Diagnose invalid input. + if not ok then + return nil, "invalid lambda string '" .. s .. "'" + end + + return fn +end, id) + + +local function map (mapfn, ifn, ...) + local argt, r = {...}, {} + if not callable (ifn) or not next (argt) then + ifn, argt = pairs, {ifn, ...} + end + + local nextfn, state, k = ifn (unpack (argt)) + local mapargs = {nextfn (state, k)} + + local arity = 1 + while mapargs[1] ~= nil do + local d, v = mapfn (unpack (mapargs)) + if v ~= nil then + arity, r = 2, {} break + end + r[#r + 1] = d + mapargs = {nextfn (state, mapargs[1])} + end + + if arity > 1 then + -- No need to start over here, because either: + -- (i) arity was never 1, and the original value of mapargs is correct + -- (ii) arity used to be 1, but we only consumed nil values, so the + -- current mapargs with arity > 1 is the correct next value to use + while mapargs[1] ~= nil do + local k, v = mapfn (unpack (mapargs)) + r[k] = v + mapargs = {nextfn (state, mapargs[1])} + end + end + return r +end + + +local function map_with (mapfn, tt) + local r = {} + for k, v in pairs (tt) do + r[k] = mapfn (unpack (v)) + end + return r +end + + +local function zip (tt) + local r = {} + for outerk, inner in pairs (tt) do + for k, v in pairs (inner) do + r[k] = r[k] or {} + r[k][outerk] = v + end + end + return r +end + + +local function zip_with (fn, tt) + return map_with (fn, zip (tt)) +end + + + +--[[ ================= ]]-- +--[[ Public Interface. ]]-- +--[[ ================= ]]-- + + +local function X (decl, fn) + return debug.argscheck ("std.functional." .. decl, fn) +end + +local M = { + --- Partially apply a function. + -- @function bind + -- @func fn function to apply partially + -- @tparam table argt table of *fn* arguments to bind + -- @return function with *argt* arguments already bound + -- @usage + -- cube = bind (std.operator.pow, {[2] = 3}) + bind = X ("bind (func, ?any...)", bind), + + --- Identify callable types. + -- @function callable + -- @param x an object or primitive + -- @return `true` if *x* can be called, otherwise `false` + -- @usage + -- if callable (functable) then functable (args) end + callable = X ("callable (?any)", callable), + + --- A rudimentary case statement. + -- Match *with* against keys in *branches* table. + -- @function case + -- @param with expression to match + -- @tparam table branches map possible matches to functions + -- @return the value associated with a matching key, or the first non-key + -- value if no key matches. Function or functable valued matches are + -- called using *with* as the sole argument, and the result of that call + -- returned; otherwise the matching value associated with the matching + -- key is returned directly; or else `nil` if there is no match and no + -- default. + -- @see cond + -- @usage + -- return case (type (object), { + -- table = "table", + -- string = function () return "string" end, + -- function (s) error ("unhandled type: " .. s) end, + -- }) + case = X ("case (?any, #table)", case), + + --- Collect the results of an iterator. + -- @function collect + -- @func[opt=std.npairs] ifn iterator function + -- @param ... *ifn* arguments + -- @treturn table of results from running *ifn* on *args* + -- @see filter + -- @see map + -- @usage + -- --> {"a", "b", "c"} + -- collect {"a", "b", "c", x=1, y=2, z=5} + collect = X ("collect ([func], any...)", base.collect), + + --- Compose functions. + -- @function compose + -- @func ... functions to compose + -- @treturn function composition of fnN .. fn1: note that this is the + -- reverse of what you might expect, but means that code like: + -- + -- functional.compose (function (x) return f (x) end, + -- function (x) return g (x) end)) + -- + -- can be read from top to bottom. + -- @usage + -- vpairs = compose (table.invert, ipairs) + -- for v, i in vpairs {"a", "b", "c"} do process (v, i) end + compose = X ("compose (func...)", compose), + + --- A rudimentary condition-case statement. + -- If *expr* is "truthy" return *branch* if given, otherwise *expr* + -- itself. If the return value is a function or functable, then call it + -- with *expr* as the sole argument and return the result; otherwise + -- return it explicitly. If *expr* is "falsey", then recurse with the + -- first two arguments stripped. + -- @function cond + -- @param expr a Lua expression + -- @param branch a function, functable or value to use if *expr* is + -- "truthy" + -- @param ... additional arguments to retry if *expr* is "falsey" + -- @see case + -- @usage + -- -- recursively calculate the nth triangular number + -- function triangle (n) + -- return cond ( + -- n <= 0, 0, + -- n == 1, 1, + -- function () return n + triangle (n - 1) end) + -- end + cond = cond, -- any number of any type arguments! + + --- Curry a function. + -- @function curry + -- @func fn function to curry + -- @int n number of arguments + -- @treturn function curried version of *fn* + -- @usage + -- add = curry (function (x, y) return x + y end, 2) + -- incr, decr = add (1), add (-1) + curry = X ("curry (func, int)", curry), + + --- Filter an iterator with a predicate. + -- @function filter + -- @tparam predicate pfn predicate function + -- @func[opt=std.pairs] ifn iterator function + -- @param ... iterator arguments + -- @treturn table elements e for which `pfn (e)` is not "falsey". + -- @see collect + -- @see map + -- @usage + -- --> {2, 4} + -- filter (lambda '|e|e%2==0', std.elems, {1, 2, 3, 4}) + filter = X ("filter (func, [func], any...)", filter), + + --- Fold a binary function left associatively. + -- If parameter *d* is omitted, the first element of *t* is used, + -- and *t* treated as if it had been passed without that element. + -- @function foldl + -- @func fn binary function + -- @param[opt=t[1]] d initial left-most argument + -- @tparam table t a table + -- @return result + -- @see foldr + -- @see reduce + -- @usage + -- foldl (std.operator.quot, {10000, 100, 10}) == (10000 / 100) / 10 + foldl = X ("foldl (function, [any], table)", foldl), + + --- Fold a binary function right associatively. + -- If parameter *d* is omitted, the last element of *t* is used, + -- and *t* treated as if it had been passed without that element. + -- @function foldr + -- @func fn binary function + -- @param[opt=t[1]] d initial right-most argument + -- @tparam table t a table + -- @return result + -- @see foldl + -- @see reduce + -- @usage + -- foldr (std.operator.quot, {10000, 100, 10}) == 10000 / (100 / 10) + foldr = X ("foldr (function, [any], table)", foldr), + + --- Identity function. + -- @function id + -- @param ... arguments + -- @return *arguments* + id = id, -- any number of any type arguments! + + --- Compile a lambda string into a Lua function. + -- + -- A valid lambda string takes one of the following forms: + -- + -- 1. `'=expression'`: equivalent to `function (...) return expression end` + -- 1. `'|args|expression'`: equivalent to `function (args) return expression end` + -- + -- The first form (starting with `'='`) automatically assigns the first + -- nine arguments to parameters `'_1'` through `'_9'` for use within the + -- expression body. The parameter `'_1'` is aliased to `'_'`, and if the + -- first non-whitespace of the whole expression is `'_'`, then the + -- leading `'='` can be omitted. + -- + -- The results are memoized, so recompiling a previously compiled + -- lambda string is extremely fast. + -- @function lambda + -- @string s a lambda string + -- @treturn functable compiled lambda string, can be called like a function + -- @usage + -- -- The following are equivalent: + -- lambda '= _1 < _2' + -- lambda '|a,b| a {1, 4, 9, 16} + -- map (lambda '=_1*_1', std.ielems, {1, 2, 3, 4}) + map = X ("map (func, [func], any...)", map), + + --- Map a function over a table of argument lists. + -- @function map_with + -- @func fn map function + -- @tparam table tt a table of *fn* argument lists + -- @treturn table new table of *fn* results + -- @see map + -- @see zip_with + -- @usage + -- --> {"123", "45"}, {a="123", b="45"} + -- conc = bind (map_with, {lambda '|...|table.concat {...}'}) + -- conc {{1, 2, 3}, {4, 5}}, conc {a={1, 2, 3, x="y"}, b={4, 5, z=6}} + map_with = X ("map_with (function, table of tables)", map_with), + + --- Memoize a function, by wrapping it in a functable. + -- + -- To ensure that memoize always returns the same results for the same + -- arguments, it passes arguments to *fn*. You can specify a more + -- sophisticated function if memoize should handle complicated argument + -- equivalencies. + -- @function memoize + -- @func fn pure function: a function with no side effects + -- @tparam[opt=std.tostring] normalize normfn function to normalize arguments + -- @treturn functable memoized function + -- @usage + -- local fast = memoize (function (...) --[[ slow code ]] end) + memoize = X ("memoize (func, ?func)", memoize), + + --- No operation. + -- This function ignores all arguments, and returns no values. + -- @function nop + -- @see id + -- @usage + -- if unsupported then vtable["memrmem"] = nop end + nop = base.nop, -- ignores all arguments + + --- Fold a binary function into an iterator. + -- @function reduce + -- @func fn reduce function + -- @param d initial first argument + -- @func[opt=std.pairs] ifn iterator function + -- @param ... iterator arguments + -- @return result + -- @see foldl + -- @see foldr + -- @usage + -- --> 2 ^ 3 ^ 4 ==> 4096 + -- reduce (std.operator.pow, 2, std.ielems, {3, 4}) + reduce = X ("reduce (func, any, [func], any...)", reduce), + + --- Zip a table of tables. + -- Make a new table, with lists of elements at the same index in the + -- original table. This function is effectively its own inverse. + -- @function zip + -- @tparam table tt a table of tables + -- @treturn table new table with lists of elements of the same key + -- from *tt* + -- @see map + -- @see zip_with + -- @usage + -- --> {{1, 3, 5}, {2, 4}}, {a={x=1, y=3, z=5}, b={x=2, y=4}} + -- zip {{1, 2}, {3, 4}, {5}}, zip {x={a=1, b=2}, y={a=3, b=4}, z={a=5}} + zip = X ("zip (table of tables)", zip), + + --- Zip a list of tables together with a function. + -- @function zip_with + -- @tparam function fn function + -- @tparam table tt table of tables + -- @treturn table a new table of results from calls to *fn* with arguments + -- made from all elements the same key in the original tables; effectively + -- the "columns" in a simple list + -- of lists. + -- @see map_with + -- @see zip + -- @usage + -- --> {"135", "24"}, {a="1", b="25"} + -- conc = bind (zip_with, {lambda '|...|table.concat {...}'}) + -- conc {{1, 2}, {3, 4}, {5}}, conc {{a=1, b=2}, x={a=3, b=4}, {b=5}} + zip_with = X ("zip_with (function, table of tables)", zip_with), +} + + + +--[[ ============= ]]-- +--[[ Deprecations. ]]-- +--[[ ============= ]]-- + + +local DEPRECATED = debug.DEPRECATED + + +M.eval = DEPRECATED ("41", "'std.functional.eval'", + "use 'std.eval' instead", base.eval) + + +local function fold (fn, d, ifn, ...) + local nextfn, state, k = ifn (...) + local t = {nextfn (state, k)} + + local r = d + while t[1] ~= nil do + r = fn (r, t[#t]) + t = {nextfn (state, t[1])} + end + return r +end + +M.fold = DEPRECATED ("41", "'std.functional.fold'", + "use 'std.functional.reduce' instead", fold) + + +local operator = require "std.operator" + +local function DEPRECATEOP (old, new) + return DEPRECATED ("41", "'std.functional.op[" .. old .. "]'", + "use 'std.operator." .. new .. "' instead", operator[new]) +end + +M.op = { + ["[]"] = DEPRECATEOP ("[]", "get"), + ["+"] = DEPRECATEOP ("+", "sum"), + ["-"] = DEPRECATEOP ("-", "diff"), + ["*"] = DEPRECATEOP ("*", "prod"), + ["/"] = DEPRECATEOP ("/", "quot"), + ["and"] = DEPRECATEOP ("and", "conj"), + ["or"] = DEPRECATEOP ("or", "disj"), + ["not"] = DEPRECATEOP ("not", "neg"), + ["=="] = DEPRECATEOP ("==", "eq"), + ["~="] = DEPRECATEOP ("~=", "neq"), +} + +return M + + + +--- Types +-- @section Types + + +--- Signature of a @{memoize} argument normalization callback function. +-- @function normalize +-- @param ... arguments +-- @treturn string normalized arguments +-- @usage +-- local normalize = function (name, value, props) return name end +-- local intern = std.functional.memoize (mksymbol, normalize) + + +--- Signature of a @{filter} predicate callback function. +-- @function predicate +-- @param ... arguments +-- @treturn boolean "truthy" if the predicate condition succeeds, +-- "falsey" otherwise +-- @usage +-- local predicate = lambda '|k,v|type(v)=="string"' +-- local strvalues = filter (predicate, std.pairs, {name="Roberto", id=12345}) diff --git a/lib/std/io.lua b/lib/std/io.lua new file mode 100644 index 0000000..056d7cc --- /dev/null +++ b/lib/std/io.lua @@ -0,0 +1,295 @@ +--[[-- + Additions to the core io module. + + The module table returned by `std.io` also contains all of the entries from + the core `io` module table. An hygienic way to import this module, then, + is simply to override core `io` locally: + + local io = require "std.io" + + @module std.io +]] + + +local base = require "std.base" +local debug = require "std.debug" + +local argerror = debug.argerror +local catfile, dirsep, insert, len, leaves, split = + base.catfile, base.dirsep, base.insert, base.len, base.leaves, base.split +local ipairs, pairs = base.ipairs, base.pairs +local setmetatable = debug.setmetatable + + + +local M, monkeys + + +local function input_handle (h) + if h == nil then + return io.input () + elseif type (h) == "string" then + return io.open (h) + end + return h +end + + +local function slurp (file) + local h, err = input_handle (file) + if h == nil then argerror ("std.io.slurp", 1, err, 2) end + + if h then + local s = h:read ("*a") + h:close () + return s + end +end + + +local function readlines (file) + local h, err = input_handle (file) + if h == nil then argerror ("std.io.readlines", 1, err, 2) end + + local l = {} + for line in h:lines () do + l[#l + 1] = line + end + h:close () + return l +end + + +local function writelines (h, ...) + if io.type (h) ~= "file" then + io.write (h, "\n") + h = io.output () + end + for v in leaves (ipairs, {...}) do + h:write (v, "\n") + end +end + + +local function monkey_patch (namespace) + namespace = namespace or _G + namespace.io = base.copy (namespace.io or {}, monkeys) + + if namespace.io.stdin then + local mt = getmetatable (namespace.io.stdin) or {} + mt.readlines = M.readlines + mt.writelines = M.writelines + setmetatable (namespace.io.stdin, mt) + end + + return M +end + + +local function process_files (fn) + -- N.B. "arg" below refers to the global array of command-line args + if len (arg) == 0 then + insert (arg, "-") + end + for i, v in ipairs (arg) do + if v == "-" then + io.input (io.stdin) + else + io.input (v) + end + fn (v, i) + end +end + + +local function warnfmt (msg, ...) + local prefix = "" + if (prog or {}).name then + prefix = prog.name .. ":" + if prog.line then + prefix = prefix .. tostring (prog.line) .. ":" + end + elseif (prog or {}).file then + prefix = prog.file .. ":" + if prog.line then + prefix = prefix .. tostring (prog.line) .. ":" + end + elseif (opts or {}).program then + prefix = opts.program .. ":" + if opts.line then + prefix = prefix .. tostring (opts.line) .. ":" + end + end + if #prefix > 0 then prefix = prefix .. " " end + return prefix .. string.format (msg, ...) +end + + +local function warn (msg, ...) + writelines (io.stderr, warnfmt (msg, ...)) +end + + + +--[[ ================= ]]-- +--[[ Public Interface. ]]-- +--[[ ================= ]]-- + + +local function X (decl, fn) + return debug.argscheck ("std.io." .. decl, fn) +end + + +M = { + --- Concatenate directory names into a path. + -- @function catdir + -- @string ... path components + -- @return path without trailing separator + -- @see catfile + -- @usage dirpath = catdir ("", "absolute", "directory") + catdir = X ("catdir (string...)", function (...) + return (table.concat ({...}, dirsep):gsub("^$", dirsep)) + end), + + --- Concatenate one or more directories and a filename into a path. + -- @function catfile + -- @string ... path components + -- @treturn string path + -- @see catdir + -- @see splitdir + -- @usage filepath = catfile ("relative", "path", "filename") + catfile = X ("catfile (string...)", base.catfile), + + --- Die with error. + -- This function uses the same rules to build a message prefix + -- as @{warn}. + -- @function die + -- @string msg format string + -- @param ... additional arguments to plug format string specifiers + -- @see warn + -- @usage die ("oh noes! (%s)", tostring (obj)) + die = X ("die (string, [any...])", function (...) + error (warnfmt (...), 0) + end), + + --- Remove the last dirsep delimited element from a path. + -- @function dirname + -- @string path file path + -- @treturn string a new path with the last dirsep and following + -- truncated + -- @usage dir = dirname "/base/subdir/filename" + dirname = X ("dirname (string)", function (path) + return (path:gsub (catfile ("", "[^", "]*$"), "")) + end), + + --- Overwrite core `io` methods with `std` enhanced versions. + -- + -- Also adds @{readlines} and @{writelines} metamethods to core file objects. + -- @function monkey_patch + -- @tparam[opt=_G] table namespace where to install global functions + -- @treturn table the `std.io` module table + -- @usage local io = require "std.io".monkey_patch () + monkey_patch = X ("monkey_patch (?table)", monkey_patch), + + --- Process files specified on the command-line. + -- Each filename is made the default input source with `io.input`, and + -- then the filename and argument number are passed to the callback + -- function. In list of filenames, `-` means `io.stdin`. If no + -- filenames were given, behave as if a single `-` was passed. + -- @todo Make the file list an argument to the function. + -- @function process_files + -- @tparam fileprocessor fn function called for each file argument + -- @usage + -- #! /usr/bin/env lua + -- -- minimal cat command + -- local io = require "std.io" + -- io.process_files (function () io.write (io.slurp ()) end) + process_files = X ("process_files (function)", process_files), + + --- Read a file or file handle into a list of lines. + -- The lines in the returned list are not `\n` terminated. + -- @function readlines + -- @tparam[opt=io.input()] file|string file file handle or name; + -- if file is a file handle, that file is closed after reading + -- @treturn list lines + -- @usage list = readlines "/etc/passwd" + readlines = X ("readlines (?file|string)", readlines), + + --- Perform a shell command and return its output. + -- @function shell + -- @string c command + -- @treturn string output, or nil if error + -- @see os.execute + -- @usage users = shell [[cat /etc/passwd | awk -F: '{print $1;}']] + shell = X ("shell (string)", function (c) return slurp (io.popen (c)) end), + + --- Slurp a file handle. + -- @function slurp + -- @tparam[opt=io.input()] file|string file file handle or name; + -- if file is a file handle, that file is closed after reading + -- @return contents of file or handle, or nil if error + -- @see process_files + -- @usage contents = slurp (filename) + slurp = X ("slurp (?file|string)", slurp), + + --- Split a directory path into components. + -- Empty components are retained: the root directory becomes `{"", ""}`. + -- @function splitdir + -- @param path path + -- @return list of path components + -- @see catdir + -- @usage dir_components = splitdir (filepath) + splitdir = X ("splitdir (string)", + function (path) return split (path, dirsep) end), + + --- Give warning with the name of program and file (if any). + -- If there is a global `prog` table, prefix the message with + -- `prog.name` or `prog.file`, and `prog.line` if any. Otherwise + -- if there is a global `opts` table, prefix the message with + -- `opts.program` and `opts.line` if any. @{std.optparse:parse} + -- returns an `opts` table that provides the required `program` + -- field, as long as you assign it back to `_G.opts`. + -- @function warn + -- @string msg format string + -- @param ... additional arguments to plug format string specifiers + -- @see std.optparse:parse + -- @see die + -- @usage + -- local OptionParser = require "std.optparse" + -- local parser = OptionParser "eg 0\nUsage: eg\n" + -- _G.arg, _G.opts = parser:parse (_G.arg) + -- if not _G.opts.keep_going then + -- require "std.io".warn "oh noes!" + -- end + warn = X ("warn (string, [any...])", warn), + + --- Write values adding a newline after each. + -- @function writelines + -- @tparam[opt=io.output()] file h open writable file handle; + -- the file is **not** closed after writing + -- @tparam string|number ... values to write (as for write) + -- @usage writelines (io.stdout, "first line", "next line") + writelines = X ("writelines (?file|string|number, [string|number...])", writelines), +} + + +monkeys = base.copy ({}, M) -- before deprecations and core merge + + +return base.merge (M, io) + + + +--- Types +-- @section Types + +--- Signature of @{process_files} callback function. +-- @function fileprocessor +-- @string filename filename +-- @int i argument number of *filename* +-- @usage +-- local fileprocessor = function (filename, i) +-- io.write (tostring (i) .. ":\n===\n" .. io.slurp (filename) .. "\n") +-- end +-- io.process_files (fileprocessor) diff --git a/lib/std/list.lua b/lib/std/list.lua new file mode 100644 index 0000000..ad04c06 --- /dev/null +++ b/lib/std/list.lua @@ -0,0 +1,515 @@ +--[[-- + Tables as lists. + + Prototype Chain + --------------- + + table + `-> Object + `-> List + + @classmod std.list +]] + + +local base = require "std.base" +local debug = require "std.debug" + +local Object = require "std.object" {} + +local ipairs, pairs = base.ipairs, base.pairs +local len = base.len +local compare = base.compare +local prototype = base.prototype +local unpack = base.unpack + +local M, List + + +local function append (l, x) + local r = l {} + r[#r + 1] = x + return r +end + + +local function concat (l, ...) + local r = List {} + for _, e in ipairs {l, ...} do + for _, v in ipairs (e) do + r[#r + 1] = v + end + end + return r +end + + +local function rep (l, n) + local r = List {} + for i = 1, n do + r = concat (r, l) + end + return r +end + + +local function sub (l, from, to) + local r = List {} + local lenl = len (l) + from = from or 1 + to = to or lenl + if from < 0 then + from = from + lenl + 1 + end + if to < 0 then + to = to + lenl + 1 + end + for i = from, to do + r[#r + 1] = l[i] + end + return r +end + + + +--[[ ================= ]]-- +--[[ Public Interface. ]]-- +--[[ ================= ]]-- + + +local function X (decl, fn) + return debug.argscheck ("std.list." .. decl, fn) +end + + +M = { + --- Append an item to a list. + -- @static + -- @function append + -- @tparam List l a list + -- @param x item + -- @treturn List new list with *x* appended + -- @usage + -- longer = append (short, "last") + append = X ("append (List, any)", append), + + --- Compare two lists element-by-element, from left-to-right. + -- @static + -- @function compare + -- @tparam List l a list + -- @tparam List|table m another list, or table + -- @return -1 if *l* is less than *m*, 0 if they are the same, and 1 + -- if *l* is greater than *m* + -- @usage + -- if a_list:compare (another_list) == 0 then print "same" end + compare = X ("compare (List, List|table)", compare), + + --- Concatenate the elements from any number of lists. + -- @static + -- @function concat + -- @tparam List l a list + -- @param ... tuple of lists + -- @treturn List new list with elements from arguments + -- @usage + -- --> {1, 2, 3, {4, 5}, 6, 7} + -- list.concat ({1, 2, 3}, {{4, 5}, 6, 7}) + concat = X ("concat (List, List|table...)", concat), + + --- Prepend an item to a list. + -- @static + -- @function cons + -- @tparam List l a list + -- @param x item + -- @treturn List new list with *x* followed by elements of *l* + -- @usage + -- --> {"x", 1, 2, 3} + -- list.cons ({1, 2, 3}, "x") + cons = X ("cons (List, any)", function (l, x) return List {x, unpack (l)} end), + + --- Repeat a list. + -- @static + -- @function rep + -- @tparam List l a list + -- @int n number of times to repeat + -- @treturn List *n* copies of *l* appended together + -- @usage + -- --> {1, 2, 3, 1, 2, 3, 1, 2, 3} + -- list.rep ({1, 2, 3}, 3) + rep = X ("rep (List, int)", rep), + + --- Return a sub-range of a list. + -- (The equivalent of @{string.sub} on strings; negative list indices + -- count from the end of the list.) + -- @static + -- @function sub + -- @tparam List l a list + -- @int[opt=1] from start of range + -- @int[opt=#l] to end of range + -- @treturn List new list containing elements between *from* and *to* + -- inclusive + -- @usage + -- --> {3, 4, 5} + -- list.sub ({1, 2, 3, 4, 5, 6}, 3, 5) + sub = X ("sub (List, ?int, ?int)", sub), + + --- Return a list with its first element removed. + -- @static + -- @function tail + -- @tparam List l a list + -- @treturn List new list with all but the first element of *l* + -- @usage + -- --> {3, {4, 5}, 6, 7} + -- list.tail {{1, 2}, 3, {4, 5}, 6, 7} + tail = X ("tail (List)", function (l) return sub (l, 2) end), +} + + + +--[[ ============= ]]-- +--[[ Deprecations. ]]-- +--[[ ============= ]]-- + +-- This entire section can be deleted in due course, with just one +-- additional small correction noted in FIXME comments in the List +-- object constructor at the end of this file. + + +local DEPRECATED = debug.DEPRECATED + + +local function depair (ls) + local t = {} + for _, v in ipairs (ls) do + t[v[1]] = v[2] + end + return t +end + + +local function enpair (t) + local ls = List {} + for i, v in pairs (t) do + ls[#ls + 1] = List {i, v} + end + return ls +end + + +local function filter (pfn, l) + local r = List {} + for _, e in ipairs (l) do + if pfn (e) then + r[#r + 1] = e + end + end + return r +end + + +local function flatten (l) + local r = List {} + for v in base.leaves (ipairs, l) do + r[#r + 1] = v + end + return r +end + + +local function foldl (fn, d, t) + if t == nil then + local tail = {} + for i = 2, len (d) do tail[#tail + 1] = d[i] end + d, t = d[1], tail + end + return base.reduce (fn, d, base.ielems, t) +end + + +local function foldr (fn, d, t) + if t == nil then + local u, last = {}, len (d) + for i = 1, last - 1 do u[#u + 1] = d[i] end + d, t = d[last], u + end + return base.reduce ( + function (x, y) return fn (y, x) end, d, base.ielems, base.ireverse (t)) +end + + +local function index_key (f, l) + local r = {} + for i, v in ipairs (l) do + local k = v[f] + if k then + r[k] = i + end + end + return r +end + + +local function index_value (f, l) + local r = {} + for i, v in ipairs (l) do + local k = v[f] + if k then + r[k] = v + end + end + return r +end + + +local function map (fn, l) + local r = List {} + for _, e in ipairs (l) do + local v = fn (e) + if v ~= nil then + r[#r + 1] = v + end + end + return r +end + + +local function map_with (fn, ls) + return map (function (...) return fn (unpack (...)) end, ls) +end + + +local function project (x, l) + return map (function (t) return t[x] end, l) +end + + +local function relems (l) return base.ielems (base.ireverse (l)) end + + +local function reverse (l) return List (base.ireverse (l)) end + + +local function shape (s, l) + l = flatten (l) + -- Check the shape and calculate the size of the zero, if any + local size = 1 + local zero + for i, v in ipairs (s) do + if v == 0 then + if zero then -- bad shape: two zeros + return nil + else + zero = i + end + else + size = size * v + end + end + if zero then + s[zero] = math.ceil (len (l) / size) + end + local function fill (i, d) + if d > len (s) then + return l[i], i + 1 + else + local r = List {} + for j = 1, s[d] do + local e + e, i = fill (i, d + 1) + r[#r + 1] = e + end + return r, i + end + end + return (fill (1, 1)) +end + + +local function transpose (ls) + local rs, lenls, dims = List {}, len (ls), map (len, ls) + if len (dims) > 0 then + for i = 1, math.max (unpack (dims)) do + rs[i] = List {} + for j = 1, lenls do + rs[i][j] = ls[j][i] + end + end + end + return rs +end + + +local function zip_with (ls, fn) + return map_with (fn, transpose (ls)) +end + + +local m = { + append = M.append, + compare = M.compare, + concat = M.concat, + cons = M.cons, + rep = M.rep, + sub = M.sub, + tail = M.tail, +} + + +m.depair = DEPRECATED ("38", "'std.list:depair'", depair) +m.map_with = DEPRECATED ("38", "'std.list:map_with'", + function (self, fn) return map_with (fn, self) end) +m.transpose = DEPRECATED ("38", "'std.list:transpose'", transpose) +m.zip_with = DEPRECATED ("38", "'std.list:zip_with'", zip_with) + + +M.depair = DEPRECATED ("41", "'std.list.depair'", depair) + +M.enpair = DEPRECATED ("41", "'std.list.enpair'", enpair) +m.enpair = DEPRECATED ("41", "'std.list:enpair'", enpair) + +M.elems = DEPRECATED ("41", "'std.list.elems'", + "use 'std.ielems' instead", base.ielems) +m.elems = DEPRECATED ("41", "'std.list:elems'", + "use 'std.ielems' instead", base.ielems) + +M.filter = DEPRECATED ("41", "'std.list.filter'", + "use 'std.functional.filter' instead", filter) +m.filter = DEPRECATED ("41", "'std.list:filter'", + "use 'std.functional.filter' instead", + function (self, p) return filter (p, self) end) + + +M.flatten = DEPRECATED ("41", "'std.list.flatten'", + "use 'std.functional.flatten' instead", flatten) +m.flatten = DEPRECATED ("41", "'std.list:flatten'", + "use 'std.functional.flatten' instead", flatten) + + +M.foldl = DEPRECATED ("41", "'std.list.foldl'", + "use 'std.functional.foldl' instead", foldl) +m.foldl = DEPRECATED ("41", "'std.list:foldl'", + "use 'std.functional.foldl' instead", + function (self, fn, e) + if e ~= nil then return foldl (fn, e, self) end + return foldl (fn, self) + end) + +M.foldr = DEPRECATED ("41", "'std.list.foldr'", + "use 'std.functional.foldr' instead", foldr) +m.foldr = DEPRECATED ("41", "'std.list:foldr'", + "use 'std.functional.foldr' instead", + function (self, fn, e) + if e ~= nil then return foldr (fn, e, self) end + return foldr (fn, self) + end) + +M.index_key = DEPRECATED ("41", "'std.list.index_key'", + "compose 'std.functional.filter' and 'std.table.invert' instead", + index_key) +m.index_key = DEPRECATED ("41", "'std.list:index_key'", + function (self, fn) return index_key (fn, self) end) + + +M.index_value = DEPRECATED ("41", "'std.list.index_value'", + "compose 'std.functional.filter' and 'std.table.invert' instead", + index_value) +m.index_value = DEPRECATED ("41", "'std.list:index_value'", + function (self, fn) return index_value (fn, self) end) + + +M.map = DEPRECATED ("41", "'std.list.map'", + "use 'std.functional.map' instead", map) +m.map = DEPRECATED ("41", "'std.list:map'", + "use 'std.functional.map' instead", + function (self, fn) return map (fn, self) end) + + + +M.map_with = DEPRECATED ("41", "'std.list.map_with'", + "use 'std.functional.map_with' instead", map_with) + +M.project = DEPRECATED ("41", "'std.list.project'", + "use 'std.table.project' instead", project) +m.project = DEPRECATED ("41", "'std.list:project'", + "use 'std.table.project' instead", + function (self, x) return project (x, self) end) + +M.relems = DEPRECATED ("41", "'std.list.relems'", + "compose 'std.ielems' and 'std.ireverse' instead", relems) +m.relems = DEPRECATED ("41", "'std.list:relems'", relems) + +M.reverse = DEPRECATED ("41", "'std.list.reverse'", + "compose 'std.list' and 'std.ireverse' instead", reverse) +m.reverse = DEPRECATED ("41", "'std.list:reverse'", + "compose 'std.list' and 'std.ireverse' instead", reverse) + +M.shape = DEPRECATED ("41", "'std.list.shape'", + "use 'std.table.shape' instead", shape) +m.shape = DEPRECATED ("41", "'std.list:shape'", + "use 'std.table.shape' instead", + function (t, l) return shape (l, t) end) + +M.transpose = DEPRECATED ("41", "'std.list.transpose'", + "use 'std.functional.zip' instead", transpose) + +M.zip_with = DEPRECATED ("41", "'std.list.zip_with'", + "use 'std.functional.zip_with' instead", zip_with) + + + +--[[ ================== ]]-- +--[[ Type Declarations. ]]-- +--[[ ================== ]]-- + + +--- An Object derived List. +-- @object List + +List = Object { + -- Derived object type. + _type = "List", + _functions = M, -- FIXME: remove this when DEPRECATIONS have gone + __index = m, -- FIXME: `__index = M` when DEPRECATIONS have gone + + ------ + -- Concatenate lists. + -- @function __concat + -- @tparam List l a list + -- @tparam List|table m another list, or table (hash part is ignored) + -- @see concat + -- @usage + -- new = alist .. {"append", "these", "elements"} + __concat = concat, + + ------ + -- Append element to list. + -- @function __add + -- @tparam List l a list + -- @param e element to append + -- @see append + -- @usage + -- list = list + "element" + __add = append, + + ------ + -- List order operator. + -- @function __lt + -- @tparam List l a list + -- @tparam List m another list + -- @see compare + -- @usage + -- max = list1 > list2 and list1 or list2 + __lt = function (list1, list2) return compare (list1, list2) < 0 end, + + ------ + -- List equality or order operator. + -- @function __le + -- @tparam List l a list + -- @tparam List m another list + -- @see compare + -- @usage + -- min = list1 <= list2 and list1 or list2 + __le = function (list1, list2) return compare (list1, list2) <= 0 end, +} + + +return List diff --git a/lib/std/math.lua b/lib/std/math.lua new file mode 100644 index 0000000..2a87904 --- /dev/null +++ b/lib/std/math.lua @@ -0,0 +1,81 @@ +--[[-- + Additions to the core math module. + + The module table returned by `std.math` also contains all of the entries from + the core math table. An hygienic way to import this module, then, is simply + to override the core `math` locally: + + local math = require "std.math" + + @module std.math +]] + + +local base = require "std.base" + +local M + + +local _floor = math.floor + +local function floor (n, p) + if p and p ~= 0 then + local e = 10 ^ p + return _floor (n * e) / e + else + return _floor (n) + end +end + + +local function monkey_patch (namespace) + namespace = namespace or _G + namespace.math = base.copy (namespace.math or {}, M) + return M +end + + +local function round (n, p) + local e = 10 ^ (p or 0) + return _floor (n * e + 0.5) / e +end + + + +--[[ ================= ]]-- +--[[ Public Interface. ]]-- +--[[ ================= ]]-- + + +local function X (decl, fn) + return require "std.debug".argscheck ("std.math." .. decl, fn) +end + + +M = { + --- Extend `math.floor` to take the number of decimal places. + -- @function floor + -- @number n number + -- @int[opt=0] p number of decimal places to truncate to + -- @treturn number `n` truncated to `p` decimal places + -- @usage tenths = floor (magnitude, 1) + floor = X ("floor (number, ?int)", floor), + + --- Overwrite core `math` methods with `std` enhanced versions. + -- @function monkey_patch + -- @tparam[opt=_G] table namespace where to install global functions + -- @treturn table the module table + -- @usage require "std.math".monkey_patch () + monkey_patch = X ("monkey_patch (?table)", monkey_patch), + + --- Round a number to a given number of decimal places + -- @function round + -- @number n number + -- @int[opt=0] p number of decimal places to round to + -- @treturn number `n` rounded to `p` decimal places + -- @usage roughly = round (exactly, 2) + round = X ("round (number, ?int)", round), +} + + +return base.merge (M, math) diff --git a/lib/std/object.lua b/lib/std/object.lua new file mode 100644 index 0000000..423507f --- /dev/null +++ b/lib/std/object.lua @@ -0,0 +1,227 @@ +--[[-- + Prototype-based objects. + + This module creates the root prototype object from which every other + object is descended. There are no classes as such, rather new objects + are created by cloning an existing object, and then changing or adding + to the clone. Further objects can then be made by cloning the changed + object, and so on. + + Objects are cloned by simply calling an existing object, which then + serves as a prototype from which the new object is copied. + + Note that Object methods are stored in the `__index` field of their + metatable, and so cannot also use `__index` to lookup references with + square brackets. See @{std.container} objects if you want to do that. + + Prototype Chain + --------------- + + table + `-> Object + + @classmod std.object +]] + + +-- Surprise!! The real root object is Container, which has less +-- functionality than Object, but that makes the heirarchy hard to +-- explain, so the documentation pretends this is the root object, and +-- Container is derived from it. Confused? ;-) + + +local base = require "std.base" +local container = require "std.container" + +local Container = container {} +local getmetamethod, prototype = base.getmetamethod, base.prototype + + + +--- Root object. +-- +-- Changing the values of these fields in a new object will change the +-- corresponding behaviour. +-- @object Object +-- @string[opt="Object"] _type object name +-- @tfield[opt={}] table|function _init object initialisation +-- @tfield table _functions module functions omitted when cloned +-- @see __call +-- @usage +-- -- `_init` can be a list of keys; then the unnamed `init_1` through +-- -- `init_m` values from the argument table are assigned to the +-- -- corresponding keys in `new_object`. +-- local Process = Object { +-- _type = "Process", +-- _init = { "status", "out", "err" }, +-- } +-- local process = Process { +-- procs[pid].status, procs[pid].out, procs[pid].err, -- auto assigned +-- command = pipeline[pid], -- manual assignment +-- } +-- @usage +-- -- Or it can be a function, in which the arguments passed to the +-- -- prototype during cloning are simply handed to the `_init` function. +-- local Bag = Object { +-- _type = "Bag", +-- _init = function (obj, ...) +-- for e in std.elems {...} do +-- obj[#obj + 1] = e +-- end +-- return obj +-- end, +-- } +-- local bag = Bag ("function", "arguments", "sent", "to", "_init") + +return Container { + _type = "Object", + + -- No need for explicit module functions here, because calls to, e.g. + -- `Object.prototype` will automatically fall back metamethods in + -- `__index`. + + __index = { + --- Clone an Object. + -- + -- Objects are essentially tables of `field_n = value_n` pairs. + -- + -- Normally `new_object` automatically shares a metatable with + -- `proto_object`. However, field names beginning with "_" are *private*, + -- and moved into the object metatable during cloning. So, adding new + -- private fields to an object during cloning will result in a new + -- metatable for `new_object` that also happens to contain a copy of all + -- the entries from the `proto_object` metatable. + -- + -- While clones of @{Object} inherit all properties of their prototype, + -- it's idiomatic to always keep separate tables for the module table and + -- the root object itself: That way you can't mistakenly engage the slower + -- clone-from-module-table process unnecessarily. + -- @static + -- @function clone + -- @tparam Object obj an object + -- @param ... a list of arguments if *obj.\_init* is a function, or a + -- single table if *obj.\_init* is a table. + -- @treturn Object a clone of *obj* + -- @see __call + -- @usage + -- local object = require "std.object" -- module table + -- local Object = object {} -- root object + -- local o = Object { + -- field_1 = "value_1", + -- method_1 = function (self) return self.field_1 end, + -- } + -- print (o.field_1) --> value_1 + -- o.field_2 = 2 + -- function o:method_2 (n) return self.field_2 + n end + -- print (o:method_2 (2)) --> 4 + -- os.exit (0) + clone = getmetamethod (container, "__call"), + + --- Type of an object, or primitive. + -- + -- It's conventional to organise similar objects according to a + -- string valued *\_type* field, which can then be queried using this + -- function. + -- + -- Additionally, this function returns the results of @{io.type} for + -- file objects, or @{type} otherwise. + -- + -- @static + -- @function prototype + -- @param x anything + -- @treturn string type of *x* + -- @usage + -- local Stack = Object { + -- _type = "Stack", + -- + -- __tostring = function (self) ... end, + -- + -- __index = { + -- push = function (self) ... end, + -- pop = function (self) ... end, + -- }, + -- } + -- local stack = Stack {} + -- assert (stack:prototype () == getmetatable (stack)._type) + -- + -- local prototype = Object.prototype + -- assert (prototype (stack) == getmetatable (stack)._type) + -- + -- local h = io.open (os.tmpname (), "w") + -- assert (prototype (h) == io.type (h)) + -- + -- assert (prototype {} == type {}) + prototype = prototype, + + + --- Return *obj* with references to the fields of *src* merged in. + -- + -- More importantly, split the fields in *src* between *obj* and its + -- metatable. If any field names begin with "_", attach a metatable + -- to *obj* by cloning the metatable from *src*, and then copy the + -- "private" `_` prefixed fields there. + -- + -- You might want to use this function to instantiate your derived + -- object clones when the *src.\_init* is a function -- when + -- *src.\_init* is a table, the default (inherited unless you overwrite + -- it) clone method calls @{mapfields} automatically. When you're + -- using a function `_init` setting, @{clone} doesn't know what to + -- copy into a new object from the `_init` function's arguments... + -- so you're on your own. Except that calling @{mapfields} inside + -- `_init` is safer than manually splitting `src` into `obj` and + -- its metatable, because you'll pick up any fixes and changes when + -- you upgrade stdlib. + -- @static + -- @function mapfields + -- @tparam table obj destination object + -- @tparam table src fields to copy int clone + -- @tparam[opt={}] table map key renames as `{old_key=new_key, ...}` + -- @treturn table *obj* with non-private fields from *src* merged, + -- and a metatable with private fields (if any) merged, both sets + -- of keys renamed according to *map* + -- @usage + -- myobject.mapfields = function (obj, src, map) + -- object.mapfields (obj, src, map) + -- ... + -- end + mapfields = container.mapfields.call, + + + -- Backwards compatibility: + type = prototype, + }, + + + --- Return a @{clone} of this object, and its metatable. + -- + -- Private fields are stored in the metatable. + -- @function __call + -- @param ... arguments for prototype's *\_init* + -- @treturn Object a clone of the this object. + -- @see clone + -- @usage + -- local Object = require "std.object" {} -- not a typo! + -- new = Object {"initialisation", "elements"} + + + --- Return an in-order iterator over public object fields. + -- @function __pairs + -- @treturn function iterator function + -- @treturn Object *self* + -- @usage + -- for k, v in std.pairs (anobject) do process (k, v) end + + + --- Return a string representation of this object. + -- + -- First the object type, and then between { and } a list of the + -- array part of the object table (without numeric keys) followed + -- by the remaining key-value pairs. + -- + -- This function doesn't recurse explicity, but relies upon suitable + -- `__tostring` metamethods in field values. + -- @function __tostring + -- @treturn string stringified object representation + -- @see tostring + -- @usage print (anobject) +} diff --git a/lib/std/operator.lua b/lib/std/operator.lua new file mode 100644 index 0000000..1f77e78 --- /dev/null +++ b/lib/std/operator.lua @@ -0,0 +1,163 @@ +--[[-- + Functional forms of Lua operators. + + @module std.operator +]] + +local base = require "std.base" + +local tostring = base.tostring + + +local M = { + --- Stringify and concatenate arguments. + -- @param a an argument + -- @param b another argument + -- @return concatenation of stringified arguments. + -- @usage + -- --> "=> 1000010010" + -- functional.foldl (concat, "=> ", {10000, 100, 10}) + concat = function (a, b) return tostring (a) .. tostring (b) end, + + --- Dereference a table. + -- @tparam table t a table + -- @param k a key to lookup in *t* + -- @return value stored at *t[k]* if any, otherwise `nil` + -- @usage + -- --> 4 + -- functional.foldl (get, {1, {{2, 3, 4}, 5}}, {2, 1, 3}) + get = function (t, k) return t and t[k] or nil end, + + --- Set a table element, honoring metamethods. + -- @tparam table t a table + -- @param k a key to lookup in *t* + -- @param v a value to set for *k* + -- @treturn table *t* + -- @usage + -- -- destructive table merge: + -- --> {"one", bar="baz", two=5} + -- functional.reduce (set, {"foo", bar="baz"}, {"one", two=5}) + set = function (t, k, v) t[k]=v; return t end, + + --- Return the sum of the arguments. + -- @param a an argument + -- @param b another argument + -- @return the sum of the *a* and *b* + -- @usage + -- --> 10110 + -- functional.foldl (sum, {10000, 100, 10}) + sum = function (a, b) return a + b end, + + --- Return the difference of the arguments. + -- @param a an argument + -- @param b another argument + -- @return the difference between *a* and *b* + -- @usage + -- --> 890 + -- functional.foldl (diff, {10000, 100, 10}) + diff = function (a, b) return a - b end, + + --- Return the product of the arguments. + -- @param a an argument + -- @param b another argument + -- @return the product of *a* and *b* + -- @usage + -- --> 10000000 + -- functional.foldl (prod, {10000, 100, 10}) + prod = function (a, b) return a * b end, + + --- Return the quotient of the arguments. + -- @param a an argument + -- @param b another argument + -- @return the quotient *a* and *b* + -- @usage + -- --> 1000 + -- functional.foldr (quot, {10000, 100, 10}) + quot = function (a, b) return a / b end, + + --- Return the modulus of the arguments. + -- @param a an argument + -- @param b another argument + -- @return the modulus of *a* and *b* + -- @usage + -- --> 3 + -- functional.foldl (mod, {65536, 100, 11}) + mod = function (a, b) return a % b end, + + --- Return the exponent of the arguments. + -- @param a an argument + -- @param b another argument + -- @return the *a* to the power of *b* + -- @usage + -- --> 4096 + -- functional.foldl (pow, {2, 3, 4}) + pow = function (a, b) return a ^ b end, + + --- Return the logical conjunction of the arguments. + -- @param a an argument + -- @param b another argument + -- @return logical *a* and *b* + -- @usage + -- --> true + -- functional.foldl (conj, {true, 1, "false"}) + conj = function (a, b) return a and b end, + + --- Return the logical disjunction of the arguments. + -- @param a an argument + -- @param b another argument + -- @return logical *a* or *b* + -- @usage + -- --> true + -- functional.foldl (disj, {true, 1, false}) + disj = function (a, b) return a or b end, + + --- Return the logical negation of the arguments. + -- @param a an argument + -- @return not *a* + -- @usage + -- --> {true, false, false, false} + -- functional.bind (functional.map, {std.ielems, neg}) {false, true, 1, 0} + neg = function (a) return not a end, + + --- Return the equality of the arguments. + -- @param a an argument + -- @param b another argument + -- @return `true` if *a* is *b*, otherwise `false` + eq = function (a, b) return a == b end, + + --- Return the inequality of the arguments. + -- @param a an argument + -- @param b another argument + -- @return `false` if *a* is *b*, otherwise `true` + -- @usage + -- --> true + -- local f = require "std.functional" + -- table.empty (f.filter (f.bind (neq, {6}), std.ielems, {6, 6, 6}) + neq = function (a, b) return a ~= b end, + + --- Return whether the arguments are in ascending order. + -- @param a an argument + -- @param b another argument + -- @return `true` if *a* is less then *b*, otherwise `false` + lt = function (a, b) return a < b end, + + --- Return whether the arguments are not in descending order. + -- @param a an argument + -- @param b another argument + -- @return `true` if *a* is not greater then *b*, otherwise `false` + lte = function (a, b) return a <= b end, + + --- Return whether the arguments are in descending order. + -- @param a an argument + -- @param b another argument + -- @return `true` if *a* is greater then *b*, otherwise `false` + gt = function (a, b) return a > b end, + + --- Return whether the arguments are not in ascending order. + -- @param a an argument + -- @param b another argument + -- @return `true` if *a* is not greater then *b*, otherwise `false` + gte = function (a, b) return a >= b end, +} + +return M diff --git a/lib/std/optparse.lua b/lib/std/optparse.lua new file mode 100644 index 0000000..29ebe93 --- /dev/null +++ b/lib/std/optparse.lua @@ -0,0 +1,728 @@ +--[=[-- + Parse and process command line options. + + Prototype Chain + --------------- + + table + `-> Object + `-> OptionParser + + @classmod std.optparse +]=] + + +local base = require "std.base" + +local Object = require "std.object" {} + +local ipairs, pairs = base.ipairs, base.pairs +local insert, last, len = base.insert, base.last, base.len + + + +--[[ ================= ]]-- +--[[ Helper Functions. ]]-- +--[[ ================= ]]-- + + +local optional, required + + +--- Normalise an argument list. +-- Separate short options, remove `=` separators from +-- `--long-option=optarg` etc. +-- @local +-- @function normalise +-- @tparam table arglist list of arguments to normalise +-- @treturn table normalised argument list +local function normalise (self, arglist) + local normal = {} + local i = 0 + while i < len (arglist) do + i = i + 1 + local opt = arglist[i] + + -- Split '--long-option=option-argument'. + if opt:sub (1, 2) == "--" then + local x = opt:find ("=", 3, true) + if x then + local optname = opt:sub (1, x -1) + + -- Only split recognised long options. + if self[optname] then + insert (normal, optname) + insert (normal, opt:sub (x + 1)) + else + x = nil + end + end + + if x == nil then + -- No '=', or substring before '=' is not a known option name. + insert (normal, opt) + end + + elseif opt:sub (1, 1) == "-" and string.len (opt) > 2 then + local orig, split, rest = opt, {} + repeat + opt, rest = opt:sub (1, 2), opt:sub (3) + + split[#split + 1] = opt + + -- If there's no handler, the option was a typo, or not supposed + -- to be an option at all. + if self[opt] == nil then + opt, split = nil, { orig } + + -- Split '-xyz' into '-x -yz', and reiterate for '-yz' + elseif self[opt].handler ~= optional and + self[opt].handler ~= required then + if string.len (rest) > 0 then + opt = "-" .. rest + else + opt = nil + end + + -- Split '-xshortargument' into '-x shortargument'. + else + split[#split + 1] = rest + opt = nil + end + until opt == nil + + -- Append split options to normalised list + for _, v in ipairs (split) do insert (normal, v) end + else + insert (normal, opt) + end + end + + normal[-1], normal[0] = arglist[-1], arglist[0] + return normal +end + + +--- Store `value` with `opt`. +-- @local +-- @function set +-- @string opt option name +-- @param value option argument value +local function set (self, opt, value) + local key = self[opt].key + local opts = self.opts[key] + + if type (opts) == "table" then + insert (opts, value) + elseif opts ~= nil then + self.opts[key] = { opts, value } + else + self.opts[key] = value + end +end + + + +--[[ ============= ]]-- +--[[ Option Types. ]]-- +--[[ ============= ]]-- + + +--- Option at `arglist[i]` can take an argument. +-- Argument is accepted only if there is a following entry that does not +-- begin with a '-'. +-- +-- This is the handler automatically assigned to options that have +-- `--opt=[ARG]` style specifications in the @{OptionParser} spec +-- argument. You can also pass it as the `handler` argument to @{on} for +-- options you want to add manually without putting them in the +-- @{OptionParser} spec. +-- +-- Like @{required}, this handler will store multiple occurrences of a +-- command-line option. +-- @static +-- @tparam table arglist list of arguments +-- @int i index of last processed element of *arglist* +-- @param[opt=true] value either a function to process the option +-- argument, or a default value if encountered without an optarg +-- @treturn int index of next element of *arglist* to process +-- @usage +-- parser:on ("--enable-nls", parser.option, parser.boolean) +function optional (self, arglist, i, value) + if i + 1 <= len (arglist) and arglist[i + 1]:sub (1, 1) ~= "-" then + return self:required (arglist, i, value) + end + + if type (value) == "function" then + value = value (self, opt, nil) + elseif value == nil then + value = true + end + + set (self, arglist[i], value) + return i + 1 +end + + +--- Option at `arglist[i}` requires an argument. +-- +-- This is the handler automatically assigned to options that have +-- `--opt=ARG` style specifications in the @{OptionParser} spec argument. +-- You can also pass it as the `handler` argument to @{on} for options +-- you want to add manually without putting them in the @{OptionParser} +-- spec. +-- +-- Normally the value stored in the `opt` table by this handler will be +-- the string given as the argument to that option on the command line. +-- However, if the option is given on the command-line multiple times, +-- `opt["name"]` will end up with all those arguments stored in the +-- array part of a table: +-- +-- $ cat ./prog +-- ... +-- parser:on ({"-e", "-exec"}, required) +-- _G.arg, _G.opt = parser:parse (_G.arg) +-- print std.string.tostring (_G.opt.exec) +-- ... +-- $ ./prog -e '(foo bar)' -e '(foo baz)' -- qux +-- {1=(foo bar),2=(foo baz)} +-- @static +-- @tparam table arglist list of arguments +-- @int i index of last processed element of *arglist* +-- @param[opt] value either a function to process the option argument, +-- or a forced value to replace the user's option argument. +-- @treturn int index of next element of *arglist* to process +-- @usage +-- parser:on ({"-o", "--output"}, parser.required) +function required (self, arglist, i, value) + local opt = arglist[i] + if i + 1 > len (arglist) then + self:opterr ("option '" .. opt .. "' requires an argument") + return i + 1 + end + + if type (value) == "function" then + value = value (self, opt, arglist[i + 1]) + elseif value == nil then + value = arglist[i + 1] + end + + set (self, opt, value) + return i + 2 +end + + +--- Finish option processing +-- +-- This is the handler automatically assigned to the option written as +-- `--` in the @{OptionParser} spec argument. You can also pass it as +-- the `handler` argument to @{on} if you want to manually add an end +-- of options marker without writing it in the @{OptionParser} spec. +-- +-- This handler tells the parser to stop processing arguments, so that +-- anything after it will be an argument even if it otherwise looks +-- like an option. +-- @static +-- @tparam table arglist list of arguments +-- @int i index of last processed element of `arglist` +-- @treturn int index of next element of `arglist` to process +-- @usage +-- parser:on ("--", parser.finished) +local function finished (self, arglist, i) + for opt = i + 1, len (arglist) do + insert (self.unrecognised, arglist[opt]) + end + return 1 + len (arglist) +end + + +--- Option at `arglist[i]` is a boolean switch. +-- +-- This is the handler automatically assigned to options that have +-- `--long-opt` or `-x` style specifications in the @{OptionParser} spec +-- argument. You can also pass it as the `handler` argument to @{on} for +-- options you want to add manually without putting them in the +-- @{OptionParser} spec. +-- +-- Beware that, _unlike_ @{required}, this handler will store multiple +-- occurrences of a command-line option as a table **only** when given a +-- `value` function. Automatically assigned handlers do not do this, so +-- the option will simply be `true` if the option was given one or more +-- times on the command-line. +-- @static +-- @tparam table arglist list of arguments +-- @int i index of last processed element of *arglist* +-- @param[opt] value either a function to process the option argument, +-- or a value to store when this flag is encountered +-- @treturn int index of next element of *arglist* to process +-- @usage +-- parser:on ({"--long-opt", "-x"}, parser.flag) +local function flag (self, arglist, i, value) + local opt = arglist[i] + if type (value) == "function" then + set (self, opt, value (self, opt, true)) + elseif value == nil then + local key = self[opt].key + self.opts[key] = true + end + + return i + 1 +end + + +--- Option should display help text, then exit. +-- +-- This is the handler automatically assigned tooptions that have +-- `--help` in the specification, e.g. `-h, -?, --help`. +-- @static +-- @function help +-- @usage +-- parser:on ("-?", parser.version) +local function help (self) + print (self.helptext) + os.exit (0) +end + + +--- Option should display version text, then exit. +-- +-- This is the handler automatically assigned tooptions that have +-- `--version` in the specification, e.g. `-V, --version`. +-- @static +-- @function version +-- @usage +-- parser:on ("-V", parser.version) +local function version (self) + print (self.versiontext) + os.exit (0) +end + + + +--[[ =============== ]]-- +--[[ Argument Types. ]]-- +--[[ =============== ]]-- + + +--- Map various option strings to equivalent Lua boolean values. +-- @table boolvals +-- @field false false +-- @field 0 false +-- @field no false +-- @field n false +-- @field true true +-- @field 1 true +-- @field yes true +-- @field y true +local boolvals = { + ["false"] = false, ["true"] = true, + ["0"] = false, ["1"] = true, + no = false, yes = true, + n = false, y = true, +} + + +--- Return a Lua boolean equivalent of various *optarg* strings. +-- Report an option parse error if *optarg* is not recognised. +-- +-- Pass this as the `value` function to @{on} when you want various +-- "truthy" or "falsey" option arguments to be coerced to a Lua `true` +-- or `false` respectively in the options table. +-- @static +-- @string opt option name +-- @string[opt="1"] optarg option argument, must be a key in @{boolvals} +-- @treturn bool `true` or `false` +-- @usage +-- parser:on ("--enable-nls", parser.optional, parser.boolean) +local function boolean (self, opt, optarg) + if optarg == nil then optarg = "1" end -- default to truthy + local b = boolvals[tostring (optarg):lower ()] + if b == nil then + return self:opterr (optarg .. ": Not a valid argument to " ..opt[1] .. ".") + end + return b +end + + +--- Report an option parse error unless *optarg* names an +-- existing file. +-- +-- Pass this as the `value` function to @{on} when you want to accept +-- only option arguments that name an existing file. +-- @fixme this only checks whether the file has read permissions +-- @static +-- @string opt option name +-- @string optarg option argument, must be an existing file +-- @treturn string *optarg* +-- @usage +-- parser:on ("--config-file", parser.required, parser.file) +local function file (self, opt, optarg) + local h, errmsg = io.open (optarg, "r") + if h == nil then + return self:opterr (optarg .. ": " .. errmsg) + end + h:close () + return optarg +end + + + +--[[ =============== ]]-- +--[[ Option Parsing. ]]-- +--[[ =============== ]]-- + + +--- Report an option parse error, then exit with status 2. +-- +-- Use this in your custom option handlers for consistency with the +-- error output from built-in @{std.optparse} error messages. +-- @static +-- @string msg error message +local function opterr (self, msg) + local prog = self.program + -- Ensure final period. + if msg:match ("%.$") == nil then msg = msg .. "." end + io.stderr:write (prog .. ": error: " .. msg .. "\n") + io.stderr:write (prog .. ": Try '" .. prog .. " --help' for help.\n") + os.exit (2) +end + + +------ +-- Function signature of an option handler for @{on}. +-- @function on_handler +-- @tparam table arglist list of arguments +-- @int i index of last processed element of *arglist* +-- @param[opt=nil] value additional `value` registered with @{on} +-- @treturn int index of next element of *arglist* to process + + +--- Add an option handler. +-- +-- When the automatically assigned option handlers don't do everything +-- you require, or when you don't want to put an option into the +-- @{OptionParser} `spec` argument, use this function to specify custom +-- behaviour. If you write the option into the `spec` argument anyway, +-- calling this function will replace the automatically assigned handler +-- with your own. +-- +-- When writing your own handlers for @{std.optparse:on}, you only need +-- to deal with normalised arguments, because combined short arguments +-- (`-xyz`), equals separators to long options (`--long=ARG`) are fully +-- expanded before any handler is called. +-- @function on +-- @tparam[string|table] opts name of the option, or list of option names +-- @tparam on_handler handler function to call when any of *opts* is +-- encountered +-- @param value additional value passed to @{on_handler} +-- @usage +-- -- Don't process any arguments after `--` +-- parser:on ('--', parser.finished) +local function on (self, opts, handler, value) + if type (opts) == "string" then opts = { opts } end + handler = handler or flag -- unspecified options behave as flags + + local normal = {} + for _, optspec in ipairs (opts) do + optspec:gsub ("(%S+)", + function (opt) + -- 'x' => '-x' + if string.len (opt) == 1 then + opt = "-" .. opt + + -- 'option-name' => '--option-name' + elseif opt:match ("^[^%-]") ~= nil then + opt = "--" .. opt + end + + if opt:match ("^%-[^%-]+") ~= nil then + -- '-xyz' => '-x -y -z' + for i = 2, string.len (opt) do + insert (normal, "-" .. opt:sub (i, i)) + end + else + insert (normal, opt) + end + end) + end + + -- strip leading '-', and convert non-alphanums to '_' + local key = last (normal):match ("^%-*(.*)$"):gsub ("%W", "_") + + for _, opt in ipairs (normal) do + self[opt] = { key = key, handler = handler, value = value } + end +end + + +------ +-- Parsed options table, with a key for each encountered option, each +-- with value set by that option's @{on_handler}. Where an option +-- has one or more long-options specified, the key will be the first +-- one of those with leading hyphens stripped and non-alphanumeric +-- characters replaced with underscores. For options that can only be +-- specified by a short option, the key will be the letter of the first +-- of the specified short options: +-- +-- {"-e", "--eval-file"} => opts.eval_file +-- {"-n", "--dryrun", "--dry-run"} => opts.dryrun +-- {"-t", "-T"} => opts.t +-- +-- Generally there will be one key for each previously specified +-- option (either automatically assigned by @{OptionParser} or +-- added manually with @{on}) containing the value(s) assigned by the +-- associated @{on_handler}. For automatically assigned handlers, +-- that means `true` for straight-forward flags and +-- optional-argument options for which no argument was given; or else +-- the string value of the argument passed with an option given only +-- once; or a table of string values of the same for arguments given +-- multiple times. +-- +-- ./prog -x -n -x => opts = { x = true, dryrun = true } +-- ./prog -e '(foo bar)' -e '(foo baz)' +-- => opts = {eval_file = {"(foo bar)", "(foo baz)"} } +-- +-- If you write your own handlers, or otherwise specify custom +-- handling of options with @{on}, then whatever value those handlers +-- return will be assigned to the respective keys in `opts`. +-- @table opts + + +--- Parse an argument list. +-- @tparam table arglist list of arguments +-- @tparam[opt] table defaults table of default option values +-- @treturn table a list of unrecognised *arglist* elements +-- @treturn opts parsing results +local function parse (self, arglist, defaults) + self.unrecognised, self.opts = {}, {} + + arglist = normalise (self, arglist) + + local i = 1 + while i > 0 and i <= len (arglist) do + local opt = arglist[i] + + if self[opt] == nil then + insert (self.unrecognised, opt) + i = i + 1 + + -- Following non-'-' prefixed argument is an optarg. + if i <= len (arglist) and arglist[i]:match "^[^%-]" then + insert (self.unrecognised, arglist[i]) + i = i + 1 + end + + -- Run option handler functions. + else + assert (type (self[opt].handler) == "function") + + i = self[opt].handler (self, arglist, i, self[opt].value) + end + end + + -- Merge defaults into user options. + for k, v in pairs (defaults or {}) do + if self.opts[k] == nil then self.opts[k] = v end + end + + -- metatable allows `io.warn` to find `parser.program` when assigned + -- back to _G.opts. + return self.unrecognised, setmetatable (self.opts, {__index = self}) +end + + +--- Take care not to register duplicate handlers. +-- @param current current handler value +-- @param new new handler value +-- @return `new` if `current` is nil +local function set_handler (current, new) + assert (current == nil, "only one handler per option") + return new +end + + +local function _init (_, spec) + local parser = {} + + parser.versiontext, parser.version, parser.helptext, parser.program = + spec:match ("^([^\n]-(%S+)\n.-)%s*([Uu]sage: (%S+).-)%s*$") + + if parser.versiontext == nil then + error ("OptionParser spec argument must match '\\n" .. + "...Usage: ...'") + end + + -- Collect helptext lines that begin with two or more spaces followed + -- by a '-'. + local specs = {} + parser.helptext:gsub ("\n %s*(%-[^\n]+)", + function (spec) insert (specs, spec) end) + + -- Register option handlers according to the help text. + for _, spec in ipairs (specs) do + local options, handler = {} + + -- Loop around each '-' prefixed option on this line. + while spec:sub (1, 1) == "-" do + + -- Capture end of options processing marker. + if spec:match "^%-%-,?%s" then + handler = set_handler (handler, finished) + + -- Capture optional argument in the option string. + elseif spec:match "^%-[%-%w]+=%[.+%],?%s" then + handler = set_handler (handler, optional) + + -- Capture required argument in the option string. + elseif spec:match "^%-[%-%w]+=%S+,?%s" then + handler = set_handler (handler, required) + + -- Capture any specially handled arguments. + elseif spec:match "^%-%-help,?%s" then + handler = set_handler (handler, help) + + elseif spec:match "^%-%-version,?%s" then + handler = set_handler (handler, version) + end + + -- Consume argument spec, now that it was processed above. + spec = spec:gsub ("^(%-[%-%w]+)=%S+%s", "%1 ") + + -- Consume short option. + local _, c = spec:gsub ("^%-([-%w]),?%s+(.*)$", + function (opt, rest) + if opt == "-" then opt = "--" end + insert (options, opt) + spec = rest + end) + + -- Be careful not to consume more than one option per iteration, + -- otherwise we might miss a handler test at the next loop. + if c == 0 then + -- Consume long option. + spec:gsub ("^%-%-([%-%w]+),?%s+(.*)$", + function (opt, rest) + insert (options, opt) + spec = rest + end) + end + end + + -- Unless specified otherwise, treat each option as a flag. + on (parser, options, handler or flag) + end + + return parser +end + + +--- Signature for initialising a custom OptionParser. +-- +-- Read the documented options from *spec* and return custom parser that +-- can be used for parsing the options described in *spec* from a run-time +-- argument list. Options in *spec* are recognised as lines that begin +-- with at least two spaces, followed by a hyphen. +-- @static +-- @function OptionParser_Init +-- @string spec option parsing specification +-- @treturn OptionParser a parser for options described by *spec* +-- @usage +-- customparser = std.optparse (optparse_spec) + + +--- OptionParser prototype object. +-- +-- Most often, after instantiating an @{OptionParser}, everything else +-- is handled automatically. +-- +-- Then, calling `parser:parse` as shown below saves unparsed arguments +-- into `_G.arg` (usually filenames or similar), and `_G.opts` will be a +-- table of successfully parsed option values. The keys into this table +-- are the long-options with leading hyphens stripped, and non-word +-- characters turned to `_`. For example if `--another-long` had been +-- found in the initial `_G.arg`, then `_G.opts` will have a key named +-- `another_long`, with an appropriate value. If there is no long +-- option name, then the short option is used, i.e. `_G.opts.b` will be +-- set. +-- +-- The values saved against those keys are controlled by the option +-- handler, usually just `true` or the option argument string as +-- appropriate. +-- @object OptionParser +-- @tparam OptionParser_Init _init initialisation function +-- @string program the first word following "Usage:" from *spec* +-- @string version the last white-space delimited word on the first line +-- of text from *spec* +-- @string versiontext everything preceding "Usage:" from *spec*, and +-- which will be displayed by the @{version} @{on_handler} +-- @string helptext everything including and following "Usage:" from +-- *spec* string and which will be displayed by the @{help} +-- @{on_handler} +-- @usage +-- local std = require "std" +-- +-- local optparser = std.optparse [[ +-- any text VERSION +-- Additional lines of text to show when the --version +-- option is passed. +-- +-- Several lines or paragraphs are permitted. +-- +-- Usage: PROGNAME +-- +-- Banner text. +-- +-- Optional long description text to show when the --help +-- option is passed. +-- +-- Several lines or paragraphs of long description are permitted. +-- +-- Options: +-- +-- -b a short option with no long option +-- --long a long option with no short option +-- --another-long a long option with internal hypen +-- -v, --verbose a combined short and long option +-- -n, --dryrun, --dry-run several spellings of the same option +-- -u, --name=USER require an argument +-- -o, --output=[FILE] accept an optional argument +-- --version display version information, then exit +-- --help display this help, then exit +-- +-- Footer text. Several lines or paragraphs are permitted. +-- +-- Please report bugs at bug-list@yourhost.com +-- ]] +-- +-- -- Note that @{std.io.die} and @{std.io.warn} will only prefix messages +-- -- with `parser.program` if the parser options are assigned back to +-- -- `_G.opts`: +-- _G.arg, _G.opts = optparser:parse (_G.arg) +return Object { + _type = "OptionParser", + + _init = _init, + + -- Prototype initial values. + opts = {}, + helptext = "", + program = "", + versiontext = "", + version = 0, + + --- @export + __index = { + boolean = boolean, + file = file, + finished = finished, + flag = flag, + help = help, + optional = optional, + required = required, + version = version, + + on = on, + opterr = opterr, + parse = parse, + }, +} diff --git a/lib/std/package.lua b/lib/std/package.lua new file mode 100644 index 0000000..562c531 --- /dev/null +++ b/lib/std/package.lua @@ -0,0 +1,217 @@ +--[[-- + Additions to the core package module. + + The module table returned by `std.package` also contains all of the entries + from the core `package` table. An hygienic way to import this module, then, is + simply to override core `package` locally: + + local package = require "std.package" + + @module std.package +]] + + +local base = require "std.base" +local debug = require "std.debug" + +local catfile, escape_pattern, invert = + base.catfile, base.escape_pattern, base.invert +local ipairs, pairs, split, unpack = + base.ipairs, base.pairs, base.split, base.unpack + +local M + + + +--- Make named constants for `package.config` +-- (undocumented in 5.1; see luaconf.h for C equivalents). +-- @table package +-- @string dirsep directory separator +-- @string pathsep path separator +-- @string path_mark string that marks substitution points in a path template +-- @string execdir (Windows only) replaced by the executable's directory in a path +-- @string igmark Mark to ignore all before it when building `luaopen_` function name. +local dirsep, pathsep, path_mark, execdir, igmark = + string.match (package.config, "^([^\n]+)\n([^\n]+)\n([^\n]+)\n([^\n]+)\n([^\n]+)") + + +local function pathsub (path) + return path:gsub ("%%?.", function (capture) + if capture == "?" then + return path_mark + elseif capture == "/" then + return dirsep + else + return capture:gsub ("^%%", "", 1) + end + end) +end + + +local function find (pathstrings, patt, init, plain) + local paths = split (pathstrings, pathsep) + if plain then patt = escape_pattern (patt) end + init = init or 1 + if init < 0 then init = #paths - init end + for i = init, #paths do + if paths[i]:find (patt) then return i, paths[i] end + end +end + + +local function normalize (...) + local i, paths, pathstrings = 1, {}, table.concat ({...}, pathsep) + for _, path in ipairs (split (pathstrings, pathsep)) do + path = pathsub (path): + gsub (catfile ("^[^", "]"), catfile (".", "%0")): + gsub (catfile ("", "%.", ""), dirsep): + gsub (catfile ("", "%.$"), ""): + gsub (catfile ("^%.", "%..", ""), catfile ("..", "")): + gsub (catfile ("", "$"), "") + + -- Carefully remove redundant /foo/../ matches. + repeat + local again = false + path = path:gsub (catfile ("", "([^", "]+)", "%.%.", ""), + function (dir1) + if dir1 == ".." then -- don't remove /../../ + return catfile ("", "..", "..", "") + else + again = true + return dirsep + end + end): + gsub (catfile ("", "([^", "]+)", "%.%.$"), + function (dir1) + if dir1 == ".." then -- don't remove /../.. + return catfile ("", "..", "..") + else + again = true + return "" + end + end) + until again == false + + -- Build an inverted table of elements to eliminate duplicates after + -- normalization. + if not paths[path] then + paths[path], i = i, i + 1 + end + end + return table.concat (invert (paths), pathsep) +end + + +local function insert (pathstrings, ...) + local paths = split (pathstrings, pathsep) + table.insert (paths, ...) + return normalize (unpack (paths)) +end + + +local function mappath (pathstrings, callback, ...) + for _, path in ipairs (split (pathstrings, pathsep)) do + local r = callback (path, ...) + if r ~= nil then return r end + end +end + + +local function remove (pathstrings, pos) + local paths = split (pathstrings, pathsep) + table.remove (paths, pos) + return table.concat (paths, pathsep) +end + + + +--[[ ================= ]]-- +--[[ Public Interface. ]]-- +--[[ ================= ]]-- + + +local function X (decl, fn) + return debug.argscheck ("std.package." .. decl, fn) +end + +M = { + --- Look for a path segment match of *patt* in *pathstrings*. + -- @function find + -- @string pathstrings `pathsep` delimited path elements + -- @string patt a Lua pattern to search for in *pathstrings* + -- @int[opt=1] init element (not byte index!) to start search at. + -- Negative numbers begin counting backwards from the last element + -- @bool[opt=false] plain unless false, treat *patt* as a plain + -- string, not a pattern. Note that if *plain* is given, then *init* + -- must be given as well. + -- @return the matching element number (not byte index!) and full text + -- of the matching element, if any; otherwise nil + -- @usage i, s = find (package.path, "^[^" .. package.dirsep .. "/]") + find = X ("find (string, string, ?int, ?boolean|:plain)", find), + + --- Insert a new element into a `package.path` like string of paths. + -- @function insert + -- @string pathstrings a `package.path` like string + -- @int[opt=n+1] pos element index at which to insert *value*, where `n` is + -- the number of elements prior to insertion + -- @string value new path element to insert + -- @treturn string a new string with the new element inserted + -- @usage + -- package.path = insert (package.path, 1, install_dir .. "/?.lua") + insert = X ("insert (string, [int], string)", insert), + + --- Call a function with each element of a path string. + -- @function mappath + -- @string pathstrings a `package.path` like string + -- @tparam mappathcb callback function to call for each element + -- @param ... additional arguments passed to *callback* + -- @return nil, or first non-nil returned by *callback* + -- @usage mappath (package.path, searcherfn, transformfn) + mappath = X ("mappath (string, function, [any...])", mappath), + + --- Normalize a path list. + -- Removing redundant `.` and `..` directories, and keep only the first + -- instance of duplicate elements. Each argument can contain any number + -- of `pathsep` delimited elements; wherein characters are subject to + -- `/` and `?` normalization, converting `/` to `dirsep` and `?` to + -- `path_mark` (unless immediately preceded by a `%` character). + -- @function normalize + -- @param ... path elements + -- @treturn string a single normalized `pathsep` delimited paths string + -- @usage package.path = normalize (user_paths, sys_paths, package.path) + normalize = X ("normalize (string...)", normalize), + + --- Remove any element from a `package.path` like string of paths. + -- @function remove + -- @string pathstrings a `package.path` like string + -- @int[opt=n] pos element index from which to remove an item, where `n` + -- is the number of elements prior to removal + -- @treturn string a new string with given element removed + -- @usage package.path = remove (package.path) + remove = X ("remove (string, ?int)", remove), +} + + +M.dirsep = dirsep +M.execdir = execdir +M.igmark = igmark +M.path_mark = path_mark +M.pathsep = pathsep + + +for k, v in pairs (package) do + M[k] = M[k] or v +end + +return M + + +--- Types +-- @section Types + +--- Function signature of a callback for @{mappath}. +-- @function mappathcb +-- @string element an element from a `pathsep` delimited string of +-- paths +-- @param ... additional arguments propagated from @{mappath} +-- @return non-nil to break, otherwise continue with the next element diff --git a/lib/std/set.lua b/lib/std/set.lua new file mode 100644 index 0000000..dacb33a --- /dev/null +++ b/lib/std/set.lua @@ -0,0 +1,363 @@ +--[[-- + Set container prototype. + + Note that Functions listed below are only available from the Set + prototype returned by requiring this module, because Container + objects cannot have object methods. + + Prototype Chain + --------------- + + table + `-> Object + `-> Container + `-> Set + + @classmod std.set + @see std.container + ]] + +local base = require "std.base" + +local Container = require "std.container" {} + +local ielems, pairs, prototype = base.ielems, base.pairs, base.prototype + + +local Set -- forward declaration + + + +--[[ ==================== ]]-- +--[[ Primitive Functions. ]]-- +--[[ ==================== ]]-- + + +-- These functions know about internal implementatation. +-- The representation is a table whose tags are the elements, and +-- whose values are true. + + +local elems = base.pairs + + +local function insert (set, e) + return rawset (set, e, true) +end + + +local function member (set, e) + return rawget (set, e) == true +end + + + +--[[ ===================== ]]-- +--[[ High Level Functions. ]]-- +--[[ ===================== ]]-- + + +-- These functions are independent of the internal implementation. + + +local difference, symmetric_difference, intersection, union, subset, + proper_subset, equal + + +function difference (set1, set2) + local r = Set {} + for e in elems (set1) do + if not member (set2, e) then + insert (r, e) + end + end + return r +end + + +function symmetric_difference (set1, set2) + return difference (union (set1, set2), intersection (set2, set1)) +end + + +function intersection (set1, set2) + local r = Set {} + for e in elems (set1) do + if member (set2, e) then + insert (r, e) + end + end + return r +end + + +function union (set1, set2) + local r = set1 {} + for e in elems (set2) do + insert (r, e) + end + return r +end + + +function subset (set1, set2) + for e in elems (set1) do + if not member (set2, e) then + return false + end + end + return true +end + + +function proper_subset (set1, set2) + return subset (set1, set2) and not subset (set2, set1) +end + + +function equal (set1, set2) + return subset (set1, set2) and subset (set2, set1) +end + + + +--[[ =========== ]]-- +--[[ Set Object. ]]-- +--[[ =========== ]]-- + + +local function X (decl, fn) + return require "std.debug".argscheck ("std.set." .. decl, fn) +end + + +--- Set prototype object. +-- +-- Set also inherits all the fields and methods from +-- @{std.container.Container}. +-- @object Set +-- @string[opt="Set"] _type object name +-- @see std.container +-- @see std.object.__call +-- @usage +-- local std = require "std" +-- std.prototype (std.set) --> "Set" +-- os.exit (0) +Set = Container { + _type = "Set", + + _init = function (self, t) + for e in ielems (t) do + insert (self, e) + end + return self + end, + + --- Union operator. + -- @static + -- @function __add + -- @tparam Set set1 a set + -- @tparam Set set2 another set + -- @treturn Set everything from *set1* plus everything from *set2* + -- @see union + -- @usage + -- union = set1 + set2 + __add = union, + + --- Difference operator. + -- @static + -- @function __sub + -- @tparam Set set1 a set + -- @tparam Set set2 another set + -- @treturn Set everything from *set1* that is not also in *set2* + -- @see difference + -- @usage + -- difference = set1 - set2 + __sub = difference, + + --- Intersection operator. + -- @static + -- @function __mul + -- @tparam Set set1 a set + -- @tparam Set set2 another set + -- @treturn Set anything this is in both *set1* and *set2* + -- @see intersection + -- @usage + -- intersection = set1 * set2 + __mul = intersection, + + --- Symmetric difference operator. + -- @function __div + -- @static + -- @tparam Set set1 a set + -- @tparam Set set2 another set + -- @treturn Set everything from *set1* or *set2* but not both + -- @see symmetric_difference + -- @usage + -- symmetric_difference = set1 / set2 + __div = symmetric_difference, + + --- Subset operator. + -- @static + -- @function __le + -- @tparam Set set1 a set + -- @tparam Set set2 another set + -- @treturn boolean `true` if everything in *set1* is also in *set2* + -- @see subset + -- @usage + -- issubset = set1 <= set2 + __le = subset, + + --- Proper subset operator. + -- @static + -- @function __lt + -- @tparam Set set1 set + -- @tparam Set set2 another set + -- @treturn boolean `true` if *set2* is not equal to *set1*, but does + -- contain everything from *set1* + -- @see proper_subset + -- @usage + -- ispropersubset = set1 < set2 + __lt = proper_subset, + + -- Return a string representation of this set. + -- @treturn string string representation of a set. + -- @see std.tostring + __tostring = function (self) + local keys = {} + for k in pairs (self) do + keys[#keys + 1] = tostring (k) + end + table.sort (keys) + return prototype (self) .. " {" .. table.concat (keys, ", ") .. "}" + end, + + + _functions = { + --- Delete an element from a set. + -- @static + -- @function delete + -- @tparam Set set a set + -- @param e element + -- @treturn Set the modified *set* + -- @usage + -- set.delete (available, found) + delete = X ("delete (Set, any)", + function (set, e) return rawset (set, e, nil) end), + + --- Find the difference of two sets. + -- @static + -- @function difference + -- @tparam Set set1 a set + -- @tparam Set set2 another set + -- @treturn Set a copy of *set1* with elements of *set2* removed + -- @usage + -- all = set.difference (all, {32, 49, 56}) + difference = X ("difference (Set, Set)", difference), + + --- Iterator for sets. + -- @static + -- @function elems + -- @tparam Set set a set + -- @treturn *set* iterator + -- @todo Make the iterator return only the key + -- @usage + -- for code in set.elems (isprintable) do print (code) end + elems = X ("elems (Set)", elems), + + --- Find whether two sets are equal. + -- @static + -- @function equal + -- @tparam Set set1 a set + -- @tparam Set set2 another set + -- @treturn boolean `true` if *set1* and *set2* each contain identical + -- elements, `false` otherwise + -- @usage + -- if set.equal (keys, {META, CTRL, "x"}) then process (keys) end + equal = X ( "equal (Set, Set)", equal), + + --- Insert an element into a set. + -- @static + -- @function insert + -- @tparam Set set a set + -- @param e element + -- @treturn Set the modified *set* + -- @usage + -- for byte = 32,126 do + -- set.insert (isprintable, string.char (byte)) + -- end + insert = X ("insert (Set, any)", insert), + + --- Find the intersection of two sets. + -- @static + -- @function intersection + -- @tparam Set set1 a set + -- @tparam Set set2 another set + -- @treturn Set a new set with elements in both *set1* and *set2* + -- @usage + -- common = set.intersection (a, b) + intersection = X ("intersection (Set, Set)", intersection), + + --- Say whether an element is in a set. + -- @static + -- @function difference + -- @tparam Set set a set + -- @param e element + -- @return `true` if *e* is in *set*, otherwise `false` + -- otherwise + -- @usage + -- if not set.member (keyset, pressed) then return nil end + member = X ("member (Set, any)", member), + + --- Find whether one set is a proper subset of another. + -- @static + -- @function proper_subset + -- @tparam Set set1 a set + -- @tparam Set set2 another set + -- @treturn boolean `true` if *set2* contains all elements in *set1* + -- but not only those elements, `false` otherwise + -- @usage + -- if set.proper_subset (a, b) then + -- for e in set.elems (set.difference (b, a)) do + -- set.delete (b, e) + -- end + -- end + -- assert (set.equal (a, b)) + proper_subset = X ("proper_subset (Set, Set)", proper_subset), + + --- Find whether one set is a subset of another. + -- @static + -- @function subset + -- @tparam Set set1 a set + -- @tparam Set set2 another set + -- @treturn boolean `true` if all elements in *set1* are also in *set2*, + -- `false` otherwise + -- @usage + -- if set.subset (a, b) then a = b end + subset = X ("subset (Set, Set)", subset), + + --- Find the symmetric difference of two sets. + -- @static + -- @function symmetric_difference + -- @tparam Set set1 a set + -- @tparam Set set2 another set + -- @treturn Set a new set with elements that are in *set1* or *set2* + -- but not both + -- @usage + -- unique = set.symmetric_difference (a, b) + symmetric_difference = X ("symmetric_difference (Set, Set)", + symmetric_difference), + + --- Find the union of two sets. + -- @static + -- @function union + -- @tparam Set set1 a set + -- @tparam Set set2 another set + -- @treturn Set a copy of *set1* with elements in *set2* merged in + -- @usage + -- all = set.union (a, b) + union = X ("union (Set, Set)", union), + }, +} + +return Set diff --git a/lib/std/strbuf.lua b/lib/std/strbuf.lua new file mode 100644 index 0000000..d23af9a --- /dev/null +++ b/lib/std/strbuf.lua @@ -0,0 +1,130 @@ +--[[-- + String buffers. + + Buffers are mutable by default, but being based on objects, they can + also be used in a functional style: + + local StrBuf = require "std.strbuf" {} + local a = StrBuf {"a"} + local b = a:concat "b" -- mutate *a* + print (a, b) --> ab ab + local c = a {} .. "c" -- copy and append + print (a, c) --> ab abc + + Prototype Chain + --------------- + + table + `-> Object + `-> StrBuf + + @classmod std.strbuf +]] + +local base = require "std.base" +local debug = require "std.debug" + +local Object = require "std.object" {} + +local ielems, insert, prototype = base.ielems, base.insert, base.prototype + +local M, StrBuf + + +local function __concat (self, x) + return insert (self, x) +end + + +local function __tostring (self) + local strs = {} + for e in ielems (self) do strs[#strs + 1] = tostring (e) end + return table.concat (strs) +end + + +--[[ ================= ]]-- +--[[ Public Interface. ]]-- +--[[ ================= ]]-- + + +local function X (decl, fn) + return debug.argscheck ("std.strbuf." .. decl, fn) +end + + +M = { + --- Add a object to a buffer. + -- Elements are stringified lazily, so if add a table and then change + -- its contents, the contents of the buffer will be affected too. + -- @static + -- @function concat + -- @param x object to add to buffer + -- @treturn StrBuf modified buffer + -- @usage + -- buf = buf:concat "append this" {" and", " this"} + concat = X ("concat (StrBuf, any)", __concat), +} + + + +--[[ ============= ]]-- +--[[ Deprecations. ]]-- +--[[ ============= ]]-- + + +local DEPRECATED = debug.DEPRECATED + +M.tostring = DEPRECATED ("41.1", "std.strbuf.tostring", + "use 'tostring (strbuf)' instead", + X ("tostring (StrBuf)", __tostring)) + + +--[[ ================== ]]-- +--[[ Type Declarations. ]]-- +--[[ ================== ]]-- + + +--- StrBuf prototype object. +-- +-- Set also inherits all the fields and methods from +-- @{std.object.Object}. +-- @object StrBuf +-- @string[opt="StrBuf"] _type object name +-- @see std.object.__call +-- @usage +-- local std = require "std" +-- local StrBuf = std.strbuf {} +-- local a = {1, 2, 3} +-- local b = {a, "five", "six"} +-- a = a .. 4 +-- b = b:concat "seven" +-- print (a, b) --> 1234 1234fivesixseven +-- os.exit (0) +StrBuf = Object { + _type = "StrBuf", + + __index = M, + + --- Support concatenation to StrBuf objects. + -- @function __concat + -- @tparam StrBuf buffer object + -- @param x a string, or object that can be coerced to a string + -- @treturn StrBuf modified *buf* + -- @see concat + -- @usage + -- buf = buf .. x + __concat = __concat, + + --- Support fast conversion to Lua string. + -- @function __tostring + -- @tparam StrBuf buffer object + -- @treturn string concatenation of buffer contents + -- @see tostring + -- @usage + -- str = tostring (buf) + __tostring = __tostring, +} + + +return StrBuf diff --git a/lib/std/strict.lua b/lib/std/strict.lua new file mode 100644 index 0000000..6d7be04 --- /dev/null +++ b/lib/std/strict.lua @@ -0,0 +1,62 @@ +--[[-- + Checks uses of undeclared global variables. + + All global variables must be 'declared' through a regular + assignment (even assigning `nil` will do) in a top-level + chunk before being used anywhere or assigned to inside a function. + + To use this module, just require it near the start of your program. + + From Lua distribution (`etc/strict.lua`). + + @module std.strict +]] + +local getinfo, error, rawset, rawget = debug.getinfo, error, rawset, rawget + +local mt = getmetatable (_G) +if mt == nil then + mt = {} + setmetatable (_G, mt) +end + + +-- The set of globally declared variables. +mt.__declared = {} + + +--- What kind of variable declaration is this? +-- @treturn string "C", "Lua" or "main" +local function what () + local d = getinfo (3, "S") + return d and d.what or "C" +end + + +--- Detect assignment to undeclared global. +-- @function __newindex +-- @tparam table t `_G` +-- @string n name of the variable being declared +-- @param v initial value of the variable +mt.__newindex = function (t, n, v) + if not mt.__declared[n] then + local w = what () + if w ~= "main" and w ~= "C" then + error ("assignment to undeclared variable '" .. n .. "'", 2) + end + mt.__declared[n] = true + end + rawset (t, n, v) +end + + +--- Detect dereference of undeclared global. +-- @function __index +-- @tparam table t `_G` +-- @string n name of the variable being dereferenced +mt.__index = function (t, n) + if not mt.__declared[n] and what () ~= "C" then + error ("variable '" .. n .. "' is not declared", 2) + end + return rawget (t, n) +end diff --git a/lib/std/string.lua b/lib/std/string.lua new file mode 100644 index 0000000..ca0e151 --- /dev/null +++ b/lib/std/string.lua @@ -0,0 +1,552 @@ +--[[-- + Additions to the core string module. + + The module table returned by `std.string` also contains all of the entries + from the core string table. An hygienic way to import this module, then, is + simply to override the core `string` locally: + + local string = require "std.string" + + @module std.string +]] + +local base = require "std.base" +local debug = require "std.debug" + +local StrBuf = require "std.strbuf" {} + +local copy = base.copy +local getmetamethod = base.getmetamethod +local insert, len = base.insert, base.len +local pairs = base.pairs +local render = base.render + +local M + + + +local _tostring = base.tostring + +local function __concat (s, o) + return _tostring (s) .. _tostring (o) +end + + +local function __index (s, i) + if type (i) == "number" then + return s:sub (i, i) + else + -- Fall back to module metamethods + return M[i] + end +end + + +local _format = string.format + +local function format (f, arg1, ...) + return (arg1 ~= nil) and _format (f, arg1, ...) or f +end + + +local function tpack (from, to, ...) + return from, to, {...} +end + +local function tfind (s, ...) + return tpack (s:find (...)) +end + + +local function finds (s, p, i, ...) + i = i or 1 + local l = {} + local from, to, r + repeat + from, to, r = tfind (s, p, i, ...) + if from ~= nil then + insert (l, {from, to, capt = r}) + i = to + 1 + end + until not from + return l +end + + +local function monkey_patch (namespace) + namespace = namespace or _G + namespace.string = base.copy (namespace.string or {}, M) + + local string_metatable = getmetatable "" + string_metatable.__concat = M.__concat + string_metatable.__index = M.__index + + return M +end + + +local function caps (s) + return (s:gsub ("(%w)([%w]*)", function (l, ls) return l:upper () .. ls end)) +end + + +local function escape_shell (s) + return (s:gsub ("([ %(%)%\\%[%]\"'])", "\\%1")) +end + + +local function ordinal_suffix (n) + n = math.abs (n) % 100 + local d = n % 10 + if d == 1 and n ~= 11 then + return "st" + elseif d == 2 and n ~= 12 then + return "nd" + elseif d == 3 and n ~= 13 then + return "rd" + else + return "th" + end +end + + +local function pad (s, w, p) + p = string.rep (p or " ", math.abs (w)) + if w < 0 then + return string.sub (p .. s, w) + end + return string.sub (s .. p, 1, w) +end + + +local function wrap (s, w, ind, ind1) + w = w or 78 + ind = ind or 0 + ind1 = ind1 or ind + assert (ind1 < w and ind < w, + "the indents must be less than the line width") + local r = StrBuf { string.rep (" ", ind1) } + local i, lstart, lens = 1, ind1, len (s) + while i <= lens do + local j = i + w - lstart + while len (s[j]) > 0 and s[j] ~= " " and j > i do + j = j - 1 + end + local ni = j + 1 + while s[j] == " " do + j = j - 1 + end + r:concat (s:sub (i, j)) + i = ni + if i < lens then + r:concat ("\n" .. string.rep (" ", ind)) + lstart = ind + end + end + return r:tostring () +end + + +local function numbertosi (n) + local SIprefix = { + [-8] = "y", [-7] = "z", [-6] = "a", [-5] = "f", + [-4] = "p", [-3] = "n", [-2] = "mu", [-1] = "m", + [0] = "", [1] = "k", [2] = "M", [3] = "G", + [4] = "T", [5] = "P", [6] = "E", [7] = "Z", + [8] = "Y" + } + local t = format("% #.2e", n) + local _, _, m, e = t:find(".(.%...)e(.+)") + local man, exp = tonumber (m), tonumber (e) + local siexp = math.floor (exp / 3) + local shift = exp - siexp * 3 + local s = SIprefix[siexp] or "e" .. tostring (siexp) + man = man * (10 ^ shift) + return format ("%0.f", man) .. s +end + + +local function trim (s, r) + r = r or "%s+" + return (s:gsub ("^" .. r, ""):gsub (r .. "$", "")) +end + + +local function prettytostring (x, indent, spacing) + indent = indent or "\t" + spacing = spacing or "" + return render (x, + function () + local s = spacing .. "{" + spacing = spacing .. indent + return s + end, + function () + spacing = string.gsub (spacing, indent .. "$", "") + return spacing .. "}" + end, + function (x) + if type (x) == "string" then + return format ("%q", x) + else + return tostring (x) + end + end, + function (x, k, v, ks, vs) + local s = spacing + if type (k) ~= "string" or k:match "[^%w_]" then + s = s .. "[" + if type (k) == "table" then + s = s .. "\n" + end + s = s .. ks + if type (k) == "table" then + s = s .. "\n" + end + s = s .. "]" + else + s = s .. k + end + s = s .. " =" + if type (v) == "table" then + s = s .. "\n" + else + s = s .. " " + end + s = s .. vs + return s + end, + function (_, k) + local s = "\n" + if k then + s = "," .. s + end + return s + end) +end + + +local function pickle (x) + if type (x) == "string" then + return format ("%q", x) + elseif type (x) == "number" or type (x) == "boolean" or + type (x) == "nil" then + return tostring (x) + else + x = copy (x) or x + if type (x) == "table" then + local s, sep = "{", "" + for i, v in pairs (x) do + s = s .. sep .. "[" .. M.pickle (i) .. "]=" .. M.pickle (v) + sep = "," + end + s = s .. "}" + return s + else + die ("cannot pickle " .. tostring (x)) + end + end +end + + + +--[[ ================= ]]-- +--[[ Public Interface. ]]-- +--[[ ================= ]]-- + + +local function X (decl, fn) + return debug.argscheck ("std.string." .. decl, fn) +end + +M = { + --- String concatenation operation. + -- @string s initial string + -- @param o object to stringify and concatenate + -- @return s .. tostring (o) + -- @usage + -- local string = require "std.string".monkey_patch () + -- concatenated = "foo" .. {"bar"} + __concat = __concat, + + --- String subscript operation. + -- @string s string + -- @tparam int|string i index or method name + -- @return `s:sub (i, i)` if i is a number, otherwise + -- fall back to a `std.string` metamethod (if any). + -- @usage + -- getmetatable ("").__index = require "std.string".__index + -- third = ("12345")[3] + __index = __index, + + --- Capitalise each word in a string. + -- @function caps + -- @string s any string + -- @treturn string *s* with each word capitalized + -- @usage userfullname = caps (input_string) + caps = X ("caps (string)", caps), + + --- Remove any final newline from a string. + -- @function chomp + -- @string s any string + -- @treturn string *s* with any single trailing newline removed + -- @usage line = chomp (line) + chomp = X ("chomp (string)", function (s) return (s:gsub ("\n$", "")) end), + + --- Escape a string to be used as a pattern. + -- @function escape_pattern + -- @string s any string + -- @treturn string *s* with active pattern characters escaped + -- @usage substr = inputstr:match (escape_pattern (literal)) + escape_pattern = X ("escape_pattern (string)", base.escape_pattern), + + --- Escape a string to be used as a shell token. + -- Quotes spaces, parentheses, brackets, quotes, apostrophes and + -- whitespace. + -- @function escape_shell + -- @string s any string + -- @treturn string *s* with active shell characters escaped + -- @usage os.execute ("echo " .. escape_shell (outputstr)) + escape_shell = X ("escape_shell (string)", escape_shell), + + --- Repeatedly `string.find` until target string is exhausted. + -- @function finds + -- @string s target string + -- @string pattern pattern to match in *s* + -- @int[opt=1] init start position + -- @bool[opt] plain inhibit magic characters + -- @return list of `{from, to; capt = {captures}}` + -- @see std.string.tfind + -- @usage + -- for t in std.elems (finds ("the target string", "%S+")) do + -- print (tostring (t.capt)) + -- end + finds = X ("finds (string, string, ?int, ?boolean|:plain)", finds), + + --- Extend to work better with one argument. + -- If only one argument is passed, no formatting is attempted. + -- @function format + -- @string f format string + -- @param[opt] ... arguments to format + -- @return formatted string + -- @usage print (format "100% stdlib!") + format = X ("format (string, [any...])", format), + + --- Remove leading matter from a string. + -- @function ltrim + -- @string s any string + -- @string[opt="%s+"] r leading pattern + -- @treturn string *s* with leading *r* stripped + -- @usage print ("got: " .. ltrim (userinput)) + ltrim = X ("ltrim (string, ?string)", + function (s, r) return (s:gsub ("^" .. (r or "%s+"), "")) end), + + --- Overwrite core `string` methods with `std` enhanced versions. + -- + -- Also adds auto-stringification to `..` operator on core strings, and + -- integer indexing of strings with `[]` dereferencing. + -- @function monkey_patch + -- @tparam[opt=_G] table namespace where to install global functions + -- @treturn table the module table + -- @usage local string = require "std.string".monkey_patch () + monkey_patch = X ("monkey_patch (?table)", monkey_patch), + + --- Write a number using SI suffixes. + -- The number is always written to 3 s.f. + -- @function numbertosi + -- @tparam number|string n any numeric value + -- @treturn string *n* simplifed using largest available SI suffix. + -- @usage print (numbertosi (bitspersecond) .. "bps") + numbertosi = X ("numbertosi (number|string)", numbertosi), + + --- Return the English suffix for an ordinal. + -- @function ordinal_suffix + -- @tparam int|string n any integer value + -- @treturn string English suffix for *n* + -- @usage + -- local now = os.date "*t" + -- print ("%d%s day of the week", now.day, ordinal_suffix (now.day)) + ordinal_suffix = X ("ordinal_suffix (int|string)", ordinal_suffix), + + --- Justify a string. + -- When the string is longer than w, it is truncated (left or right + -- according to the sign of w). + -- @function pad + -- @string s a string to justify + -- @int w width to justify to (-ve means right-justify; +ve means + -- left-justify) + -- @string[opt=" "] p string to pad with + -- @treturn string *s* justified to *w* characters wide + -- @usage print (pad (trim (outputstr, 78)) .. "\n") + pad = X ("pad (string, int, ?string)", pad), + + --- Convert a value to a string. + -- The string can be passed to `functional.eval` to retrieve the value. + -- @todo Make it work for recursive tables. + -- @param x object to pickle + -- @treturn string reversible string rendering of *x* + -- @see std.eval + -- @usage + -- function slow_identity (x) return functional.eval (pickle (x)) end + pickle = pickle, + + --- Pretty-print a table, or other object. + -- @function prettytostring + -- @param x object to convert to string + -- @string[opt="\t"] indent indent between levels + -- @string[opt=""] spacing space before every line + -- @treturn string pretty string rendering of *x* + -- @usage print (prettytostring (std, " ")) + prettytostring = X ("prettytostring (?any, ?string, ?string)", prettytostring), + + --- Turn tables into strings with recursion detection. + -- N.B. Functions calling render should not recurse, or recursion + -- detection will not work. + -- @function render + -- @param x object to convert to string + -- @tparam opentablecb open open table rendering function + -- @tparam closetablecb close close table rendering function + -- @tparam elementcb elem element rendering function + -- @tparam paircb pair pair rendering function + -- @tparam separatorcb sep separator rendering function + -- @tparam[opt] table roots accumulates table references to detect recursion + -- @return string representation of *x* + -- @usage + -- function tostring (x) + -- return render (x, lambda '="{"', lambda '="}"', tostring, + -- lambda '=_4.."=".._5', lambda '= _4 and "," or ""', + -- lambda '=","') + -- end + render = X ("render (?any, func, func, func, func, func, ?table)", render), + + --- Remove trailing matter from a string. + -- @function rtrim + -- @string s any string + -- @string[opt="%s+"] r trailing pattern + -- @treturn string *s* with trailing *r* stripped + -- @usage print ("got: " .. rtrim (userinput)) + rtrim = X ("rtrim (string, ?string)", + function (s, r) return (s:gsub ((r or "%s+") .. "$", "")) end), + + --- Split a string at a given separator. + -- Separator is a Lua pattern, so you have to escape active characters, + -- `^$()%.[]*+-?` with a `%` prefix to match a literal character in *s*. + -- @function split + -- @string s to split + -- @string[opt="%s+"] sep separator pattern + -- @return list of strings + -- @usage words = split "a very short sentence" + split = X ("split (string, ?string)", base.split), + + --- Do `string.find`, returning a table of captures. + -- @function tfind + -- @string s target string + -- @string pattern pattern to match in *s* + -- @int[opt=1] init start position + -- @bool[opt] plain inhibit magic characters + -- @treturn int start of match + -- @treturn int end of match + -- @treturn table list of captured strings + -- @see std.string.finds + -- @usage b, e, captures = tfind ("the target string", "%s", 10) + tfind = X ("tfind (string, string, ?int, ?boolean|:plain)", tfind), + + --- Remove leading and trailing matter from a string. + -- @function trim + -- @string s any string + -- @string[opt="%s+"] r trailing pattern + -- @treturn string *s* with leading and trailing *r* stripped + -- @usage print ("got: " .. trim (userinput)) + trim = X ("trim (string, ?string)", trim), + + --- Wrap a string into a paragraph. + -- @function wrap + -- @string s a paragraph of text + -- @int[opt=78] w width to wrap to + -- @int[opt=0] ind indent + -- @int[opt=ind] ind1 indent of first line + -- @treturn string *s* wrapped to *w* columns + -- @usage + -- print (wrap (copyright, 72, 4)) + wrap = X ("wrap (string, ?int, ?int, ?int)", wrap), +} + + + +--[[ ============= ]]-- +--[[ Deprecations. ]]-- +--[[ ============= ]]-- + + +local DEPRECATED = debug.DEPRECATED + + +M.assert = DEPRECATED ("41", "'std.string.assert'", + "use 'std.assert' instead", base.assert) + + +M.require_version = DEPRECATED ("41", "'std.string.require_version'", + "use 'std.require' instead", base.require) + + +M.tostring = DEPRECATED ("41", "'std.string.tostring'", + "use 'std.tostring' instead", base.tostring) + + + +return base.merge (M, string) + + + +--- Types +-- @section Types + +--- Signature of @{render} open table callback. +-- @function opentablecb +-- @tparam table t table about to be rendered +-- @treturn string open table rendering +-- @see render +-- @usage function open (t) return "{" end + + +--- Signature of @{render} close table callback. +-- @function closetablecb +-- @tparam table t table just rendered +-- @treturn string close table rendering +-- @see render +-- @usage function close (t) return "}" end + + +--- Signature of @{render} element callback. +-- @function elementcb +-- @param x element to render +-- @treturn string element rendering +-- @see render +-- @usage function element (e) return require "std".tostring (e) end + + +--- Signature of @{render} pair callback. +-- Trying to re-render *key* or *value* here will break recursion +-- detection, use *strkey* and *strvalue* pre-rendered values instead. +-- @function paircb +-- @tparam table t table containing pair being rendered +-- @param key key part of key being rendered +-- @param value value part of key being rendered +-- @string keystr prerendered *key* +-- @string valuestr prerendered *value* +-- @treturn string pair rendering +-- @see render +-- @usage +-- function pair (_, _, _, key, value) return key .. "=" .. value end + + +--- Signature of @{render} separator callback. +-- @function separatorcb +-- @tparam table t table currently being rendered +-- @param pk *t* key preceding separator, or `nil` for first key +-- @param pv *t* value preceding separator, or `nil` for first value +-- @param fk *t* key following separator, or `nil` for last key +-- @param fv *t* value following separator, or `nil` for last value +-- @treturn string separator rendering +-- @usage +-- function separator (_, _, _, fk) return fk and "," or "" end diff --git a/lib/std/table.lua b/lib/std/table.lua new file mode 100644 index 0000000..e12d02e --- /dev/null +++ b/lib/std/table.lua @@ -0,0 +1,529 @@ +--[[-- + Extensions to the core table module. + + The module table returned by `std.table` also contains all of the entries from + the core table module. An hygienic way to import this module, then, is simply + to override the core `table` locally: + + local table = require "std.table" + + @module std.table +]] + + +local core = _G.table + +local base = require "std.base" +local debug = require "std.debug" + +local argerror = debug.argerror +local collect = base.collect +local leaves = base.leaves +local ipairs, pairs = base.ipairs, base.pairs +local len = base.len + + +local M, monkeys + + +local function merge_allfields (t, u, map, nometa) + if type (map) ~= "table" then + map, nometa = nil, map + end + + if not nometa then + setmetatable (t, getmetatable (u)) + end + if map then + for k, v in pairs (u) do t[map[k] or k] = v end + else + for k, v in pairs (u) do t[k] = v end + end + return t +end + + +local function merge_namedfields (t, u, keys, nometa) + if type (keys) ~= "table" then + keys, nometa = nil, keys + end + + if not nometa then + setmetatable (t, getmetatable (u)) + end + for _, k in pairs (keys or {}) do t[k] = u[k] end + return t +end + + +local function depair (ls) + local t = {} + for _, v in ipairs (ls) do + t[v[1]] = v[2] + end + return t +end + + +local function enpair (t) + local tt = {} + for i, v in pairs (t) do + tt[#tt + 1] = {i, v} + end + return tt +end + + +local function flatten (t) + return collect (leaves, ipairs, t) +end + + +local function keys (t) + local l = {} + for k in pairs (t) do + l[#l + 1] = k + end + return l +end + + +local function new (x, t) + return setmetatable (t or {}, + {__index = function (t, i) + return x + end}) +end + + +local function project (fkey, tt) + local r = {} + for _, t in ipairs (tt) do + r[#r + 1] = t[fkey] + end + return r +end + + +local function shape (dims, t) + t = flatten (t) + -- Check the shape and calculate the size of the zero, if any + local size = 1 + local zero + for i, v in ipairs (dims) do + if v == 0 then + if zero then -- bad shape: two zeros + return nil + else + zero = i + end + else + size = size * v + end + end + if zero then + dims[zero] = math.ceil (len (t) / size) + end + local function fill (i, d) + if d > len (dims) then + return t[i], i + 1 + else + local r = {} + for j = 1, dims[d] do + local e + e, i = fill (i, d + 1) + r[#r + 1] = e + end + return r, i + end + end + return (fill (1, 1)) +end + + +local function size (t) + local n = 0 + for _ in pairs (t) do + n = n + 1 + end + return n +end + + +-- Preserve core table sort function. +local _sort = table.sort + +local function sort (t, c) + _sort (t, c) + return t +end + + +local function monkey_patch (namespace) + namespace = namespace or _G + namespace.table = base.copy (namespace.table or {}, monkeys) + return M +end + + +local _remove = table.remove + +local function remove (t, pos) + local lent = len (t) + pos = pos or lent + if pos < math.min (1, lent) or pos > lent + 1 then -- +1? whu? that's what 5.2.3 does!?! + argerror ("std.table.remove", 2, "position " .. pos .. " out of bounds", 2) + end + return _remove (t, pos) +end + + +local function values (t) + local l = {} + for _, v in pairs (t) do + l[#l + 1] = v + end + return l +end + + + +--[[ ================= ]]-- +--[[ Public Interface. ]]-- +--[[ ================= ]]-- + + +local function X (decl, fn) + return debug.argscheck ("std.table." .. decl, fn) +end + +M = { + --- Make a shallow copy of a table, including any metatable. + -- + -- To make deep copies, use @{tree.clone}. + -- @function clone + -- @tparam table t source table + -- @tparam[opt={}] table map table of `{old_key=new_key, ...}` + -- @bool[opt] nometa if non-nil don't copy metatable + -- @return copy of *t*, also sharing *t*'s metatable unless *nometa* + -- is true, and with keys renamed according to *map* + -- @see merge + -- @see clone_select + -- @usage + -- shallowcopy = clone (original, {rename_this = "to_this"}, ":nometa") + clone = X ("clone (table, [table], ?boolean|:nometa)", + function (...) return merge_allfields ({}, ...) end), + + --- Make a partial clone of a table. + -- + -- Like `clone`, but does not copy any fields by default. + -- @function clone_select + -- @tparam table t source table + -- @tparam[opt={}] table keys list of keys to copy + -- @bool[opt] nometa if non-nil don't copy metatable + -- @treturn table copy of fields in *selection* from *t*, also sharing *t*'s + -- metatable unless *nometa* + -- @see clone + -- @see merge_select + -- @usage + -- partialcopy = clone_select (original, {"this", "and_this"}, true) + clone_select = X ("clone_select (table, [table], ?boolean|:nometa)", + function (...) return merge_namedfields ({}, ...) end), + + --- Turn a list of pairs into a table. + -- @todo Find a better name. + -- @function depair + -- @tparam table ls list of lists + -- @treturn table a flat table with keys and values from *ls* + -- @see enpair + -- @usage + -- --> {a=1, b=2, c=3} + -- depair {{"a", 1}, {"b", 2}, {"c", 3}} + depair = X ("depair (list of lists)", depair), + + --- Turn a table into a list of pairs. + -- @todo Find a better name. + -- @function enpair + -- @tparam table t a table `{i1=v1, ..., in=vn}` + -- @treturn table a new list of pairs containing `{{i1, v1}, ..., {in, vn}}` + -- @see depair + -- @usage + -- --> {{1, "a"}, {2, "b"}, {3, "c"}} + -- enpair {"a", "b", "c"} + enpair = X ("enpair (table)", enpair), + + --- Return whether table is empty. + -- @function empty + -- @tparam table t any table + -- @treturn boolean `true` if *t* is empty, otherwise `false` + -- @usage if empty (t) then error "ohnoes" end + empty = X ("empty (table)", function (t) return not next (t) end), + + --- Flatten a nested table into a list. + -- @function flatten + -- @tparam table t a table + -- @treturn table a list of all non-table elements of *t* + -- @usage + -- --> {1, 2, 3, 4, 5} + -- flatten {{1, {{2}, 3}, 4}, 5} + flatten = X ("flatten (table)", flatten), + + --- Enhance core *table.insert* to return its result. + -- If *pos* is not given, respect `__len` metamethod when calculating + -- default append. Also, diagnose out of bounds *pos* arguments + -- consistently on any supported version of Lua. + -- @function insert + -- @tparam table t a table + -- @int[opt=len (t)] pos index at which to insert new element + -- @param v value to insert into *t* + -- @treturn table *t* + -- @usage + -- --> {1, "x", 2, 3, "y"} + -- insert (insert ({1, 2, 3}, 2, "x"), "y") + insert = X ("insert (table, [int], any)", base.insert), + + --- Invert a table. + -- @function invert + -- @tparam table t a table with `{k=v, ...}` + -- @treturn table inverted table `{v=k, ...}` + -- @usage + -- --> {a=1, b=2, c=3} + -- invert {"a", "b", "c"} + invert = X ("invert (table)", base.invert), + + --- Make the list of keys in table. + -- @function keys + -- @tparam table t a table + -- @treturn table list of keys from *t* + -- @see okeys + -- @see values + -- @usage globals = keys (_G) + keys = X ("keys (table)", keys), + + --- Equivalent to `#` operation, but respecting `__len` even on Lua 5.1. + -- @function len + -- @tparam table t a table + -- @treturn int length of list part of *t* + -- @usage for i = 1, len (t) do process (t[i]) end + len = X ("len (table)", base.len), + + --- Largest integer key in a table. + -- @function maxn + -- @tparam table t a table + -- @treturn int largest integer key in *t* + -- @usage + -- --> 42 + -- maxn {"a", b="c", 99, [42]="x", "x", [5]=67} + maxn = X ("maxn (table)", base.maxn), + + --- Destructively merge another table's fields into another. + -- @function merge + -- @tparam table t destination table + -- @tparam table u table with fields to merge + -- @tparam[opt={}] table map table of `{old_key=new_key, ...}` + -- @bool[opt] nometa if `true` or ":nometa" don't copy metatable + -- @treturn table *t* with fields from *u* merged in + -- @see clone + -- @see merge_select + -- @usage merge (_G, require "std.debug", {say = "log"}, ":nometa") + merge = X ("merge (table, table, [table], ?boolean|:nometa)", merge_allfields), + + --- Destructively merge another table's named fields into *table*. + -- + -- Like `merge`, but does not merge any fields by default. + -- @function merge_select + -- @tparam table t destination table + -- @tparam table u table with fields to merge + -- @tparam[opt={}] table keys list of keys to copy + -- @bool[opt] nometa if `true` or ":nometa" don't copy metatable + -- @treturn table copy of fields in *selection* from *t*, also sharing *t*'s + -- metatable unless *nometa* + -- @see merge + -- @see clone_select + -- @usage merge_select (_G, require "std.debug", {"say"}, false) + merge_select = X ("merge_select (table, table, [table], ?boolean|:nometa)", + merge_namedfields), + + --- Overwrite core `table` methods with `std` enhanced versions. + -- @function monkey_patch + -- @tparam[opt=_G] table namespace where to install global functions + -- @treturn table the module table + -- @usage local table = require "std.table".monkey_patch () + monkey_patch = X ("monkey_patch (?table)", monkey_patch), + + --- Make a table with a default value for unset keys. + -- @function new + -- @param[opt=nil] x default entry value + -- @tparam[opt={}] table t initial table + -- @treturn table table whose unset elements are *x* + -- @usage t = new (0) + new = X ("new (?any, ?table)", new), + + --- Make an ordered list of keys in table. + -- @function okeys + -- @tparam table t a table + -- @treturn table ordered list of keys from *t* + -- @see keys + -- @usage globals = keys (_G) + okeys = X ("okeys (table)", base.okeys), + + --- Turn a tuple into a list. + -- @function pack + -- @param ... tuple + -- @return list + -- @usage + -- --> {1, 2, "ax"} + -- pack (("ax1"):find "(%D+)") + pack = function (...) return {...} end, + + --- Project a list of fields from a list of tables. + -- @function project + -- @param fkey field to project + -- @tparam table tt a list of tables + -- @treturn table list of *fkey* fields from *tt* + -- @usage + -- --> {1, 3, "yy"} + -- project ("xx", {{"a", xx=1, yy="z"}, {"b", yy=2}, {"c", xx=3}, {xx="yy"}) + project = X ("project (any, list of tables)", project), + + --- Enhance core *table.remove* to respect `__len` when *pos* is omitted. + -- Also, diagnose out of bounds *pos* arguments consistently on any supported + -- version of Lua. + -- @function remove + -- @tparam table t a table + -- @int[opt=len (t)] pos index from which to remove an element + -- @return removed value, or else `nil` + -- @usage + -- --> {1, 2, 5} + -- t = {1, 2, "x", 5} + -- remove (t, 3) == "x" and t + remove = X ("remove (table, ?int)", remove), + + --- Shape a table according to a list of dimensions. + -- + -- Dimensions are given outermost first and items from the original + -- list are distributed breadth first; there may be one 0 indicating + -- an indefinite number. Hence, `{0}` is a flat list, + -- `{1}` is a singleton, `{2, 0}` is a list of + -- two lists, and `{0, 2}` is a list of pairs. + -- + -- Algorithm: turn shape into all positive numbers, calculating + -- the zero if necessary and making sure there is at most one; + -- recursively walk the shape, adding empty tables until the bottom + -- level is reached at which point add table items instead, using a + -- counter to walk the flattened original list. + -- + -- @todo Use ileaves instead of flatten (needs a while instead of a + -- for in fill function) + -- @function shape + -- @tparam table dims table of dimensions `{d1, ..., dn}` + -- @tparam table t a table of elements + -- @return reshaped list + -- @usage + -- --> {{"a", "b"}, {"c", "d"}, {"e", "f"}} + -- shape ({3, 2}, {"a", "b", "c", "d", "e", "f"}) + shape = X ("shape (table, table)", shape), + + --- Find the number of elements in a table. + -- @function size + -- @tparam table t any table + -- @treturn int number of non-nil values in *t* + -- @usage + -- --> 3 + -- size {foo = true, bar = true, baz = false} + size = X ("size (table)", size), + + --- Enhance core *table.sort* to return its result. + -- @function sort + -- @tparam table t unsorted table + -- @tparam[opt=std.operator.lt] comparator c ordering function callback + -- @return *t* with keys sorted accordind to *c* + -- @usage table.concat (sort (object)) + sort = X ("sort (table, ?function)", sort), + + --- Enhance core *table.unpack* to always unpack up to `maxn (t)`. + -- @function unpack + -- @tparam table t table to act on + -- @int[opt=1] i first index to unpack + -- @int[opt=table.maxn(t)] j last index to unpack + -- @return ... values of numeric indices of *t* + -- @usage return unpack (results_table) + unpack = X ("unpack (table, ?int, ?int)", base.unpack), + + --- Make the list of values of a table. + -- @function values + -- @tparam table t any table + -- @treturn table list of values in *t* + -- @see keys + -- @usage + -- --> {"a", "c", 42} + -- values {"a", b="c", [-1]=42} + values = X ("values (table)", values), +} + + +monkeys = base.copy ({}, M) -- before deprecations and core merge + + +--[[ ============= ]]-- +--[[ Deprecations. ]]-- +--[[ ============= ]]-- + + +local DEPRECATED = debug.DEPRECATED + +M.clone_rename = DEPRECATED ("39", "'std.table.clone_rename'", + "use the new `map` argument to 'std.table.clone' instead", + function (map, t) + local r = merge_allfields ({}, t) + for i, v in pairs (map) do + r[v] = t[i] + r[i] = nil + end + return r + end) + + +M.metamethod = DEPRECATED ("41", "'std.table.metamethod'", + "use 'std.getmetamethod' instead", base.getmetamethod) + + +M.ripairs = DEPRECATED ("41", "'std.table.ripairs'", + "use 'std.ripairs' instead", base.ripairs) + + +M.totable = DEPRECATED ("41", "'std.table.totable'", + "use 'std.pairs' instead", + function (x) + local m = base.getmetamethod (x, "__totable") + if m then + return m (x) + elseif type (x) == "table" then + return x + elseif type (x) == "string" then + local t = {} + x:gsub (".", function (c) t[#t + 1] = c end) + return t + else + return nil + end + end) + + + +return base.merge (M, table) + + + +--- Types +-- @section Types + +--- Signature of a @{sort} comparator function. +-- @function comparator +-- @param a any object +-- @param b any object +-- @treturn boolean `true` if *a* sorts before *b*, otherwise `false` +-- @see sort +-- @usage +-- local reversor = function (a, b) return a > b end +-- sort (t, reversor) diff --git a/lib/std/tree.lua b/lib/std/tree.lua new file mode 100644 index 0000000..2feaedc --- /dev/null +++ b/lib/std/tree.lua @@ -0,0 +1,280 @@ +--[[-- + Tree container prototype. + + Note that Functions listed below are only available from the Tree + prototype returned by requiring this module, because Container objects + cannot have object methods. + + Prototype Chain + --------------- + + table + `-> Object + `-> Container + `-> Tree + + @classmod std.tree + @see std.container +]] + +local base = require "std.base" +local operator = require "std.operator" + +local Container = require "std.container" {} + +local ielems, ipairs, leaves, pairs, prototype = + base.ielems, base.ipairs, base.leaves, base.pairs, base.prototype +local last, len = base.last, base.len +local reduce = base.reduce + +local Tree -- forward declaration + + + +--- Tree iterator. +-- @tparam function it iterator function +-- @tparam tree|table tr tree or tree-like table +-- @treturn string type ("leaf", "branch" (pre-order) or "join" (post-order)) +-- @treturn table path to node (`{i1, ...in}`) +-- @treturn node node +local function _nodes (it, tr) + local p = {} + local function visit (n) + if type (n) == "table" then + coroutine.yield ("branch", p, n) + for i, v in it (n) do + p[#p + 1] = i + visit (v) + table.remove (p) + end + coroutine.yield ("join", p, n) + else + coroutine.yield ("leaf", p, n) + end + end + return coroutine.wrap (visit), tr +end + + +local function clone (t, nometa) + local r = {} + if not nometa then + setmetatable (r, getmetatable (t)) + end + local d = {[t] = r} + local function copy (o, x) + for i, v in pairs (x) do + if type (v) == "table" then + if not d[v] then + d[v] = {} + if not nometa then + setmetatable (d[v], getmetatable (v)) + end + o[i] = copy (d[v], v) + else + o[i] = d[v] + end + else + o[i] = v + end + end + return o + end + return copy (r, t) +end + + +local function merge (t, u) + for ty, p, n in _nodes (pairs, u) do + if ty == "leaf" then + t[p] = n + end + end + return t +end + + + +--[[ ============ ]]-- +--[[ Tree Object. ]]-- +--[[ ============ ]]-- + + +local function X (decl, fn) + return require "std.debug".argscheck ("std.tree." .. decl, fn) +end + + +--- Tree prototype object. +-- @object Tree +-- @string[opt="Tree"] _type object name +-- @see std.container +-- @see std.object.__call +-- @usage +-- local std = require "std" +-- local Tree = std.tree {} +-- local tr = Tree {} +-- tr[{"branch1", 1}] = "leaf1" +-- tr[{"branch1", 2}] = "leaf2" +-- tr[{"branch2", 1}] = "leaf3" +-- print (tr[{"branch1"}]) --> Tree {leaf1, leaf2} +-- print (tr[{"branch1", 2}]) --> leaf2 +-- print (tr[{"branch1", 3}]) --> nil +-- --> leaf1 leaf2 leaf3 +-- for leaf in std.tree.leaves (tr) do +-- io.write (leaf .. "\t") +-- end +Tree = Container { + _type = "Tree", + + --- Deep retrieval. + -- @static + -- @function __index + -- @tparam Tree tr a tree + -- @param i non-table, or list of keys `{i1, ...i_n}` + -- @return `tr[i1]...[i_n]` if *i* is a key list, `tr[i]` otherwise + -- @todo the following doesn't treat list keys correctly + -- e.g. tr[{{1, 2}, {3, 4}}], maybe flatten first? + -- @usage + -- del_other_window = keymap[{"C-x", "4", KEY_DELETE}] + __index = function (tr, i) + if prototype (i) == "table" then + return reduce (operator.get, tr, ielems, i) + else + return rawget (tr, i) + end + end, + + --- Deep insertion. + -- @static + -- @function __newindex + -- @tparam Tree tr a tree + -- @param i non-table, or list of keys `{i1, ...i_n}` + -- @param[opt] v value + -- @usage + -- function bindkey (keylist, fn) keymap[keylist] = fn end + __newindex = function (tr, i, v) + if prototype (i) == "table" then + for n = 1, len (i) - 1 do + if prototype (tr[i[n]]) ~= "Tree" then + rawset (tr, i[n], Tree {}) + end + tr = tr[i[n]] + end + rawset (tr, last (i), v) + else + rawset (tr, i, v) + end + end, + + _functions = { + --- Make a deep copy of a tree, including any metatables. + -- @static + -- @function clone + -- @tparam table t tree or tree-like table + -- @tparam boolean nometa if non-`nil` don't copy metatables + -- @treturn Tree|table a deep copy of *tr* + -- @see std.table.clone + -- @usage + -- tr = {"one", {two=2}, {{"three"}, four=4}} + -- copy = clone (tr) + -- copy[2].two=5 + -- assert (tr[2].two == 2) + clone = X ("clone (table, ?boolean|:nometa)", clone), + + --- Tree iterator which returns just numbered leaves, in order. + -- @static + -- @function ileaves + -- @tparam Tree|table tr tree or tree-like table + -- @treturn function iterator function + -- @treturn Tree|table the tree *tr* + -- @see inodes + -- @see leaves + -- @usage + -- --> t = {"one", "three", "five"} + -- for leaf in ileaves {"one", {two=2}, {{"three"}, four=4}}, foo="bar", "five"} + -- do + -- t[#t + 1] = leaf + -- end + ileaves = X ("ileaves (table)", function (t) return leaves (ipairs, t) end), + + --- Tree iterator over numbered nodes, in order. + -- + -- The iterator function behaves like @{nodes}, but only traverses the + -- array part of the nodes of *tr*, ignoring any others. + -- @static + -- @function inodes + -- @tparam Tree|table tr tree or tree-like table to iterate over + -- @treturn function iterator function + -- @treturn tree|table the tree, *tr* + -- @see nodes + inodes = X ("inodes (table)", function (t) return _nodes (ipairs, t) end), + + --- Tree iterator which returns just leaves. + -- @static + -- @function leaves + -- @tparam table t tree or tree-like table + -- @treturn function iterator function + -- @treturn table *t* + -- @see ileaves + -- @see nodes + -- @usage + -- for leaf in leaves {"one", {two=2}, {{"three"}, four=4}}, foo="bar", "five"} + -- do + -- t[#t + 1] = leaf + -- end + -- --> t = {2, 4, "five", "foo", "one", "three"} + -- table.sort (t, lambda "=tostring(_1) < tostring(_2)") + leaves = X ("leaves (table)", function (t) return leaves (pairs, t) end), + + --- Destructively deep-merge one tree into another. + -- @static + -- @function merge + -- @tparam table t destination tree + -- @tparam table u table with nodes to merge + -- @treturn table *t* with nodes from *u* merged in + -- @see std.table.merge + -- @usage + -- merge (dest, {{exists=1}, {{not = {present = { inside = "dest" }}}}}) + merge = X ("merge (table, table)", merge), + + --- Tree iterator over all nodes. + -- + -- The returned iterator function performs a depth-first traversal of + -- `tr`, and at each node it returns `{node-type, tree-path, tree-node}` + -- where `node-type` is `branch`, `join` or `leaf`; `tree-path` is a + -- list of keys used to reach this node, and `tree-node` is the current + -- node. + -- + -- Note that the `tree-path` reuses the same table on each iteration, so + -- you must `table.clone` a copy if you want to take a snap-shot of the + -- current state of the `tree-path` list before the next iteration + -- changes it. + -- @static + -- @function nodes + -- @tparam Tree|table tr tree or tree-like table to iterate over + -- @treturn function iterator function + -- @treturn Tree|table the tree, *tr* + -- @see inodes + -- @usage + -- -- tree = +-- node1 + -- -- | +-- leaf1 + -- -- | '-- leaf2 + -- -- '-- leaf 3 + -- tree = Tree { Tree { "leaf1", "leaf2"}, "leaf3" } + -- for node_type, path, node in nodes (tree) do + -- print (node_type, path, node) + -- end + -- --> "branch" {} {{"leaf1", "leaf2"}, "leaf3"} + -- --> "branch" {1} {"leaf1", "leaf"2") + -- --> "leaf" {1,1} "leaf1" + -- --> "leaf" {1,2} "leaf2" + -- --> "join" {1} {"leaf1", "leaf2"} + -- --> "leaf" {2} "leaf3" + -- --> "join" {} {{"leaf1", "leaf2"}, "leaf3"} + -- os.exit (0) + nodes = X ("nodes (table)", function (t) return _nodes (pairs, t) end), + }, +} + +return Tree diff --git a/lib/std/version.lua b/lib/std/version.lua new file mode 100644 index 0000000..0dc347a --- /dev/null +++ b/lib/std/version.lua @@ -0,0 +1 @@ +return "General Lua libraries / 41.2.2" diff --git a/luarocks-config.lua.in b/luarocks-config.lua.in deleted file mode 100644 index 412b3ed..0000000 --- a/luarocks-config.lua.in +++ /dev/null @@ -1,3 +0,0 @@ -rocks_trees = { - "@abs_srcdir@/luarocks" -} diff --git a/m4/ax_compare_version.m4 b/m4/ax_compare_version.m4 deleted file mode 100644 index 74dc0fd..0000000 --- a/m4/ax_compare_version.m4 +++ /dev/null @@ -1,177 +0,0 @@ -# =========================================================================== -# http://www.gnu.org/software/autoconf-archive/ax_compare_version.html -# =========================================================================== -# -# SYNOPSIS -# -# AX_COMPARE_VERSION(VERSION_A, OP, VERSION_B, [ACTION-IF-TRUE], [ACTION-IF-FALSE]) -# -# DESCRIPTION -# -# This macro compares two version strings. Due to the various number of -# minor-version numbers that can exist, and the fact that string -# comparisons are not compatible with numeric comparisons, this is not -# necessarily trivial to do in a autoconf script. This macro makes doing -# these comparisons easy. -# -# The six basic comparisons are available, as well as checking equality -# limited to a certain number of minor-version levels. -# -# The operator OP determines what type of comparison to do, and can be one -# of: -# -# eq - equal (test A == B) -# ne - not equal (test A != B) -# le - less than or equal (test A <= B) -# ge - greater than or equal (test A >= B) -# lt - less than (test A < B) -# gt - greater than (test A > B) -# -# Additionally, the eq and ne operator can have a number after it to limit -# the test to that number of minor versions. -# -# eq0 - equal up to the length of the shorter version -# ne0 - not equal up to the length of the shorter version -# eqN - equal up to N sub-version levels -# neN - not equal up to N sub-version levels -# -# When the condition is true, shell commands ACTION-IF-TRUE are run, -# otherwise shell commands ACTION-IF-FALSE are run. The environment -# variable 'ax_compare_version' is always set to either 'true' or 'false' -# as well. -# -# Examples: -# -# AX_COMPARE_VERSION([3.15.7],[lt],[3.15.8]) -# AX_COMPARE_VERSION([3.15],[lt],[3.15.8]) -# -# would both be true. -# -# AX_COMPARE_VERSION([3.15.7],[eq],[3.15.8]) -# AX_COMPARE_VERSION([3.15],[gt],[3.15.8]) -# -# would both be false. -# -# AX_COMPARE_VERSION([3.15.7],[eq2],[3.15.8]) -# -# would be true because it is only comparing two minor versions. -# -# AX_COMPARE_VERSION([3.15.7],[eq0],[3.15]) -# -# would be true because it is only comparing the lesser number of minor -# versions of the two values. -# -# Note: The characters that separate the version numbers do not matter. An -# empty string is the same as version 0. OP is evaluated by autoconf, not -# configure, so must be a string, not a variable. -# -# The author would like to acknowledge Guido Draheim whose advice about -# the m4_case and m4_ifvaln functions make this macro only include the -# portions necessary to perform the specific comparison specified by the -# OP argument in the final configure script. -# -# LICENSE -# -# Copyright (c) 2008 Tim Toolan -# -# Copying and distribution of this file, with or without modification, are -# permitted in any medium without royalty provided the copyright notice -# and this notice are preserved. This file is offered as-is, without any -# warranty. - -#serial 11 - -dnl ######################################################################### -AC_DEFUN([AX_COMPARE_VERSION], [ - AC_REQUIRE([AC_PROG_AWK]) - - # Used to indicate true or false condition - ax_compare_version=false - - # Convert the two version strings to be compared into a format that - # allows a simple string comparison. The end result is that a version - # string of the form 1.12.5-r617 will be converted to the form - # 0001001200050617. In other words, each number is zero padded to four - # digits, and non digits are removed. - AS_VAR_PUSHDEF([A],[ax_compare_version_A]) - A=`echo "$1" | sed -e 's/\([[0-9]]*\)/Z\1Z/g' \ - -e 's/Z\([[0-9]]\)Z/Z0\1Z/g' \ - -e 's/Z\([[0-9]][[0-9]]\)Z/Z0\1Z/g' \ - -e 's/Z\([[0-9]][[0-9]][[0-9]]\)Z/Z0\1Z/g' \ - -e 's/[[^0-9]]//g'` - - AS_VAR_PUSHDEF([B],[ax_compare_version_B]) - B=`echo "$3" | sed -e 's/\([[0-9]]*\)/Z\1Z/g' \ - -e 's/Z\([[0-9]]\)Z/Z0\1Z/g' \ - -e 's/Z\([[0-9]][[0-9]]\)Z/Z0\1Z/g' \ - -e 's/Z\([[0-9]][[0-9]][[0-9]]\)Z/Z0\1Z/g' \ - -e 's/[[^0-9]]//g'` - - dnl # In the case of le, ge, lt, and gt, the strings are sorted as necessary - dnl # then the first line is used to determine if the condition is true. - dnl # The sed right after the echo is to remove any indented white space. - m4_case(m4_tolower($2), - [lt],[ - ax_compare_version=`echo "x$A -x$B" | sed 's/^ *//' | sort -r | sed "s/x${A}/false/;s/x${B}/true/;1q"` - ], - [gt],[ - ax_compare_version=`echo "x$A -x$B" | sed 's/^ *//' | sort | sed "s/x${A}/false/;s/x${B}/true/;1q"` - ], - [le],[ - ax_compare_version=`echo "x$A -x$B" | sed 's/^ *//' | sort | sed "s/x${A}/true/;s/x${B}/false/;1q"` - ], - [ge],[ - ax_compare_version=`echo "x$A -x$B" | sed 's/^ *//' | sort -r | sed "s/x${A}/true/;s/x${B}/false/;1q"` - ],[ - dnl Split the operator from the subversion count if present. - m4_bmatch(m4_substr($2,2), - [0],[ - # A count of zero means use the length of the shorter version. - # Determine the number of characters in A and B. - ax_compare_version_len_A=`echo "$A" | $AWK '{print(length)}'` - ax_compare_version_len_B=`echo "$B" | $AWK '{print(length)}'` - - # Set A to no more than B's length and B to no more than A's length. - A=`echo "$A" | sed "s/\(.\{$ax_compare_version_len_B\}\).*/\1/"` - B=`echo "$B" | sed "s/\(.\{$ax_compare_version_len_A\}\).*/\1/"` - ], - [[0-9]+],[ - # A count greater than zero means use only that many subversions - A=`echo "$A" | sed "s/\(\([[0-9]]\{4\}\)\{m4_substr($2,2)\}\).*/\1/"` - B=`echo "$B" | sed "s/\(\([[0-9]]\{4\}\)\{m4_substr($2,2)\}\).*/\1/"` - ], - [.+],[ - AC_WARNING( - [illegal OP numeric parameter: $2]) - ],[]) - - # Pad zeros at end of numbers to make same length. - ax_compare_version_tmp_A="$A`echo $B | sed 's/./0/g'`" - B="$B`echo $A | sed 's/./0/g'`" - A="$ax_compare_version_tmp_A" - - # Check for equality or inequality as necessary. - m4_case(m4_tolower(m4_substr($2,0,2)), - [eq],[ - test "x$A" = "x$B" && ax_compare_version=true - ], - [ne],[ - test "x$A" != "x$B" && ax_compare_version=true - ],[ - AC_WARNING([illegal OP parameter: $2]) - ]) - ]) - - AS_VAR_POPDEF([A])dnl - AS_VAR_POPDEF([B])dnl - - dnl # Execute ACTION-IF-TRUE / ACTION-IF-FALSE. - if test "$ax_compare_version" = "true" ; then - m4_ifvaln([$4],[$4],[:])dnl - m4_ifvaln([$5],[else $5])dnl - fi -]) dnl AX_COMPARE_VERSION diff --git a/m4/ax_lua.m4 b/m4/ax_lua.m4 deleted file mode 100644 index c5b54f6..0000000 --- a/m4/ax_lua.m4 +++ /dev/null @@ -1,629 +0,0 @@ -# =========================================================================== -# ax_lua.m4 -# =========================================================================== -# -# SYNOPSIS -# -# AX_PROG_LUA[([MINIMUM-VERSION], [TOO-BIG-VERSION], -# [ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND])] -# AX_LUA_HEADERS[([ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND])] -# AX_LUA_LIBS[([ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND])] -# AX_LUA_READLINE[([ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND])] -# -# DESCRIPTION -# -# Detect a Lua interpreter, optionally specifying a minimum and maximum -# version number. Set up important Lua paths, such as the directories in -# which to install scripts and modules (shared libraries). -# -# Also detect Lua headers and libraries. The Lua version contained in the -# header is checked to match the Lua interpreter version exactly. When -# searching for Lua libraries, the version number is used as a suffix. This -# is done with the goal of supporting multiple Lua installs (5.1 and 5.2 -# side-by-side). -# -# -# *** A note on compatibility with previous versions: This file has been -# mostly rewritten for serial 18. Most developers should be able to use -# these macros without needing to modify configure.ac. Care has been taken -# to preserve each macro's behaviour, but there are some differences: -# -# 1) AX_WITH_LUA is deprecated; it now expands to the exact same thing as -# AX_PROG_LUA with no arguments. -# -# 2) AX_LUA_HEADERS now checks that the version number defined in lua.h -# matches the interpreter version. AX_LUA_HEADERS_VERSION is therefore -# unnecessary, so it is deprecated and does not expand to anything. -# -# 3) The configure flag --with-lua-suffix no longer exists; the user -# should instead specify the LUA precious variable on the command line. -# See the AX_PROG_LUA description for details. -# -# Please read the macro descriptions for more information. *** -# -# -# -# AX_PROG_LUA[([MINIMUM-VERSION], [TOO-BIG-VERSION], -# [ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND])] -# ------------------------------------------------------- -# -# Search for the Lua interpreter, and set up important Lua paths. -# Adds precious variable LUA, which may contain the path of the Lua -# interpreter. If LUA is blank, the user's path is searched for an -# suitable interpreter. -# -# If MINIMUM-VERSION is supplied, then only Lua interpreters with a version -# number greater or equal to MINIMUM-VERSION will be accepted. If TOO-BIG- -# VERSION is also supplied, then only Lua interpreters with a version -# number greater or equal to MINIMUM-VERSION and less than TOO-BIG-VERSION -# will be accepted. -# -# Version comparisions require the AX_COMPARE_VERSION macro, which is -# provided by ax_compare_version.m4 from the Autoconf Archive. -# -# The Lua version number, LUA_VERSION, is found from the interpreter, and -# substituted. LUA_PLATFORM is also found, but not currently supported (no -# standard representation). -# -# Finally, the macro finds four paths: -# -# luadir Directory to install Lua scripts. -# pkgluadir $luadir/$PACKAGE -# luaexecdir Directory to install Lua modules. -# pkgluaexecdir $luaexecdir/$PACKAGE -# -# These paths a found based on $prefix, $exec_prefix, Lua's package.path, -# and package.cpath. The first path of package.path beginning with $prefix -# is selected as luadir. The first path of package.cpath beginning with -# $exec_prefix is used as luaexecdir. This should work on all reasonable -# Lua installations. If a path cannot be determined, a default path is -# used. Of course, the user can override these later when invoking make. -# -# luadir Default: $prefix/share/lua/$LUA_VERSION -# luaexecdir Default: $exec_prefix/lib/lua/$LUA_VERSION -# -# These directories can be used by Automake as install destinations. -# The variable name minus 'dir' needs to be used as a prefix to the -# appropriate Automake primary, e.g. lua_SCRIPS or luaexec_LIBRARIES. -# -# If an acceptable Lua interpreter is found, then ACTION-IF-FOUND is -# performed, otherwise ACTION-IF-NOT-FOUND is preformed. If ACTION-IF-NOT- -# FOUND is blank, then it will default to printing an error. To prevent -# the default behavior, give ':' as an action. -# -# -# AX_LUA_HEADERS[([ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND])] -# ---------------------------------------------------------- -# -# Search for Lua headers. Requires that AX_PROG_LUA be expanded before this -# macro. Adds precious variable LUA_INCLUDE, which may contain Lua specific -# include flags, e.g. -I/usr/include/lua5.1. If LUA_INCLUDE is blank, then -# this macro will attempt to find suitable flags. -# -# LUA_INCLUDE can be used by Automake to compile Lua modules or executables -# with embedded interpreters. The *_CPPFLAGS variables should be used for -# this purpose, e.g. myprog_CPPFLAGS = $(LUA_INCLUDE). -# -# This macro searches for the header lua.h (and others). The search is -# performed with a combination of CPPFLAGS, CPATH, etc, and LUA_INCLUDE. -# If the search is unsuccessful, then some common directories are tried. If -# the headers are then found, then LUA_INCLUDE is set accordingly. -# -# The paths automatically searched are: -# -# * /usr/include/luaX.Y -# * /usr/include/lua/X.Y -# * /usr/include/luaXY -# * /usr/local/include/luaX.Y -# * /usr/local/include/lua/X.Y -# * /usr/local/include/luaXY -# -# (Where X.Y is the Lua version number, e.g. 5.1.) -# -# The Lua version number found in the headers is always checked to match -# the Lua interpreter's version number. Lua headers with mismatched -# version numbers are not accepted. -# -# If headers are found, then ACTION-IF-FOUND is performed, otherwise -# ACTION-IF-NOT-FOUND is performed. If ACTION-IF-NOT-FOUND is blank, -# then it will default to printing an error. To prevent the default -# behavior, set the action to ':'. -# -# -# AX_LUA_LIBS[([ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND])] -# ------------------------------------------------------- -# -# Search for Lua libraries. Requires that AX_PROG_LUA be expanded before -# this macro. Adds precious variable LUA_LIB, which may contain Lua specific -# linker flags, e.g. -llua5.1. If LUA_LIB is blank, then this macro will -# attempt to find suitable flags. -# -# LUA_LIB can be used by Automake to link Lua modules or executables with -# embedded interpreters. The *_LIBADD and *_LDADD variables should be used -# for this purpose, e.g. mymod_LIBADD = $(LUA_LIB). -# -# This macro searches for the Lua library. More technically, it searches -# for a library containing the function lua_load. The search is performed -# with a combination of LIBS, LIBRARY_PATH, and LUA_LIB. -# -# If the search determines that some linker flags are missing, then those -# flags will be added to LUA_LIB. -# -# If libraries are found, then ACTION-IF-FOUND is performed, otherwise -# ACTION-IF-NOT-FOUND is performed. If ACTION-IF-NOT-FOUND is blank, -# then it will default to printing an error. To prevent the default -# behavior, set the action to ':'. -# -# -# AX_LUA_READLINE[([ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND])] -# ----------------------------------------------------------- -# -# Search for readline headers and libraries. Requires the AX_LIB_READLINE -# macro, which is provided by ax_lib_readline.m4 from the Autoconf Archive. -# -# If a readline compatible library is found, then ACTION-IF-FOUND is -# performed, otherwise ACTION-IF-NOT-FOUND is performed. -# -# -# LICENSE -# -# Copyright (C) 2013 Tim Perkins -# Copyright (C) 2013 Reuben Thomas -# -# This program is free software: you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the -# Free Software Foundation, either version 3 of the License, or (at your -# option) any later version. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General -# Public License for more details. -# -# You should have received a copy of the GNU General Public License along -# with this program. If not, see . -# -# As a special exception, the respective Autoconf Macro's copyright owner -# gives unlimited permission to copy, distribute and modify the configure -# scripts that are the output of Autoconf when processing the Macro. You -# need not follow the terms of the GNU General Public License when using -# or distributing such scripts, even though portions of the text of the -# Macro appear in them. The GNU General Public License (GPL) does govern -# all other use of the material that constitutes the Autoconf Macro. -# -# This special exception to the GPL applies to versions of the Autoconf -# Macro released by the Autoconf Archive. When you make and distribute a -# modified version of the Autoconf Macro, you may extend this special -# exception to the GPL to apply to your modified version as well. -# -# THANKS -# -# This file was inspired by Andrew Dalke's and James Henstridge's python.m4 -# and Tom Payne's, Matthieu Moy's, and Reuben Thomas's ax_lua.m4 (serial -# 17). Basically, this file is a mashup of those two files. I like to think -# it combines the best of the two! - -#serial 18 - - -dnl ========================================================================= -dnl AX_PROG_LUA([MINIMUM-VERSION], [TOO-BIG-VERSION], -dnl [ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND]) -dnl ========================================================================= -AC_DEFUN([AX_PROG_LUA], -[ - dnl Make LUA a precious variable. - AC_ARG_VAR([LUA], [The Lua interpreter, e.g. /usr/bin/lua5.1]) - - dnl Find a Lua interpreter. - m4_define_default([_AX_LUA_INTERPRETER_LIST], - [lua lua5.2 lua5.1 lua50]) - - m4_if([$1], [], - [ dnl No version check is needed. Find any Lua interpreter. - AS_IF([test "x$LUA" = 'x'], - [AC_PATH_PROGS([LUA], [_AX_LUA_INTERPRETER_LIST], [:])]) - ax_display_LUA='lua' - - dnl At least check if this is a Lua interpreter. - AC_MSG_CHECKING([if $LUA is a Lua interprester]) - _AX_LUA_CHK_IS_INTRP([$LUA], - [AC_MSG_RESULT([yes])], - [ AC_MSG_RESULT([no]) - AC_MSG_ERROR([not a Lua interpreter]) - ]) - ], - [ dnl A version check is needed. - AS_IF([test "x$LUA" != 'x'], - [ dnl Check if this is a Lua interpreter. - AC_MSG_CHECKING([if $LUA is a Lua interprester]) - _AX_LUA_CHK_IS_INTRP([$LUA], - [AC_MSG_RESULT([yes])], - [ AC_MSG_RESULT([no]) - AC_MSG_ERROR([not a Lua interpreter]) - ]) - dnl Check the version. - m4_if([$2], [], - [_ax_check_text="whether $LUA version >= $1"], - [_ax_check_text="whether $LUA version >= $1, < $2"]) - AC_MSG_CHECKING([$_ax_check_text]) - _AX_LUA_CHK_VER([$LUA], [$1], [$2], - [AC_MSG_RESULT([yes])], - [ AC_MSG_RESULT([no]) - AC_MSG_ERROR([version is out of range for specified LUA])]) - ax_display_LUA=$LUA - ], - [ dnl Try each interpreter until we find one that satisfies VERSION. - m4_if([$2], [], - [_ax_check_text="for a Lua interpreter with version >= $1"], - [_ax_check_text="for a Lua interpreter with version >= $1, < $2"]) - AC_CACHE_CHECK([$_ax_check_text], - [ax_cv_pathless_LUA], - [ for ax_cv_pathless_LUA in _AX_LUA_INTERPRETER_LIST none; do - test "x$ax_cv_pathless_LUA" = 'xnone' && break - _AX_LUA_CHK_IS_INTRP([$ax_cv_pathless_LUA], [], [continue]) - _AX_LUA_CHK_VER([$ax_cv_pathless_LUA], [$1], [$2], [break]) - done - ]) - dnl Set $LUA to the absolute path of $ax_cv_pathless_LUA. - AS_IF([test "x$ax_cv_pathless_LUA" = 'xnone'], - [LUA=':'], - [AC_PATH_PROG([LUA], [$ax_cv_pathless_LUA])]) - ax_display_LUA=$ax_cv_pathless_LUA - ]) - ]) - - AS_IF([test "x$LUA" = 'x:'], - [ dnl Run any user-specified action, or abort. - m4_default([$4], [AC_MSG_ERROR([cannot find suitable Lua interpreter])]) - ], - [ dnl Query Lua for its version number. - AC_CACHE_CHECK([for $ax_display_LUA version], [ax_cv_lua_version], - [ ax_cv_lua_version=`$LUA -e "print(_VERSION)" | \ - sed "s|^Lua \(.*\)|\1|" | \ - grep -o "^@<:@0-9@:>@\+\\.@<:@0-9@:>@\+"` - ]) - AS_IF([test "x$ax_cv_lua_version" = 'x'], - [AC_MSG_ERROR([invalid Lua version number])]) - AC_SUBST([LUA_VERSION], [$ax_cv_lua_version]) - AC_SUBST([LUA_SHORT_VERSION], [`echo "$LUA_VERSION" | sed 's|\.||'`]) - - dnl The following check is not supported: - dnl At times (like when building shared libraries) you may want to know - dnl which OS platform Lua thinks this is. - AC_CACHE_CHECK([for $ax_display_LUA platform], [ax_cv_lua_platform], - [ax_cv_lua_platform=`$LUA -e "print('unknown')"`]) - AC_SUBST([LUA_PLATFORM], [$ax_cv_lua_platform]) - - dnl Use the values of $prefix and $exec_prefix for the corresponding - dnl values of LUA_PREFIX and LUA_EXEC_PREFIX. These are made distinct - dnl variables so they can be overridden if need be. However, the general - dnl consensus is that you shouldn't need this ability. - AC_SUBST([LUA_PREFIX], ['${prefix}']) - AC_SUBST([LUA_EXEC_PREFIX], ['${exec_prefix}']) - - dnl Lua provides no way to query the script directory, and instead - dnl provides LUA_PATH. However, we should be able to make a safe educated - dnl guess. If the built-in search path contains a directory which is - dnl prefixed by $prefix, then we can store scripts there. The first - dnl matching path will be used. - AC_CACHE_CHECK([for $ax_display_LUA script directory], - [ax_cv_lua_luadir], - [ AS_IF([test "x$prefix" = 'xNONE'], - [ax_lua_prefix=$ac_default_prefix], - [ax_lua_prefix=$prefix]) - - dnl Initialize to the default path. - ax_cv_lua_luadir="$LUA_PREFIX/share/lua/$LUA_VERSION" - - dnl Try to find a path with the prefix. - _AX_LUA_FND_PRFX_PTH([$LUA], [$ax_lua_prefix], [package.path]) - AS_IF([test "x$ax_lua_prefixed_path" != 'x'], - [ dnl Fix the prefix. - _ax_strip_prefix=`echo "$ax_lua_prefix" | sed 's|.|.|g'` - ax_cv_lua_luadir=`echo "$ax_lua_prefixed_path" | \ - sed "s,^$_ax_strip_prefix,$LUA_PREFIX,"` - ]) - ]) - AC_SUBST([luadir], [$ax_cv_lua_luadir]) - AC_SUBST([pkgluadir], [\${luadir}/$PACKAGE]) - - dnl Lua provides no way to query the module directory, and instead - dnl provides LUA_PATH. However, we should be able to make a safe educated - dnl guess. If the built-in search path contains a directory which is - dnl prefixed by $exec_prefix, then we can store modules there. The first - dnl matching path will be used. - AC_CACHE_CHECK([for $ax_display_LUA module directory], - [ax_cv_lua_luaexecdir], - [ AS_IF([test "x$exec_prefix" = 'xNONE'], - [ax_lua_exec_prefix=$ax_lua_prefix], - [ax_lua_exec_prefix=$exec_prefix]) - - dnl Initialize to the default path. - ax_cv_lua_luaexecdir="$LUA_EXEC_PREFIX/lib/lua/$LUA_VERSION" - - dnl Try to find a path with the prefix. - _AX_LUA_FND_PRFX_PTH([$LUA], - [$ax_lua_exec_prefix], [package.cpathd]) - AS_IF([test "x$ax_lua_prefixed_path" != 'x'], - [ dnl Fix the prefix. - _ax_strip_prefix=`echo "$ax_lua_exec_prefix" | sed 's|.|.|g'` - ax_cv_lua_luaexecdir=`echo "$ax_lua_prefixed_path" | \ - sed "s,^$_ax_strip_prefix,$LUA_EXEC_PREFIX,"` - ]) - ]) - AC_SUBST([luaexecdir], [$ax_cv_lua_luaexecdir]) - AC_SUBST([pkgluaexecdir], [\${luaexecdir}/$PACKAGE]) - - dnl Run any user specified action. - $3 - ]) -]) - -dnl AX_WITH_LUA is now the same thing as AX_PROG_LUA. -AC_DEFUN([AX_WITH_LUA], -[ - AC_MSG_WARN([[$0 is deprecated, please use AX_PROG_LUA]]) - AX_PROG_LUA -]) - - -dnl ========================================================================= -dnl _AX_LUA_CHK_IS_INTRP(PROG, [ACTION-IF-TRUE], [ACTION-IF-FALSE]) -dnl ========================================================================= -AC_DEFUN([_AX_LUA_CHK_IS_INTRP], -[ - dnl Just print _VERSION because all Lua interpreters have this global. - AS_IF([$1 -e "print('Hello ' .. _VERSION .. '!')" &>/dev/null], - [$2], [$3]) -]) - - -dnl ========================================================================= -dnl _AX_LUA_CHK_VER(PROG, MINIMUM-VERSION, [TOO-BIG-VERSION], -dnl [ACTION-IF-TRUE], [ACTION-IF-FALSE]) -dnl ========================================================================= -AC_DEFUN([_AX_LUA_CHK_VER], -[ - _ax_test_ver=`$1 -e "print(_VERSION)" 2>/dev/null | \ - sed "s|^Lua \(.*\)|\1|" | grep -o "^@<:@0-9@:>@\+\\.@<:@0-9@:>@\+"` - AS_IF([test "x$_ax_test_ver" = 'x'], - [_ax_test_ver='0']) - AX_COMPARE_VERSION([$_ax_test_ver], [ge], [$2]) - m4_if([$3], [], [], - [ AS_IF([$ax_compare_version], - [AX_COMPARE_VERSION([$_ax_test_ver], [lt], [$3])]) - ]) - AS_IF([$ax_compare_version], [$4], [$5]) -]) - - -dnl ========================================================================= -dnl _AX_LUA_FND_PRFX_PTH(PROG, PREFIX, LUA-PATH-VARIABLE) -dnl ========================================================================= -AC_DEFUN([_AX_LUA_FND_PRFX_PTH], -[ - dnl Invokes the Lua interpreter PROG to print the path variable - dnl LUA-PATH-VARIABLE, usually package.path or package.cpath. Paths are - dnl then matched against PREFIX. The first path to begin with PREFIX is set - dnl to ax_lua_prefixed_path. - - ax_lua_prefixed_path='' - _ax_package_paths=`$1 -e 'print($3)' 2>/dev/null | sed 's|;|\n|g'` - dnl Try the paths in order, looking for the prefix. - for _ax_package_path in $_ax_package_paths; do - dnl Copy the path, up to the use of a Lua wildcard. - _ax_path_parts=`echo "$_ax_package_path" | sed 's|/|\n|g'` - _ax_reassembled='' - for _ax_path_part in $_ax_path_parts; do - echo "$_ax_path_part" | grep '\?' >/dev/null && break - _ax_reassembled="$_ax_reassembled/$_ax_path_part" - done - dnl Check the path against the prefix. - _ax_package_path=$_ax_reassembled - if echo "$_ax_package_path" | grep "^$2" >/dev/null; then - dnl Found it. - ax_lua_prefixed_path=$_ax_package_path - break - fi - done -]) - - -dnl ========================================================================= -dnl AX_LUA_HEADERS([ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND]) -dnl ========================================================================= -AC_DEFUN([AX_LUA_HEADERS], -[ - dnl Check for LUA_VERSION. - AC_MSG_CHECKING([if LUA_VERSION is defined]) - AS_IF([test "x$LUA_VERSION" != 'x'], - [AC_MSG_RESULT([yes])], - [ AC_MSG_RESULT([no]) - AC_MSG_ERROR([cannot check Lua headers without knowing LUA_VERSION]) - ]) - - dnl Make LUA_INCLUDE a precious variable. - AC_ARG_VAR([LUA_INCLUDE], [The Lua includes, e.g. -I/usr/include/lua5.1]) - - dnl Some default directories to search. - LUA_SHORT_VERSION=`echo "$LUA_VERSION" | sed 's|\.||'` - m4_define_default([_AX_LUA_INCLUDE_LIST], - [ /usr/include/lua$LUA_VERSION \ - /usr/include/lua/$LUA_VERSION \ - /usr/include/lua$LUA_SHORT_VERSION \ - /usr/local/include/lua$LUA_VERSION \ - /usr/local/include/lua/$LUA_VERSION \ - /usr/local/include/lua$LUA_SHORT_VERSION \ - ]) - - dnl Try to find the headers. - _ax_lua_saved_cppflags=$CPPFLAGS - CPPFLAGS="$CPPFLAGS $LUA_INCLUDE" - AC_CHECK_HEADERS([lua.h lualib.h lauxlib.h luaconf.h]) - CPPFLAGS=$_ax_lua_saved_cppflags - - dnl Try some other directories if LUA_INCLUDE was not set. - AS_IF([test "x$LUA_INCLUDE" = 'x' && - test "x$ac_cv_header_lua_h" != 'xyes'], - [ dnl Try some common include paths. - for _ax_include_path in _AX_LUA_INCLUDE_LIST; do - test ! -d "$_ax_include_path" && continue - - AC_MSG_CHECKING([for Lua headers in]) - AC_MSG_RESULT([$_ax_include_path]) - - AS_UNSET([ac_cv_header_lua_h]) - AS_UNSET([ac_cv_header_lualib_h]) - AS_UNSET([ac_cv_header_lauxlib_h]) - AS_UNSET([ac_cv_header_luaconf_h]) - - _ax_lua_saved_cppflags=$CPPFLAGS - CPPFLAGS="$CPPFLAGS -I$_ax_include_path" - AC_CHECK_HEADERS([lua.h lualib.h lauxlib.h luaconf.h]) - CPPFLAGS=$_ax_lua_saved_cppflags - - AS_IF([test "x$ac_cv_header_lua_h" = 'xyes'], - [ LUA_INCLUDE="-I$_ax_include_path" - break - ]) - done - ]) - - AS_IF([test "x$ac_cv_header_lua_h" = 'xyes'], - [ dnl Make a program to print LUA_VERSION defined in the header. - dnl TODO This probably shouldn't be a runtime test. - - AC_CACHE_CHECK([for Lua header version], - [ax_cv_lua_header_version], - [ _ax_lua_saved_cppflags=$CPPFLAGS - CPPFLAGS="$CPPFLAGS $LUA_INCLUDE" - AC_RUN_IFELSE( - [ AC_LANG_SOURCE([[ -#include -#include -#include -int main(int argc, char ** argv) -{ - if(argc > 1) printf("%s", LUA_VERSION); - exit(EXIT_SUCCESS); -} -]]) - ], - [ ax_cv_lua_header_version=`./conftest$EXEEXT p | \ - sed "s|^Lua \(.*\)|\1|" | \ - grep -o "^@<:@0-9@:>@\+\\.@<:@0-9@:>@\+"` - ], - [ax_cv_lua_header_version='unknown']) - CPPFLAGS=$_ax_lua_saved_cppflags - ]) - - dnl Compare this to the previously found LUA_VERSION. - AC_MSG_CHECKING([if Lua header version matches $LUA_VERSION]) - AS_IF([test "x$ax_cv_lua_header_version" = "x$LUA_VERSION"], - [ AC_MSG_RESULT([yes]) - ax_header_version_match='yes' - ], - [ AC_MSG_RESULT([no]) - ax_header_version_match='no' - ]) - ]) - - dnl Was LUA_INCLUDE specified? - AS_IF([test "x$ax_header_version_match" != 'xyes' && - test "x$LUA_INCLUDE" != 'x'], - [AC_MSG_ERROR([cannot find headers for specified LUA_INCLUDE])]) - - dnl Test the final result and run user code. - AS_IF([test "x$ax_header_version_match" = 'xyes'], [$1], - [m4_default([$2], [AC_MSG_ERROR([cannot find Lua includes])])]) -]) - -dnl AX_LUA_HEADERS_VERSION no longer exists, use AX_LUA_HEADERS. -AC_DEFUN([AX_LUA_HEADERS_VERSION], -[ - AC_MSG_WARN([[$0 is deprecated, please use AX_LUA_HEADERS]]) -]) - - -dnl ========================================================================= -dnl AX_LUA_LIBS([ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND]) -dnl ========================================================================= -AC_DEFUN([AX_LUA_LIBS], -[ - dnl TODO Should this macro also check various -L flags? - - dnl Check for LUA_VERSION. - AC_MSG_CHECKING([if LUA_VERSION is defined]) - AS_IF([test "x$LUA_VERSION" != 'x'], - [AC_MSG_RESULT([yes])], - [ AC_MSG_RESULT([no]) - AC_MSG_ERROR([cannot check Lua libs without knowing LUA_VERSION]) - ]) - - dnl Make LUA_LIB a precious variable. - AC_ARG_VAR([LUA_LIB], [The Lua library, e.g. -llua5.1]) - - AS_IF([test "x$LUA_LIB" != 'x'], - [ dnl Check that LUA_LIBS works. - _ax_lua_saved_libs=$LIBS - LIBS="$LIBS $LUA_LIB" - AC_SEARCH_LIBS([lua_load], [], - [_ax_found_lua_libs='yes'], - [_ax_found_lua_libs='no']) - LIBS=$_ax_lua_saved_libs - - dnl Check the result. - AS_IF([test "x$_ax_found_lua_libs" != 'xyes'], - [AC_MSG_ERROR([cannot find libs for specified LUA_LIB])]) - ], - [ dnl First search for extra libs. - _ax_lua_extra_libs='' - - _ax_lua_saved_libs=$LIBS - LIBS="$LIBS $LUA_LIB" - AC_SEARCH_LIBS([exp], [m]) - AC_SEARCH_LIBS([dlopen], [dl]) - LIBS=$_ax_lua_saved_libs - - AS_IF([test "x$ac_cv_search_exp" != 'xno' && - test "x$ac_cv_search_exp" != 'xnone required'], - [_ax_lua_extra_libs="$_ax_lua_extra_libs $ac_cv_search_exp"]) - - AS_IF([test "x$ac_cv_search_dlopen" != 'xno' && - test "x$ac_cv_search_dlopen" != 'xnone required'], - [_ax_lua_extra_libs="$_ax_lua_extra_libs $ac_cv_search_dlopen"]) - - dnl Try to find the Lua libs. - _ax_lua_saved_libs=$LIBS - LIBS="$LIBS $LUA_LIB" - AC_SEARCH_LIBS([lua_load], [lua$LUA_VERSION lua$LUA_SHORT_VERSION lua], - [_ax_found_lua_libs='yes'], - [_ax_found_lua_libs='no'], - [$_ax_lua_extra_libs]) - LIBS=$_ax_lua_saved_libs - - AS_IF([test "x$ac_cv_search_lua_load" != 'xno' && - test "x$ac_cv_search_lua_load" != 'xnone required'], - [LUA_LIB="$ac_cv_search_lua_load $_ax_lua_extra_libs"]) - ]) - - dnl Test the result and run user code. - AS_IF([test "x$_ax_found_lua_libs" = 'xyes'], [$1], - [m4_default([$2], [AC_MSG_ERROR([cannot find Lua libs])])]) -]) - - -dnl ========================================================================= -dnl AX_LUA_READLINE([ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND]) -dnl ========================================================================= -AC_DEFUN([AX_LUA_READLINE], -[ - AX_LIB_READLINE - AS_IF([test "x$ac_cv_header_readline_readline_h" != 'x' && - test "x$ac_cv_header_readline_history_h" != 'x'], - [ LUA_LIBS_CFLAGS="-DLUA_USE_READLINE $LUA_LIBS_CFLAGS" - $1 - ], - [$2]) -]) diff --git a/m4/ax_with_prog.m4 b/m4/ax_with_prog.m4 deleted file mode 100644 index f337c05..0000000 --- a/m4/ax_with_prog.m4 +++ /dev/null @@ -1,70 +0,0 @@ -# =========================================================================== -# http://www.gnu.org/software/autoconf-archive/ax_with_prog.html -# =========================================================================== -# -# SYNOPSIS -# -# AX_WITH_PROG([VARIABLE],[program],[VALUE-IF-NOT-FOUND],[PATH]) -# -# DESCRIPTION -# -# Locates an installed program binary, placing the result in the precious -# variable VARIABLE. Accepts a present VARIABLE, then --with-program, and -# failing that searches for program in the given path (which defaults to -# the system path). If program is found, VARIABLE is set to the full path -# of the binary; if it is not found VARIABLE is set to VALUE-IF-NOT-FOUND -# if provided, unchanged otherwise. -# -# A typical example could be the following one: -# -# AX_WITH_PROG(PERL,perl) -# -# NOTE: This macro is based upon the original AX_WITH_PYTHON macro from -# Dustin J. Mitchell . -# -# LICENSE -# -# Copyright (c) 2008 Francesco Salvestrini -# Copyright (c) 2008 Dustin J. Mitchell -# -# Copying and distribution of this file, with or without modification, are -# permitted in any medium without royalty provided the copyright notice -# and this notice are preserved. This file is offered as-is, without any -# warranty. - -#serial 16 - -AC_DEFUN([AX_WITH_PROG],[ - AC_PREREQ([2.61]) - - pushdef([VARIABLE],$1) - pushdef([EXECUTABLE],$2) - pushdef([VALUE_IF_NOT_FOUND],$3) - pushdef([PATH_PROG],$4) - - AC_ARG_VAR(VARIABLE,Absolute path to EXECUTABLE executable) - - AS_IF(test -z "$VARIABLE",[ - AC_MSG_CHECKING(whether EXECUTABLE executable path has been provided) - AC_ARG_WITH(EXECUTABLE,AS_HELP_STRING([--with-EXECUTABLE=[[[PATH]]]],absolute path to EXECUTABLE executable), [ - AS_IF([test "$withval" != yes && test "$withval" != no],[ - VARIABLE="$withval" - AC_MSG_RESULT($VARIABLE) - ],[ - VARIABLE="" - AC_MSG_RESULT([no]) - AS_IF([test "$withval" != no], [ - AC_PATH_PROG([]VARIABLE[],[]EXECUTABLE[],[]VALUE_IF_NOT_FOUND[],[]PATH_PROG[]) - ]) - ]) - ],[ - AC_MSG_RESULT([no]) - AC_PATH_PROG([]VARIABLE[],[]EXECUTABLE[],[]VALUE_IF_NOT_FOUND[],[]PATH_PROG[]) - ]) - ]) - - popdef([PATH_PROG]) - popdef([VALUE_IF_NOT_FOUND]) - popdef([EXECUTABLE]) - popdef([VARIABLE]) -]) diff --git a/mkrockspecs.lua b/mkrockspecs.lua deleted file mode 100644 index ee3e80b..0000000 --- a/mkrockspecs.lua +++ /dev/null @@ -1,49 +0,0 @@ --- Generate rockspecs from a prototype with variants - -require "std" - -if select ("#", ...) < 2 then - io.stderr:write "Usage: mkrockspecs PACKAGE VERSION\n" - os.exit () -end - -package_name = select (1, ...) -version = select (2, ...) - -function format (x, indent) - indent = indent or "" - if type (x) == "table" then - local s = "{\n" - for i, v in pairs (x) do - if type (i) ~= "number" then - s = s..indent..i.." = "..format (v, indent.." ")..",\n" - end - end - for i, v in ipairs (x) do - s = s..indent..format (v, indent.." ")..",\n" - end - return s..indent:sub (1, -3).."}" - elseif type (x) == "string" then - return string.format ("%q", x) - else - return tostring (x) - end -end - -for f, spec in pairs (loadfile ("rockspecs.lua") ()) do - if f ~= "default" then - local specfile = package_name.."-"..(f ~= "" and f:lower ().."-" or "")..version.."-1.rockspec" - h = io.open (specfile, "w") - assert (h) - flavour = f -- a global, visible in loadfile - local specs = loadfile ("rockspecs.lua") () -- reload to get current flavour interpolated - local spec = tree.merge (tree.new (specs.default), tree.new (specs[f])) - local s = "" - for i, v in pairs (spec) do - s = s..i.." = "..format (v, " ").."\n" - end - h:write (s) - h:close () - os.execute ("luarocks lint " .. specfile) - end -end diff --git a/rockspecs.lua b/rockspecs.lua deleted file mode 100644 index 002dbf5..0000000 --- a/rockspecs.lua +++ /dev/null @@ -1,44 +0,0 @@ --- Rockspec data - --- Variables to be interpolated: --- --- flavour: regex library --- package --- version - -local version_dashed = version:gsub ("%.", "-") - -local default = { - package = package_name, - version = version.."-1", - source = { - url = "git://github.com/rrthomas/lua-stdlib.git", - }, - description = { - summary = "General Lua libraries", - detailed = [[ - stdlib is a library of modules for common programming tasks, - including list, table and functional operations, regexps, objects, - pickling, pretty-printing and getopt. - ]], - homepage = "http://github.com/rrthomas/lua-stdlib/", - license = "MIT/X11", - }, - dependencies = { - "lua >= 5.1", - }, - build = { - type = "command", - build_command = "LUA=$(LUA) CPPFLAGS=-I$(LUA_INCDIR) ./configure --prefix=$(PREFIX) --libdir=$(LIBDIR) --datadir=$(LUADIR) && make clean && make", - install_command = "make install", - copy_directories = {}, - }, -} - -if version ~= "git" then - default.source.branch = "release-v"..version_dashed -else - default.build.build_command = "autoreconf -i && " .. default.build.build_command -end - -return {default=default, [""]={}} diff --git a/spec/container_spec.yaml b/spec/container_spec.yaml new file mode 100644 index 0000000..8f1c333 --- /dev/null +++ b/spec/container_spec.yaml @@ -0,0 +1,120 @@ +before: + Container = require "std.container" {} + prototype = require "std.object".prototype + +specify std.container: +- context when required: + - context by name: + - it does not touch the global table: + expect (show_apis {added_to="_G", by="std.container"}). + to_equal {} + +- describe construction: + - context with table _init: + - it diagnoses missing arguments: | + expect (Container ()). + to_raise "bad argument #1 to 'Container' (table expected, got no value)" + - it diagnoses too many arguments: | + expect (Container ({}, false)). + to_raise "bad argument #2 to 'Container' (no more than 1 argument expected, got 2)" + - context with function _init: + - before: + Thing = Container { _type = "Thing", _init = function (obj) return obj end } + - it doesn't diagnose missing arguments: + expect (Thing ()).not_to_raise "any error" + - it doesn't diagnose too many args: + expect (Thing ({}, false)).not_to_raise "any error" + + - context from Container prototype: + - before: + things = Container {"foo", "bar", baz="quux"} + - it constructs a new container: + expect (things).not_to_be (Container) + expect (type (things)).to_be "table" + expect (prototype (things)).to_be "Container" + - it reuses the container metatable: + o, p = things {"o"}, things {"p"} + expect (getmetatable (o)).to_be (getmetatable (p)) + - it sets container fields from arguments: + o = Container {"foo", "bar", baz="quux"} + expect (o).to_equal (things) + - it serves as a prototype for new instances: + o = things {} + expect (prototype (o)).to_be "Container" + expect (o).to_copy (things) + expect (getmetatable (o)).to_be (getmetatable (things)) + - it separates '_' prefixed fields: + expect (Container {foo="bar", _baz="quux"}). + to_equal (Container {foo="bar"}) + - it puts '_' prefixed fields in a new metatable: + things = Container {foo="bar", _baz="quux"} + expect (getmetatable (things)).not_to_be (getmetatable (Container)) + expect (getmetatable (things)._baz).to_be "quux" + - context with module functions: + - before: + reduce = require "std.functional".reduce + elems = require "std".elems + functions = { + count = function (bag) + return reduce (function (r, k) return r + k end, 0, elems, bag) + end, + } + Bag = Container { + _type = "Bag", _functions = functions, count = functions.count, + } + - it does not propagate _functions: + things = Bag {} + expect (things.count).to_be (nil) + - it does not provide object methods: | + things = Bag {} + expect (things:count ()).to_raise.any_of { + "attempt to call method 'count'", + "attempt to call a nil value (method 'count'" + } + - it does retain module functions: + things = Bag { apples = 1, oranges = 3 } + expect (Bag.count (things)).to_be (4) + - it does allow elements named after module functions: + things = Bag { count = 1337 } + expect (Bag.count (things)).to_be (1337) + + +- describe field access: + - before: + things = Container {"foo", "bar", baz="quux"} + - context with bracket notation: + - it provides access to existing contents: + expect (things[1]).to_be "foo" + expect (things["baz"]).to_be "quux" + - it assigns new contents: + things["new"] = "value" + expect (things["new"]).to_be "value" + - context with dot notation: + - it provides access to existing contents: + expect (things.baz).to_be "quux" + - it assigns new contents: + things.new = "value" + expect (things.new).to_be "value" + + +- describe stringification: + - before: + things = Container {_type = "Derived", "one", "two", "three"} + - it returns a string: + expect (type (tostring (things))).to_be "string" + - it contains the type: + expect (tostring (Container {})).to_contain "Container" + expect (tostring (things)).to_contain (prototype (things)) + - it contains the ordered array part elements: + expect (tostring (things)).to_contain "one, two, three" + - it contains the ordered dictionary part elements: + expect (tostring (Container {one = true, two = true, three = true})). + to_contain "one=true, three=true, two=true" + expect (tostring (things {one = true, two = true, three = true})). + to_contain "one=true, three=true, two=true" + - it contains a ';' separator only when container has array and dictionary parts: + expect (tostring (things)).not_to_contain ";" + expect (tostring (Container {one = true, two = true, three = true})). + not_to_contain ";" + expect (tostring (things {one = true, two = true, three = true})). + to_contain ";" diff --git a/spec/debug_spec.yaml b/spec/debug_spec.yaml new file mode 100644 index 0000000..9a8757e --- /dev/null +++ b/spec/debug_spec.yaml @@ -0,0 +1,1224 @@ +before: | + base_module = "debug" + this_module = "std.debug" + global_table = "_G" + + extend_base = { "DEPRECATED", "DEPRECATIONMSG", "argcheck", "argerror", + "argscheck", "extramsg_mismatch", "extramsg_toomany", + "getfenv", "parsetypes", "resulterror", "setfenv", "say", + "toomanyargmsg", "typesplit", "trace", "_setdebug" } + + M = require (this_module) + +specify std.debug: +- context when required: + - context by name: + - it does not touch the global table: + expect (show_apis {added_to=global_table, by=this_module}). + to_equal {} + - it does not touch the core debug table: + expect (show_apis {added_to=base_module, by=this_module}). + to_equal {} + - it contains apis from the core debug table: + expect (show_apis {from=base_module, not_in=this_module}). + to_contain.a_permutation_of (extend_base) + + - context via the std module: + - it does not touch the global table: + expect (show_apis {added_to=global_table, by="std"}). + to_equal {} + - it does not touch the core debug table: + expect (show_apis {added_to=base_module, by="std"}). + to_equal {} + + +- describe DEPRECATED: + - before: | + function runscript (body, name, args) + return luaproc ( + "require '" .. this_module .. "'.DEPRECATED ('0', '" .. + (name or "runscript") .. "', function (...)" .. + " " .. body .. + " end) " .. + "('" .. table.concat (args or {}, "', '") .. "')" + ) + end + + f, badarg = init (M, this_module, "DEPRECATED") + + - it returns a function: + expect (type (f ("0", "deprecated", nop))).to_be "function" + expect (f ("0", "deprecated", nop)).not_to_be (nop) + - context with deprecated function: + - it executes the deprecated function: + expect (runscript 'error "oh noes!"').to_contain_error "oh noes!" + - it passes arguments to the deprecated function: + expect (runscript ("print (table.concat ({...}, ', '))", nil, + {"foo", "bar", "baz"})).to_output "foo, bar, baz\n" + - it returns deprecated function results: | + script = [[ + DEPRECATED = require "std.debug".DEPRECATED + fn = DEPRECATED ("0", "fn", function () return "foo", "bar", "baz" end) + print (fn ()) + ]] + expect (luaproc (script)).to_output "foo\tbar\tbaz\n" + - it writes a warning to stderr: + expect (runscript 'error "oh noes!"'). + to_match_error "deprecated.*, and will be removed" + - it writes the version string to stderr: + expect (runscript 'error "oh noes!"'). + to_contain_error "in release 0" + - it writes the call location to stderr: | + expect (runscript 'error "oh noes!"'). + to_match_error "^%S+:1: " + - context with _DEBUG: + - before: | + script = [[ + DEPRECATED = require "]] .. this_module .. [[".DEPRECATED + fn = DEPRECATED ("0", "fn", function () io.stderr:write "oh noes!\n" end) + fn () -- line 3 + fn () -- line 4 + ]] + - it warns every call by default: + expect (luaproc (script)).to_match_error "^%S+:3:.*deprecated" + expect (luaproc (script)).to_match_error "\n%S+:4:.*deprecated" + - it does not warn at all with _DEBUG set to false: + script = "_DEBUG = false " .. script + expect (luaproc (script)).not_to_match_error "%d:.*deprecated" + - it does not define the function with _DEBUG set to true: | + script = "_DEBUG = true " .. script + expect (luaproc (script)).to_contain_error.any_of { + ":3: attempt to call global 'fn'", + ":3: attempt to call a nil value (global 'fn')", + } + - it warns on every call with _DEBUG.deprecate unset: + script = "_DEBUG = {} " .. script + expect (luaproc (script)).to_match_error "^%S+:3:.*deprecated" + expect (luaproc (script)).to_match_error "\n%S+:4:.*deprecated" + - it does not warn at all with _DEBUG.deprecate set to false: + script = "_DEBUG = { deprecate = false } " .. script + expect (luaproc (script)).not_to_match_error "%d:.*deprecated" + - it warns on every call with _DEBUG.deprecate set to true: | + script = "_DEBUG = { deprecate = true } " .. script + expect (luaproc (script)).to_contain_error.any_of { + ":3: attempt to call global 'fn'", + ":3: attempt to call a nil value (global 'fn')", + } + + +- describe DEPRECATIONMSG: + - before: | + function mkscript (lvl) + return [[ + DEPRECATIONMSG = require "]] .. this_module .. [[".DEPRECATIONMSG + function fn () + io.stderr:write (DEPRECATIONMSG ("42", "spec file", ]] .. lvl .. [[)) + end + fn () -- line 5 + fn () -- line 6 + ]] + end + + f = M.DEPRECATIONMSG + + - it contains deprecating the release version: + expect (f ("41", "foo", 2)).to_contain "41" + - it contains the deprecation function name: + expect (f ("41", "some.module.fname", 2)).to_contain "some.module.fname" + - it appends an optional extra message: + expect (f ("41", "wuh", "ah boo", 2)).to_contain ", ah boo." + - it blames the given stack level: + expect (luaproc (mkscript (1))).to_match_error "^%S+:3:.*deprecated" + expect (luaproc (mkscript (2))).to_match_error "^%S+:5:.*deprecated" + + +- describe resulterror: + - before: | + function mkstack (level) + return string.format ([[ + _DEBUG = true -- line 1 + local debug = require "std.debug" -- line 2 + function ohnoes () -- line 3 + debug.resulterror ("ohnoes", 1, nil, %s) -- line 4 + end -- line 5 + function caller () -- line 6 + local r = ohnoes () -- line 7 + return "not a tail call" -- line 8 + end -- line 9 + caller () -- line 10 + ]], tostring (level)) + end + + f = M.resulterror + + - it blames the call site by default: | + expect (luaproc (mkstack ())).to_contain_error ":4: bad result" + - it honors optional call stack level reporting: | + expect (luaproc (mkstack (1))).to_contain_error ":4: bad result" + expect (luaproc (mkstack (2))).to_contain_error ":7: bad result" + - it reports the calling function name: + expect (f ('expect', 1)).to_raise "'expect'" + - it reports the argument number: | + expect (f ('expect', 12345)).to_raise "#12345" + - it reports extra message in parentheses: + expect (f ('expect', 1, "extramsg")).to_raise " (extramsg)" + + +- describe argerror: + - before: | + function mkstack (level) + return string.format ([[ + _DEBUG = true -- line 1 + local debug = require "std.debug" -- line 2 + function ohnoes () -- line 3 + debug.argerror ("ohnoes", 1, nil, %s) -- line 4 + end -- line 5 + function caller () -- line 6 + local r = ohnoes () -- line 7 + return "not a tail call" -- line 8 + end -- line 9 + caller () -- line 10 + ]], tostring (level)) + end + + f, badarg = init (M, this_module, "argerror") + + - it diagnoses missing arguments: + pending "Lua 5.1 support is dropped" + expect (f ()).to_raise (badarg (1, "string")) + expect (f "foo").to_raise (badarg (2, "int")) + - it diagnoses wrong argument types: + pending "Lua 5.1 support is dropped" + expect (f (false)).to_raise (badarg (1, "string", "boolean")) + expect (f ("foo", false)).to_raise (badarg (2, "int", "boolean")) + expect (f ("foo", 1, false)). + to_raise (badarg (3, "string or nil", "boolean")) + expect (f ("foo", 1, "bar", false)). + to_raise (badarg (4, "int or nil", "boolean")) + - it diagnoses too many arguments: + pending "Lua 5.1 support is dropped" + expect (f ("foo", 1, "bar", 2, false)).to_raise (badarg (5)) + + - it blames the call site by default: | + expect (luaproc (mkstack ())).to_contain_error ":4: bad argument" + - it honors optional call stack level reporting: | + expect (luaproc (mkstack (1))).to_contain_error ":4: bad argument" + expect (luaproc (mkstack (2))).to_contain_error ":7: bad argument" + - it reports the calling function name: + expect (f ('expect', 1)).to_raise "'expect'" + - it reports the argument number: | + expect (f ('expect', 12345)).to_raise "#12345" + - it reports extra message in parentheses: + expect (f ('expect', 1, "extramsg")).to_raise " (extramsg)" + + +- describe argcheck: + - before: | + Object = require 'std.object' + List = Object { _type = "List" } + Foo = Object { _type = "Foo" } + + function fn (...) return M.argcheck ('expect', 1, ...) end + + function mkstack (level, debugp) + return string.format ([[ + _DEBUG = %s -- line 1 + local debug = require "std.debug" -- line 2 + function ohnoes (t) -- line 3 + debug.argcheck ("ohnoes", 1, "table", t, %s) -- line 4 + end -- line 5 + function caller () -- line 6 + local r = ohnoes "not a table" -- line 7 + return "not a tail call" -- line 8 + end -- line 9 + caller () -- line 10 + ]], tostring (debugp), tostring (level)) + end + + f, badarg = init (M, this_module, "argcheck") + + - it diagnoses missing arguments: + pending "Lua 5.1 support is dropped" + expect (f ()).to_raise (badarg (1, "string")) + expect (f "foo").to_raise (badarg (2, "int")) + expect (f ("foo", 1)).to_raise (badarg (3, "string")) + - it diagnoses wrong argument types: + pending "Lua 5.1 support is dropped" + expect (f (false)).to_raise (badarg (1, "string", "boolean")) + expect (f ("foo", false)).to_raise (badarg (2, "int", "boolean")) + expect (f ("foo", 1, false)).to_raise (badarg (3, "string", "boolean")) + expect (f ("foo", 1, "bar", 2, false)). + to_raise (badarg (5, "int or nil", "boolean")) + - it diagnoses too many arguments: + pending "Lua 5.1 support is dropped" + expect (f ("foo", 1, "bar", 2, 3, false)).to_raise (badarg (6)) + + - it blames the calling function by default: | + expect (luaproc (mkstack ())).to_contain_error ":7: bad argument" + - it honors optional call stack level reporting: | + expect (luaproc (mkstack (1))).to_contain_error ":4: bad argument" + expect (luaproc (mkstack (2))).to_contain_error ":7: bad argument" + expect (luaproc (mkstack (3))).to_contain_error ":10: bad argument" + - it can be disabled by setting _DEBUG to false: + expect (luaproc (mkstack (nil, false))). + not_to_contain_error "bad argument" + - it can be disabled by setting _DEBUG.argcheck to false: + expect (luaproc (mkstack (nil, "{ argcheck = false }"))). + not_to_contain_error "bad argument" + - it is not disabled by setting _DEBUG.argcheck to true: + expect (luaproc (mkstack (nil, "{ argcheck = true }"))). + to_contain_error "bad argument" + - it is not disabled by leaving _DEBUG.argcheck unset: + expect (luaproc (mkstack (nil, "{}"))). + to_contain_error "bad argument" + + - context with primitives: + - it diagnoses missing types: + expect (fn ("bool", nil)).to_raise "boolean expected, got no value" + expect (fn ("boolean", nil)).to_raise "boolean expected, got no value" + expect (fn ("file", nil)).to_raise "FILE* expected, got no value" + expect (fn ("number", nil)).to_raise "number expected, got no value" + expect (fn ("string", nil)).to_raise "string expected, got no value" + expect (fn ("table", nil)).to_raise "table expected, got no value" + - it diagnoses mismatched types: + expect (fn ("bool", {0})).to_raise "boolean expected, got table" + expect (fn ("boolean", {0})).to_raise "boolean expected, got table" + expect (fn ("file", {0})).to_raise "FILE* expected, got table" + expect (fn ("number", {0})).to_raise "number expected, got table" + expect (fn ("string", {0})).to_raise "string expected, got table" + expect (fn ("table", false)).to_raise "table expected, got boolean" + - it matches types: + expect (fn ("bool", true)).not_to_raise "any error" + expect (fn ("boolean", true)).not_to_raise "any error" + expect (fn ("file", io.stderr)).not_to_raise "any error" + expect (fn ("number", 1)).not_to_raise "any error" + expect (fn ("string", "s")).not_to_raise "any error" + expect (fn ("table", {})).not_to_raise "any error" + expect (fn ("table", require "std.object")).not_to_raise "any error" + + - context with int: + - it diagnoses missing types: + expect (fn ("int", nil)).to_raise "int expected, got no value" + - it diagnoses mismatched types: + expect (fn ("int", false)).to_raise "int expected, got boolean" + expect (fn ("int", 1.234)).to_raise "int expected, got number" + expect (fn ("int", 1234e-3)).to_raise "int expected, got number" + - it matches types: + expect (fn ("int", 1)).not_to_raise "any error" + expect (fn ("int", 1.0)).not_to_raise "any error" + expect (fn ("int", 0x1234)).not_to_raise "any error" + expect (fn ("int", 1.234e3)).not_to_raise "any error" + - context with constant string: + - it diagnoses missing types: + expect (fn (":foo", nil)).to_raise ":foo expected, got no value" + - it diagnoses mismatched types: + expect (fn (":foo", false)).to_raise ":foo expected, got boolean" + expect (fn (":foo", ":bar")).to_raise ":foo expected, got :bar" + expect (fn (":foo", "foo")).to_raise ":foo expected, got string" + - it matches types: + expect (fn (":foo", ":foo")).not_to_raise "any error" + - context with callable types: + - it diagnoses missing types: + expect (fn ("func", nil)).to_raise "function expected, got no value" + expect (fn ("function", nil)).to_raise "function expected, got no value" + - it diagnoses mismatched types: + expect (fn ("func", {0})).to_raise "function expected, got table" + expect (fn ("function", {0})).to_raise "function expected, got table" + - it matches types: + expect (fn ("func", function () end)).not_to_raise "any error" + expect (fn ("func", setmetatable ({}, {__call = function () end}))). + not_to_raise "any error" + expect (fn ("function", function () end)).not_to_raise "any error" + expect (fn ("function", setmetatable ({}, {__call = function () end}))). + not_to_raise "any error" + - context with table of homogenous elements: + - it diagnoses missing types: + expect (fn ("table of boolean", nil)). + to_raise "table expected, got no value" + expect (fn ("table of booleans", nil)). + to_raise "table expected, got no value" + - it diagnoses mismatched types: + expect (fn ("table of file", io.stderr)). + to_raise "table expected, got file" + expect (fn ("table of files", io.stderr)). + to_raise "table expected, got file" + - it diagnoses mismatched element types: + expect (fn ("table of number", {false})). + to_raise "table of numbers expected, got boolean at index 1" + expect (fn ("table of numbers", {1, 2, "3"})). + to_raise "table of numbers expected, got string at index 3" + expect (fn ("table of numbers", {a=1, b=2, c="3"})). + to_raise "table of numbers expected, got string at index c" + - it matches types: + expect (fn ("table of string", {})).not_to_raise "any error" + expect (fn ("table of string", {"foo"})).not_to_raise "any error" + expect (fn ("table of string", {"f", "o", "o"})).not_to_raise "any error" + expect (fn ("table of string", {b="b", a="a", r="r"})).not_to_raise "any error" + - context with non-empty table types: + - it diagnoses missing types: + expect (fn ("#table", nil)). + to_raise "non-empty table expected, got no value" + - it diagnoses mismatched types: + expect (fn ("#table", false)). + to_raise "non-empty table expected, got boolean" + expect (fn ("#table", {})). + to_raise "non-empty table expected, got empty table" + - it matches types: + expect (fn ("#table", {0})).not_to_raise "any error" + - context with non-empty table of homogenous elements: + - it diagnoses missing types: + expect (fn ("#table of boolean", nil)). + to_raise "non-empty table expected, got no value" + expect (fn ("#table of booleans", nil)). + to_raise "non-empty table expected, got no value" + - it diagnoses mismatched types: + expect (fn ("#table of file", {})). + to_raise "non-empty table expected, got empty table" + expect (fn ("#table of file", io.stderr)). + to_raise "non-empty table expected, got file" + - it diagnoses mismatched element types: + expect (fn ("#table of number", {false})). + to_raise "non-empty table of numbers expected, got boolean at index 1" + expect (fn ("#table of numbers", {1, 2, "3"})). + to_raise "non-empty table of numbers expected, got string at index 3" + expect (fn ("#table of numbers", {a=1, b=2, c="3"})). + to_raise "non-empty table of numbers expected, got string at index c" + - it matches types: + expect (fn ("#table of string", {"foo"})).not_to_raise "any error" + expect (fn ("#table of string", {"f", "o", "o"})).not_to_raise "any error" + expect (fn ("#table of string", {b="b", a="a", r="r"})).not_to_raise "any error" + - context with list: + - it diagnonses missing types: + expect (fn ("list", nil)). + to_raise "list expected, got no value" + - it diagnoses mismatched types: + expect (fn ("list", false)). + to_raise "list expected, got boolean" + expect (fn ("list", {foo=1})). + to_raise "list expected, got table" + expect (fn ("list", Object)). + to_raise "list expected, got Object" + - it matches types: + expect (fn ("list", {})).not_to_raise "any error" + expect (fn ("list", {1})).not_to_raise "any error" + - context with list of homogenous elements: + - it diagnoses missing types: + expect (fn ("list of boolean", nil)). + to_raise "list expected, got no value" + expect (fn ("list of booleans", nil)). + to_raise "list expected, got no value" + - it diagnoses mismatched types: + expect (fn ("list of file", io.stderr)). + to_raise "list expected, got file" + expect (fn ("list of files", io.stderr)). + to_raise "list expected, got file" + expect (fn ("list of files", {file=io.stderr})). + to_raise "list expected, got table" + - it diagnoses mismatched element types: + expect (fn ("list of number", {false})). + to_raise "list of numbers expected, got boolean at index 1" + expect (fn ("list of numbers", {1, 2, "3"})). + to_raise "list of numbers expected, got string at index 3" + - it matches types: + expect (fn ("list of string", {})).not_to_raise "any error" + expect (fn ("list of string", {"foo"})).not_to_raise "any error" + expect (fn ("list of string", {"f", "o", "o"})).not_to_raise "any error" + - context with non-empty list: + - it diagnonses missing types: + expect (fn ("#list", nil)). + to_raise "non-empty list expected, got no value" + - it diagnoses mismatched types: + expect (fn ("#list", false)). + to_raise "non-empty list expected, got boolean" + expect (fn ("#list", {})). + to_raise "non-empty list expected, got empty list" + expect (fn ("#list", {foo=1})). + to_raise "non-empty list expected, got table" + expect (fn ("#list", Object)). + to_raise "non-empty list expected, got empty Object" + expect (fn ("#list", List {})). + to_raise "non-empty list expected, got empty List" + - it matches types: + expect (fn ("#list", {1})).not_to_raise "any error" + - context with non-empty list of homogenous elements: + - it diagnoses missing types: + expect (fn ("#list of boolean", nil)). + to_raise "non-empty list expected, got no value" + expect (fn ("#list of booleans", nil)). + to_raise "non-empty list expected, got no value" + - it diagnoses mismatched types: + expect (fn ("#list of file", {})). + to_raise "non-empty list expected, got empty table" + expect (fn ("#list of file", io.stderr)). + to_raise "non-empty list expected, got file" + expect (fn ("#list of files", {file=io.stderr})). + to_raise "non-empty list expected, got table" + - it diagnoses mismatched element types: + expect (fn ("#list of number", {false})). + to_raise "non-empty list of numbers expected, got boolean at index 1" + expect (fn ("#list of numbers", {1, 2, "3"})). + to_raise "non-empty list of numbers expected, got string at index 3" + - it matches types: + expect (fn ("#list of string", {"foo"})).not_to_raise "any error" + expect (fn ("#list of string", {"f", "o", "o"})).not_to_raise "any error" + - context with container: + - it diagnoses missing types: + expect (fn ("List of boolean", nil)). + to_raise "List expected, got no value" + expect (fn ("List of booleans", nil)). + to_raise "List expected, got no value" + - it diagnoses mismatched types: + expect (fn ("List of file", io.stderr)). + to_raise "List expected, got file" + expect (fn ("List of files", io.stderr)). + to_raise "List expected, got file" + expect (fn ("List of files", {file=io.stderr})). + to_raise "List expected, got table" + - it diagnoses mismatched element types: + expect (fn ("List of number", List {false})). + to_raise "List of numbers expected, got boolean at index 1" + expect (fn ("List of numbers", List {1, 2, "3"})). + to_raise "List of numbers expected, got string at index 3" + - it matches types: + expect (fn ("list of string", List {})).not_to_raise "any error" + expect (fn ("list of string", List {"foo"})).not_to_raise "any error" + expect (fn ("list of string", List {"f", "o", "o"})).not_to_raise "any error" + - context with object: + - it diagnoses missing types: + expect (fn ("object", nil)).to_raise "object expected, got no value" + expect (fn ("Object", nil)).to_raise "Object expected, got no value" + expect (fn ("Foo", nil)).to_raise "Foo expected, got no value" + expect (fn ("any", nil)).to_raise "any value expected, got no value" + - it diagnoses mismatched types: + expect (fn ("object", {0})).to_raise "object expected, got table" + expect (fn ("Object", {0})).to_raise "Object expected, got table" + expect (fn ("object", {_type="Object"})).to_raise "object expected, got table" + expect (fn ("Object", {_type="Object"})).to_raise "Object expected, got table" + expect (fn ("Object", Foo)).to_raise "Object expected, got Foo" + expect (fn ("Foo", {0})).to_raise "Foo expected, got table" + expect (fn ("Foo", Object)).to_raise "Foo expected, got Object" + - it matches types: + expect (fn ("object", Object)).not_to_raise "any error" + expect (fn ("object", Object {})).not_to_raise "any error" + expect (fn ("object", Foo)).not_to_raise "any error" + expect (fn ("object", Foo {})).not_to_raise "any error" + - it matches anything: + expect (fn ("any", true)).not_to_raise "any error" + expect (fn ("any", {})).not_to_raise "any error" + expect (fn ("any", Object)).not_to_raise "any error" + expect (fn ("any", Foo {})).not_to_raise "any error" + - context with a list of valid types: + - it diagnoses missing elements: + expect (fn ("string|table", nil)). + to_raise "string or table expected, got no value" + expect (fn ("string|list|#table", nil)). + to_raise "string, list or non-empty table expected, got no value" + expect (fn ("string|number|list|object", nil)). + to_raise "string, number, list or object expected, got no value" + - it diagnoses mismatched elements: + expect (fn ("string|table", false)). + to_raise "string or table expected, got boolean" + expect (fn ("string|#table", {})). + to_raise "string or non-empty table expected, got empty table" + expect (fn ("string|number|#list|object", {})). + to_raise "string, number, non-empty list or object expected, got empty table" + - it matches any type from a list: + expect (fn ("string|table", "foo")).not_to_raise "any error" + expect (fn ("string|table", {})).not_to_raise "any error" + expect (fn ("string|table", {0})).not_to_raise "any error" + expect (fn ("table|table", {})).not_to_raise "any error" + expect (fn ("#table|table", {})).not_to_raise "any error" + - context with an optional type element: + - it diagnoses mismatched elements: + expect (fn ("?boolean", "string")). + to_raise "boolean or nil expected, got string" + expect (fn ("?boolean|:symbol", {})). + to_raise "boolean, :symbol or nil expected, got empty table" + - it matches nil against a single type: + expect (fn ("?any", nil)).not_to_raise "any error" + expect (fn ("?boolean", nil)).not_to_raise "any error" + expect (fn ("?string", nil)).not_to_raise "any error" + - it matches nil against a list of types: + expect (fn ("?boolean|table", nil)).not_to_raise "any error" + expect (fn ("?string|table", nil)).not_to_raise "any error" + expect (fn ("?table|#table", nil)).not_to_raise "any error" + expect (fn ("?#table|table", nil)).not_to_raise "any error" + - it matches nil against a list of optional types: + expect (fn ("?boolean|?table", nil)).not_to_raise "any error" + expect (fn ("?string|?table", nil)).not_to_raise "any error" + expect (fn ("?table|?#table", nil)).not_to_raise "any error" + expect (fn ("?#table|?table", nil)).not_to_raise "any error" + - it matches any named type: + expect (fn ("?any", false)).not_to_raise "any error" + expect (fn ("?boolean", false)).not_to_raise "any error" + expect (fn ("?string", "string")).not_to_raise "any error" + - it matches any type from a list: + expect (fn ("?boolean|table", {})).not_to_raise "any error" + expect (fn ("?string|table", {0})).not_to_raise "any error" + expect (fn ("?table|#table", {})).not_to_raise "any error" + expect (fn ("?#table|table", {})).not_to_raise "any error" + - it matches any type from a list with several optional specifiers: + expect (fn ("?boolean|?table", {})).not_to_raise "any error" + expect (fn ("?string|?table", {0})).not_to_raise "any error" + expect (fn ("?table|?table", {})).not_to_raise "any error" + expect (fn ("?#table|?table", {})).not_to_raise "any error" + + +- describe debug: + - before: | + function mkwrap (k, v) + local fmt = "%s" + if type (v) == "string" then fmt = "%q" end + return k, string.format (fmt, require "std".tostring (v)) + end + + function mkdebug (debugp, ...) + return string.format ([[ + _DEBUG = %s + require "std.debug" (%s) + ]], + require "std".tostring (debugp), + table.concat (require "std.functional".map (mkwrap, {...}), ", ")) + end + + - it does nothing when _DEBUG is disabled: + expect (luaproc (mkdebug (false, "nothing to see here"))). + not_to_contain_error "nothing to see here" + - it writes to stderr when _DEBUG is not set: + expect (luaproc (mkdebug (nil, "debugging"))). + to_contain_error "debugging" + - it writes to stderr when _DEBUG is enabled: + expect (luaproc (mkdebug (true, "debugging"))). + to_contain_error "debugging" + - it writes to stderr when _DEBUG.level is not set: + expect (luaproc (mkdebug ({}, "debugging"))). + to_contain_error "debugging" + - it writes to stderr when _DEBUG.level is specified: + expect (luaproc (mkdebug ({level = 0}, "debugging"))). + to_contain_error "debugging" + expect (luaproc (mkdebug ({level = 1}, "debugging"))). + to_contain_error "debugging" + expect (luaproc (mkdebug ({level = 2}, "debugging"))). + to_contain_error "debugging" + + +- describe argscheck: + - before: | + function mkstack (name, spec) + return string.format ([[ + local argscheck = require "std.debug".argscheck -- line 1 + local function caller () -- line 2 + argscheck ("%s", function () end) -- line 3 + end -- line 4 + caller () -- line 5 + ]], tostring (name), tostring (spec)) + end + + f = M.argscheck + + mkmagic = function () return "MAGIC" end + wrapped = f ("inner ()", mkmagic) + + _, badarg, badresult = init (M, "", "inner") + id = function (...) return unpack {...} end + + - it returns the wrapped function: + expect (wrapped).not_to_be (inner) + expect (wrapped ()).to_be "MAGIC" + - it does not wrap the function when _ARGCHECK is disabled: | + script = [[ + _DEBUG = false + local debug = require "std.debug_init" + local argscheck = require "std.debug".argscheck + local function inner () return "MAGIC" end + local wrapped = argscheck ("inner (?any)", inner) + os.exit (wrapped == inner and 0 or 1) + ]] + expect (luaproc (script)).to_succeed () + + - context when checking zero argument function: + - it diagnoses too many arguments: + expect (wrapped (false)).to_raise (badarg (1)) + - it accepts correct argument types: + expect (wrapped ()).to_be "MAGIC" + + - context when checking single argument function: + - before: + wrapped = f ("inner (#table)", mkmagic) + - it diagnoses missing arguments: + expect (wrapped ()).to_raise (badarg (1, "non-empty table")) + - it diagnoses wrong argument types: + expect (wrapped {}).to_raise (badarg (1, "non-empty table", "empty table")) + - it diagnoses too many arguments: + expect (wrapped ({1}, 2, nop, "", false)).to_raise (badarg (1, 5)) + - it accepts correct argument types: + expect (wrapped ({1})).to_be "MAGIC" + + - context when checking multi-argument function: + - before: + wrapped = f ("inner (table, function)", mkmagic) + - it diagnoses missing arguments: + expect (wrapped ()).to_raise (badarg (1, "table")) + expect (wrapped ({})).to_raise (badarg (2, "function")) + - it diagnoses wrong argument types: + expect (wrapped (false)).to_raise (badarg (1, "table", "boolean")) + expect (wrapped ({}, false)).to_raise (badarg (2, "function", "boolean")) + - it diagnoses too many arguments: + expect (wrapped ({}, nop, false)).to_raise (badarg (3)) + - it accepts correct argument types: + expect (wrapped ({}, nop)).to_be "MAGIC" + + - context when checking nil argument function: + - before: + wrapped = f ("inner (?int, string)", mkmagic) + - it diagnoses wrong argument types: + expect (wrapped (false)).to_raise (badarg (1, "int or nil", "boolean")) + expect (wrapped (1, false)).to_raise (badarg (2, "string", "boolean")) + expect (wrapped (nil, false)).to_raise (badarg (2, "string", "boolean")) + - it diagnoses too many arguments: + expect (wrapped (1, "foo", nop)).to_raise (badarg (3)) + expect (wrapped (nil, "foo", nop)).to_raise (badarg (3)) + - it accepts correct argument types: + expect (wrapped (1, "foo")).to_be "MAGIC" + expect (wrapped (nil, "foo")).to_be "MAGIC" + + - context when checking optional multi-argument function: + - before: + wrapped = f ("inner ([int], string)", mkmagic) + - it diagnoses missing arguments: + expect (wrapped ()).to_raise (badarg (1, "int or string")) + expect (wrapped (1)).to_raise (badarg (2, "string")) + - it diagnoses wrong argument types: + expect (wrapped (false)).to_raise (badarg (1, "int or string", "boolean")) + - it diagnoses too many arguments: + expect (wrapped (1, "two", nop)).to_raise (badarg (3)) + - it accepts correct argument types: + expect (wrapped ("two")).to_be "MAGIC" + expect (wrapped (1, "two")).to_be "MAGIC" + + - context when checking final optional multi-argument function: + - before: + wrapped = f ("inner (?any, ?string, [any])", mkmagic) + - it diagnoses wrong argument types: + expect (wrapped (1, false)).to_raise (badarg (2, "string or nil", "boolean")) + expect (wrapped (nil, false)).to_raise (badarg (2, "string or nil", "boolean")) + - it diagnoses too many arguments: + expect (wrapped (1, "two", 3, false)).to_raise (badarg (4)) + expect (wrapped (nil, "two", 3, false)).to_raise (badarg (4)) + expect (wrapped (1, nil, 3, false)).to_raise (badarg (4)) + expect (wrapped (nil, nil, 3, false)).to_raise (badarg (4)) + - it accepts correct argument types: + expect (wrapped ()).to_be "MAGIC" + expect (wrapped (1)).to_be "MAGIC" + expect (wrapped (nil, "two")).to_be "MAGIC" + expect (wrapped (1, "two")).to_be "MAGIC" + expect (wrapped (nil, nil, 3)).to_be "MAGIC" + expect (wrapped (1, nil, 3)).to_be "MAGIC" + expect (wrapped (nil, "two", 3)).to_be "MAGIC" + expect (wrapped ("one", "two", 3)).to_be "MAGIC" + + - context when checking final ellipsis function: + - before: + wrapped = f ("inner (string, int...)", mkmagic) + - it diagnoses missing arguments: + expect (wrapped ()).to_raise (badarg (1, "string")) + expect (wrapped ("foo")).to_raise (badarg (2, "int")) + - it diagnoses wrong argument types: + expect (wrapped (false)).to_raise (badarg (1, "string", "boolean")) + expect (wrapped ("foo", false)).to_raise (badarg (2, "int", "boolean")) + expect (wrapped ("foo", 1, false)).to_raise (badarg (3, "int", "boolean")) + expect (wrapped ("foo", 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, false)). + to_raise (badarg (12, "int", "boolean")) + - it accepts correct argument types: + expect (wrapped ("foo", 1)).to_be "MAGIC" + expect (wrapped ("foo", 1, 2)).to_be "MAGIC" + expect (wrapped ("foo", 1, 2, 5)).to_be "MAGIC" + + - context when checking optional final parameter: + - context with single argument: + - before: + wrapped = f ("inner ([int])", mkmagic) + - it diagnoses wrong argument types: + expect (wrapped (false)).to_raise (badarg (1, "int", "boolean")) + - it diagnoses too many arguments: + expect (wrapped (1, nop)).to_raise (badarg (2)) + - it accepts correct argument types: + expect (wrapped ()).to_be "MAGIC" + expect (wrapped (1)).to_be "MAGIC" + - context with trailing ellipsis: + - before: + wrapped = f ("inner (string, [int]...)", mkmagic) + - it diagnoses missing arguments: + expect (wrapped ()).to_raise (badarg (1, "string")) + - it diagnoses wrong argument types: + expect (wrapped (false)).to_raise (badarg (1, "string", "boolean")) + expect (wrapped ("foo", false)).to_raise (badarg (2, "int", "boolean")) + expect (wrapped ("foo", 1, false)).to_raise (badarg (3, "int", "boolean")) + expect (wrapped ("foo", 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, false)). + to_raise (badarg (12, "int", "boolean")) + - it accepts correct argument types: + expect (wrapped ("foo")).to_be "MAGIC" + expect (wrapped ("foo", 1)).to_be "MAGIC" + expect (wrapped ("foo", 1, 2)).to_be "MAGIC" + expect (wrapped ("foo", 1, 2, 5)).to_be "MAGIC" + - context with inner ellipsis: + - before: + wrapped = f ("inner (string, [int...])", mkmagic) + - it diagnoses missing arguments: + expect (wrapped ()).to_raise (badarg (1, "string")) + - it diagnoses wrong argument types: + expect (wrapped (false)).to_raise (badarg (1, "string", "boolean")) + expect (wrapped ("foo", false)).to_raise (badarg (2, "int", "boolean")) + expect (wrapped ("foo", 1, false)).to_raise (badarg (3, "int", "boolean")) + expect (wrapped ("foo", 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, false)). + to_raise (badarg (12, "int", "boolean")) + - it accepts correct argument types: + expect (wrapped ("foo")).to_be "MAGIC" + expect (wrapped ("foo", 1)).to_be "MAGIC" + expect (wrapped ("foo", 1, 2)).to_be "MAGIC" + expect (wrapped ("foo", 1, 2, 5)).to_be "MAGIC" + + - context when omitting self type: + - before: + me = { + wrapped = f ("me:inner (string)", mkmagic) + } + _, badarg, badresult = init (M, "", "me:inner") + - it diagnoses missing arguments: + expect (me:wrapped ()).to_raise (badarg (1, "string")) + - it diagnoses wrong argument types: + expect (me:wrapped (false)).to_raise (badarg (1, "string", "boolean")) + - it diagnoses too many arguments: + expect (me:wrapped ("foo", false)).to_raise (badarg (2)) + - it accepts correct argument types: + expect (me:wrapped ("foo")).to_be "MAGIC" + + - context with too many args: + - before: + wrapped = f ("inner ([string], int)", mkmagic) + - it diagnoses missing arguments: + expect (wrapped ()).to_raise (badarg (1, "string or int")) + expect (wrapped ("one")).to_raise (badarg (2, "int")) + - it diagnoses wrong argument types: + expect (wrapped (false)).to_raise (badarg (1, "string or int", "boolean")) + expect (wrapped ("one", false)).to_raise (badarg (2, "int", "boolean")) + - it diagnoses too many arguments: + expect (wrapped ("one", 2, false)).to_raise (badarg (3)) + expect (wrapped (1, false)).to_raise (badarg (2)) + - it accepts correct argument types: + expect (wrapped (1)).to_be "MAGIC" + expect (wrapped ("one", 2)).to_be "MAGIC" + + - context when checking single return value function: + - before: | + wrapped = f ("inner (?any...) => #table", id) + - it diagnoses missing results: + expect (wrapped ()).to_raise (badresult (1, "non-empty table")) + - it diagnoses wrong result types: + expect (wrapped {}). + to_raise (badresult (1, "non-empty table", "empty table")) + - it diagnoses too many results: + expect (wrapped ({1}, 2, nop, "", false)).to_raise (badresult (1, 5)) + - it accepts correct results: + expect ({wrapped {1}}).to_equal {{1}} + + - context with variant single return value function: + - before: + wrapped = f ("inner (?any...) => int or nil", id) + - it diagnoses wrong result types: + expect (wrapped (false)).to_raise (badresult (1, "int or nil", "boolean")) + - it diagnoses too many results: + expect (wrapped (1, nop)).to_raise (badresult (2)) + - it accepts correct result types: + expect ({wrapped ()}).to_equal {} + expect ({wrapped (1)}).to_equal {1} + + - context when checking multi-return value function: + - before: + wrapped = f ("inner (?any...) => int, string", id) + - it diagnoses missing results: + expect (wrapped ()).to_raise (badresult (1, "int")) + expect (wrapped (1)).to_raise (badresult (2, "string")) + - it diagnoses wrong result types: + expect (wrapped (false)).to_raise (badresult (1, "int", "boolean")) + expect (wrapped (1, false)).to_raise (badresult (2, "string", "boolean")) + - it diagnoses too many results: + expect (wrapped (1, "two", false)).to_raise (badresult (3)) + - it accepts correct argument types: + expect ({wrapped (1, "two")}).to_equal {1, "two"} + + - context when checking nil return specifier: + - before: + wrapped = f ("inner (?any...) => ?int, string", id) + - it diagnoses wrong result types: + expect (wrapped (false)).to_raise (badresult (1, "int or nil", "boolean")) + expect (wrapped (1, false)).to_raise (badresult (2, "string", "boolean")) + expect (wrapped (nil, false)).to_raise (badresult (2, "string", "boolean")) + - it diagnoses too many results: + expect (wrapped (1, "foo", nop)).to_raise (badresult (3)) + expect (wrapped (nil, "foo", nop)).to_raise (badresult (3)) + - it accepts correct result types: + expect ({wrapped (1, "foo")}).to_equal {1, "foo"} + expect ({wrapped (nil, "foo")}).to_equal {[2] = "foo"} + + - context when checking variant multi-return value function: + - before: + wrapped = f ("inner (?any...) => int, string or string", id) + - it diagnoses missing results: + expect (wrapped ()).to_raise (badresult (1, "int or string")) + expect (wrapped (1)).to_raise (badresult (2, "string")) + - it diagnoses wrong result types: + expect (wrapped (false)).to_raise (badresult (1, "int or string", "boolean")) + - it diagnoses too many results: + expect (wrapped (1, "two", nop)).to_raise (badresult (3)) + - it accepts correct result types: + expect ({wrapped ("two")}).to_equal {"two"} + expect ({wrapped (1, "two")}).to_equal {1, "two"} + + - context when checking variant nil,errmsg pattern function: + - before: + wrapped = f ("inner (?any...) => int, string or nil, string", id) + - it diagnoses missing results: + expect (wrapped ()).to_raise (badresult (2, "string")) + expect (wrapped (1)).to_raise (badresult (2, "string")) + - it diagnoses wrong result types: + expect (wrapped (false)).to_raise (badresult (1, "int or nil", "boolean")) + expect (wrapped (1, false)).to_raise (badresult (2, "string", "boolean")) + - it diagnoses too many results: + expect (wrapped (1, "two", nop)).to_raise (badresult (3)) + expect (wrapped (nil, "errmsg", nop)).to_raise (badresult (3)) + - it accepts correct result types: + expect ({wrapped (1, "two")}).to_equal {1, "two"} + expect ({wrapped (nil, "errmsg")}).to_equal {[2] = "errmsg"} + + - context when checking optional multi-return value function: + - before: + wrapped = f ("inner (?any...) => [int], string", id) + - it diagnoses missing results: + expect (wrapped ()).to_raise (badresult (1, "int or string")) + expect (wrapped (1)).to_raise (badresult (2, "string")) + - it diagnoses wrong result types: + expect (wrapped (false)).to_raise (badresult (1, "int or string", "boolean")) + - it diagnoses too many results: + expect (wrapped (1, "two", nop)).to_raise (badresult (3)) + - it accepts correct result types: + expect ({wrapped ("two")}).to_equal {"two"} + expect ({wrapped (1, "two")}).to_equal {1, "two"} + + - context when checking final optional multi-return value function: + - before: + wrapped = f ("inner (?any...) => ?any, ?string, [any]", id) + - it diagnoses wrong result types: + expect (wrapped (1, false)).to_raise (badresult (2, "string or nil", "boolean")) + expect (wrapped (nil, false)).to_raise (badresult (2, "string or nil", "boolean")) + - it diagnoses too many results: + expect (wrapped (1, "two", 3, false)).to_raise (badresult (4)) + expect (wrapped (nil, "two", 3, false)).to_raise (badresult (4)) + expect (wrapped (1, nil, 3, false)).to_raise (badresult (4)) + expect (wrapped (nil, nil, 3, false)).to_raise (badresult (4)) + - it accepts correct result types: + expect ({wrapped ()}).to_equal {} + expect ({wrapped (1)}).to_equal {1} + expect ({wrapped (nil, "two")}).to_equal {[2]="two"} + expect ({wrapped (1, "two")}).to_equal {1, "two"} + expect ({wrapped (nil, nil, 3)}).to_equal {[3]=3} + expect ({wrapped (1, nil, 3)}).to_equal {1, [3]=3} + expect ({wrapped (nil, "two", 3)}).to_equal {[2]="two", [3]=3} + expect ({wrapped ("one", "two", 3)}).to_equal {"one", "two", 3} + + - context when checking optional final result: + - context with single result: + - before: + wrapped = f ("inner (?any...) => [int]", id) + - it diagnoses wrong result types: + expect (wrapped (false)).to_raise (badresult (1, "int", "boolean")) + - it diagnoses too many results: + expect (wrapped (1, nop)).to_raise (badresult (2)) + - it accepts correct result types: + expect ({wrapped ()}).to_equal {} + expect ({wrapped (1)}).to_equal {1} + - context with trailing ellipsis: + - before: + wrapped = f ("inner (?any...) => string, [int]...", id) + - it diagnoses missing results: + expect (wrapped ()).to_raise (badresult (1, "string")) + - it diagnoses wrong result types: + expect (wrapped (false)).to_raise (badresult (1, "string", "boolean")) + expect (wrapped ("foo", false)).to_raise (badresult (2, "int", "boolean")) + expect (wrapped ("foo", 1, false)).to_raise (badresult (3, "int", "boolean")) + expect (wrapped ("foo", 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, false)). + to_raise (badresult (12, "int", "boolean")) + - it accepts correct result types: + expect ({wrapped ("foo")}).to_equal {"foo"} + expect ({wrapped ("foo", 1)}).to_equal {"foo", 1} + expect ({wrapped ("foo", 1, 2)}).to_equal {"foo", 1, 2} + expect ({wrapped ("foo", 1, 2, 5)}).to_equal {"foo", 1, 2, 5} + - context with inner ellipsis: + - before: + wrapped = f ("inner (?any...) => string, [int...]", id) + - it diagnoses missing results: + expect (wrapped ()).to_raise (badresult (1, "string")) + - it diagnoses wrong result types: + expect (wrapped (false)).to_raise (badresult (1, "string", "boolean")) + expect (wrapped ("foo", false)).to_raise (badresult (2, "int", "boolean")) + expect (wrapped ("foo", 1, false)).to_raise (badresult (3, "int", "boolean")) + expect (wrapped ("foo", 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, false)). + to_raise (badresult (12, "int", "boolean")) + - it accepts correct result types: + expect ({wrapped ("foo")}).to_equal {"foo"} + expect ({wrapped ("foo", 1)}).to_equal {"foo", 1} + expect ({wrapped ("foo", 1, 2)}).to_equal {"foo", 1, 2} + expect ({wrapped ("foo", 1, 2, 5)}).to_equal {"foo", 1, 2, 5} + + - context with too many results: + - before: + wrapped = f ("inner (?any...) => [string], int", id) + - it diagnoses missing results: + expect (wrapped ()).to_raise (badresult (1, "string or int")) + expect (wrapped "one").to_raise (badresult (2, "int")) + - it diagnoses wrong result types: + expect (wrapped (false)). + to_raise (badresult (1, "string or int", "boolean")) + expect (wrapped ("one", false)). + to_raise (badresult (2, "int", "boolean")) + - it diagnoses too many results: + expect (wrapped ("one", 2, false)).to_raise (badresult (3)) + expect (wrapped (1, false)).to_raise (badresult (2)) + - it accepts correct argument types: + expect ({wrapped (1)}).to_equal {1} + expect ({wrapped ("one", 2)}).to_equal {"one", 2} + + +- describe extramsg_mismatch: + - before: + f = M.extramsg_mismatch + + - it returns the expected types: + expect (f "nil").to_contain "nil expected, " + expect (f "bool").to_contain "boolean expected, " + expect (f "?bool").to_contain "boolean or nil expected, " + expect (f "string|table").to_contain "string or table expected, " + - it returns expected container types: + expect (f ("table of int", nil, 1)).to_contain "table of ints expected, " + expect (f ("table of int|bool", nil, 1)). + to_contain "table of ints or booleans expected, " + expect (f ("table of int|bool|string", nil, 1)). + to_contain "table of ints, booleans or strings expected, " + expect (f ("table of int|bool|string|table", nil, 1)). + to_contain "table of ints, booleans, strings or tables expected, " + - it returns the actual type: + expect (f ("int")).to_contain ", got no value" + expect (f ("int", false)).to_contain ", got boolean" + expect (f ("int", {})).to_contain ", got empty table" + - it returns table field type: + expect (f ("table of int", nil, 1)).to_contain ", got no value at index 1" + expect (f ("table of int", "two", 2)).to_contain ", got string at index 2" + expect (f ("table of int|bool", "five", 3)).to_contain ", got string at index 3" + + +- describe extramsg_toomany: + - before: + f = M.extramsg_toomany + + - it returns the expected thing: + expect (f ("mojo", 1, 2)).to_contain "no more than 1 mojo" + - it uses singular thing when 1 is expected: + expect (f ("argument", 1, 2)).to_contain "no more than 1 argument" + - it uses plural thing otherwise: + expect (f ("thing", 0, 3)).to_contain "no more than 0 things" + expect (f ("result", 2, 3)).to_contain "no more than 2 results" + - it returns the actual count: + expect (f ("bad", 0, 1)).to_contain ", got 1" + expect (f ("bad", 99, 999)).to_contain ", got 999" + + +- context function environments: + - before: + env = { + tostring = function (x) return '"' .. tostring (x) .. '"' end, + } + fn = function (x) return tostring (x) end + ft = setmetatable ({}, { __call = function (_, ...) return fn (...) end }) + + - describe getfenv: + - before: + f = M.getfenv + - it returns a table: + expect (type (f (fn))).to_be "table" + - it gets a function execution environment: + M.setfenv (fn, env) + expect (f (fn)).to_be (env) + - it understands functables: + M.setfenv (ft, env) + expect (f (ft)).to_be (env) + + - describe setfenv: + - before: + f = M.setfenv + - it returns the passed function: + expect (f (fn, env)).to_be (fn) + - it sets a function execution environment: + f (fn, env) + expect (fn (42)).to_be '"42"' + - it understands functables: + f (ft, env) + expect (fn (5)).to_be '"5"' + + +- describe say: + - before: | + function mkwrap (k, v) + local fmt = "%s" + if type (v) == "string" then fmt = "%q" end + return k, string.format (fmt, require "std".tostring (v)) + end + + function mksay (debugp, ...) + return string.format ([[ + _DEBUG = %s + require "std.debug".say (%s) + ]], + require "std".tostring (debugp), + table.concat (require "std.functional".map (mkwrap, {...}), ", ")) + end + + f = M.say + + - it uses stdlib tostring: + expect (luaproc [[require "std.debug".say {"debugging"}]]). + to_contain_error (require "std".tostring {"debugging"}) + - context when _DEBUG is disabled: + - it does nothing when message level is not set: + expect (luaproc (mksay (false, "nothing to see here"))). + not_to_contain_error "nothing to see here" + - it does nothing when message is set: + expect (luaproc (mksay (false, -999, "nothing to see here"))). + not_to_contain_error "nothing to see here" + expect (luaproc (mksay (false, 0, "nothing to see here"))). + not_to_contain_error "nothing to see here" + expect (luaproc (mksay (false, 1, "nothing to see here"))). + not_to_contain_error "nothing to see here" + expect (luaproc (mksay (false, 2, "nothing to see here"))). + not_to_contain_error "nothing to see here" + expect (luaproc (mksay (false, 999, "nothing to see here"))). + not_to_contain_error "nothing to see here" + - context when _DEBUG is not set: + - it writes to stderr when message level is not set: + expect (luaproc (mksay (nil, "debugging"))). + to_contain_error "debugging" + - it writes to stderr when message level is 1 or lower: + expect (luaproc (mksay (nil, -999, "debugging"))). + to_contain_error "debugging" + expect (luaproc (mksay (nil, 0, "debugging"))). + to_contain_error "debugging" + expect (luaproc (mksay (nil, 1, "debugging"))). + to_contain_error "debugging" + - it does nothing when message level is 2 or higher: + expect (luaproc (mksay (nil, 2, "nothing to see here"))). + not_to_contain_error "nothing to see here" + expect (luaproc (mksay (nil, 999, "nothing to see here"))). + not_to_contain_error "nothing to see here" + - context when _DEBUG is enabled: + - it writes to stderr when message level is not set: + expect (luaproc (mksay (true, "debugging"))). + to_contain_error "debugging" + - it writes to stderr when message level is 1 or lower: + expect (luaproc (mksay (true, -999, "debugging"))). + to_contain_error "debugging" + expect (luaproc (mksay (true, 0, "debugging"))). + to_contain_error "debugging" + expect (luaproc (mksay (true, 1, "debugging"))). + to_contain_error "debugging" + - it does nothing when message level is 2 or higher: + expect (luaproc (mksay (true, 2, "nothing to see here"))). + not_to_contain_error "nothing to see here" + expect (luaproc (mksay (true, 999, "nothing to see here"))). + not_to_contain_error "nothing to see here" + - context when _DEBUG.level is not set: + - it writes to stderr when message level is not set: + expect (luaproc (mksay ({}, "debugging"))). + to_contain_error "debugging" + - it writes to stderr when message level is 1 or lower: + expect (luaproc (mksay ({}, -999, "debugging"))). + to_contain_error "debugging" + expect (luaproc (mksay ({}, 0, "debugging"))). + to_contain_error "debugging" + expect (luaproc (mksay ({}, 1, "debugging"))). + to_contain_error "debugging" + - it does nothing when message level is 2 or higher: + expect (luaproc (mksay ({}, 2, "nothing to see here"))). + not_to_contain_error "nothing to see here" + expect (luaproc (mksay ({}, 999, "nothing to see here"))). + not_to_contain_error "nothing to see here" + - context when _DEBUG.level is specified: + - it writes to stderr when message level is 1 or lower: + expect (luaproc (mksay ({level = 0}, "debugging"))). + to_contain_error "debugging" + expect (luaproc (mksay ({level = 1}, "debugging"))). + to_contain_error "debugging" + expect (luaproc (mksay ({level = 2}, "debugging"))). + to_contain_error "debugging" + - it does nothing when message level is higher than debug level: + expect (luaproc (mksay ({level = 2}, 3, "nothing to see here"))). + not_to_contain_error "nothing to see here" + - it writes to stderr when message level equals debug level: + expect (luaproc (mksay ({level = 2}, 2, "debugging"))). + to_contain_error "debugging" + - it writes to stderr when message level is lower than debug level: + expect (luaproc (mksay ({level = 2}, 1, "debugging"))). + to_contain_error "debugging" + + +- describe trace: + - before: + f = init (M, this_module, "trace") + + - it does nothing when _DEBUG is disabled: + expect (luaproc [[ + _DEBUG = false + require "std.debug" + os.exit (0) + ]]).to_succeed_with "" + - it does nothing when _DEBUG is not set: + expect (luaproc [[ + require "std.debug" + os.exit (0) + ]]).to_succeed_with "" + - it does nothing when _DEBUG is enabled: + expect (luaproc [[ + _DEBUG = true + require "std.debug" + os.exit (0) + ]]).to_succeed_with "" + - it enables automatically when _DEBUG.call is set: | + expect (luaproc [[ + _DEBUG = {call = true} + local debug = require "std.debug" + os.exit (1) + ]]).to_fail_while_containing ":3 call exit" + - it is enabled manually with debug.sethook: | + expect (luaproc [[ + local debug = require "std.debug" + debug.sethook (debug.trace, "cr") + os.exit (1) + ]]).to_fail_while_containing ":3 call exit" + - it writes call trace log to standard error: | + expect (luaproc [[ + local debug = require "std.debug" + debug.sethook (debug.trace, "cr") + os.exit (0) + ]]).to_contain_error ":3 call exit" + - it traces lua calls: | + expect (luaproc [[ + local debug = require "std.debug" -- line 1 + local function incr (i) return i + 1 end -- line 2 + debug.sethook (debug.trace, "cr") -- line 3 + os.exit (incr (41)) -- line 4 + ]]).to_fail_while_matching ".*:4 call incr <2:.*:4 return incr <2:.*" + - it traces C api calls: | + expect (luaproc [[ + local debug = require "std.debug" + local function incr (i) return i + 1 end + debug.sethook (debug.trace, "cr") + os.exit (incr (41)) + ]]).to_fail_while_matching ".*:4 call exit %[C%]%s$" diff --git a/spec/functional_spec.yaml b/spec/functional_spec.yaml new file mode 100644 index 0000000..619c251 --- /dev/null +++ b/spec/functional_spec.yaml @@ -0,0 +1,764 @@ +before: + base = require "std.base" + + this_module = "std.functional" + global_table = "_G" + + exported_apis = { "bind", "callable", "case", "collect", "compose", + "cond", "curry", "eval", "filter", "fold", "foldl", + "foldr", "id", "lambda", "map", "map_with", + "memoize", "nop", "op", "reduce", "zip", "zip_with" } + + M = require (this_module) + + +specify std.functional: +- context when required: + - context by name: + - it does not touch the global table: + expect (show_apis {added_to=global_table, by=this_module}). + to_equal {} + - it exports the documented apis: + t = {} + for k in pairs (M) do t[#t + 1] = k end + expect (t).to_contain.a_permutation_of (exported_apis) + + - context via the std module: + - it does not touch the global table: + expect (show_apis {added_to=global_table, by="std"}). + to_equal {} + + +- describe bind: + - before: + op = require "std.operator" + + f = M.bind + + - it writes an argument passing deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {nop, M, "bind"})). + to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {nop, M, "bind"})). + not_to_contain_error "was deprecated" + + - context with bad arguments: + badargs.diagnose (f, "std.functional.bind (function, ?any*)") + + - it does not affect normal operation if no arguments are bound: + expect (f (math.min, {}) (2, 3, 4)).to_be (2) + - it takes the extra arguments into account: + expect (f (math.min, {1, 0}) (2, 3, 4)).to_be (0) + - it appends final call arguments: + expect (f (math.max, {2, 3}) (4, 5, 1)).to_be (5) + - it does not require all arguments in final call: + div = function (a, b) return a / b end + expect (f (div, {100}) (25)).to_be (4) + - it supports out of order extra arguments: + expect (f (op.pow, {[2] = 3}) (2)).to_be (8) + - it propagates nil arguments correctly: + expect ({f (M.id, {[2]="b", [4]="d"}) (nil, 3, 5, 6, nil)}). + to_equal {nil, "b", 3, "d", 5, 6, nil} + - it supports the legacy api: + expect (f (math.min) (2, 3, 4)).to_be (2) + expect (f (math.min, 1, 0) (2, 3, 4)).to_be (0) + expect (f (op.pow, nil, 3) (2)).to_be (8) + + +- describe callable: + - before: + f = M.callable + + - context with bad arguments: + badargs.diagnose (f, "std.functional.callable (?any)") + + - it returns the function associated with a callable: + Container = require "std.container" { __call = M.nop } + for _, v in ipairs { + true, + 42, + "str", + io.stderr, + {}, + M.nop, + setmetatable ({}, {__call = M.nop}), + Container, + } do + expect (f (v)).to_be (pcall (v, {}) and M.nop or nil) + end + - it returns 'nil' for uncallable arguments: + expect (f ()).to_be (nil) + expect (f {}).to_be (nil) + expect (f "").to_be (nil) + +- describe case: + - before: + yes = function () return true end + no = function () return false end + default = function (s) return s end + branches = { yes = yes, no = no, default } + + f = M.case + + - context with bad arguments: | + badargs.diagnose (f, "std.functional.case (?any, #table)") + + - it matches against branch keys: + expect (f ("yes", branches)).to_be (true) + expect (f ("no", branches)).to_be (false) + - it has a default for unmatched keys: + expect (f ("none", branches)).to_be "none" + - it returns nil for unmatched keys with no default: + expect (f ("none", { yes = yes, no = no })).to_be (nil) + - it returns non-function matches: + expect (f ("t", {t = true})).to_be (true) + - it evaluates returned functions: + expect (f ("fn", {fn = function () return true end})). + to_be (true) + - it passes 'with' to function matches: + expect (f ("with", {function (s) return s end})).to_be "with" + - it evaluates returned functables: + functable = setmetatable ({}, {__call = function (t, with) return with end}) + expect (f ("functable", {functable})).to_be "functable" + - it evaluates 'with' exactly once: + s = "prince" + function acc () s = s .. "s"; return s end + expect (f (acc (), { + prince = function () return "one" end, + princes = function () return "many" end, + princess = function () return "one" end, + function () return "gibberish" end, + })).to_be "many" + + +- describe collect: + - before: + f = M.collect + + - context with bad arguments: + badargs.diagnose (f, "std.functional.collect ([func], any*)") + + - it collects a list of single return value iterator results: + expect (f (base.ielems, {"a", "b", "c"})).to_equal {"a", "b", "c"} + - it collects a table of key:value iterator results: + t = {"first", second="two", last=3} + expect (f (pairs, t)).to_equal (t) + - it propagates nil arguments correctly: + expect (f {"a", nil, nil, "d", "e"}).to_equal {"a", [4]="d", [5]="e"} + - it defaults to npairs iteration: + expect (f {1, 2, [5]=5, a="b", c="d"}).to_equal {1, 2, [5]=5} + + +- describe compose: + - before: + f = M.compose + + - context with bad arguments: + badargs.diagnose (f, "std.functional.compose (func*)") + + - it composes a single function correctly: + expect (f (M.id) (1)).to_be (1) + - it propagates nil arguments correctly: + expect ({f (M.id) (1, nil, nil, 4)}).to_equal {1, nil, nil, 4} + expect ({f (M.id, M.id) (1, nil, nil, 4)}).to_equal {1, nil, nil, 4} + - it composes functions in the correct order: + expect (f (math.sin, math.cos) (1)). + to_be (math.cos (math.sin (1))) + + +- describe cond: + - before: + yes = function () return true end + no = function () return false end + default = function (s) return s end + branches = { yes = yes, no = no, default } + + f = M.cond + + - it returns nil for no arguments: + expect (f ()).to_be (nil) + - it evaluates a single function argument: + expect (f (function () return true end)).to_be (true) + - it evaluates a single functable argument: + functable = setmetatable ({}, {__call = function () return true end}) + expect (f (functable)).to_be (true) + - it returns a non-callable single argument directly: + expect (f "foo").to_be "foo" + - it evaluates a branch function if expr is truthy: + expect (f ("truthy", function (s) return s end)).to_be "truthy" + - it returns nil if the last expr is falsey: + expect (f (nil, function (s) return "falsey" end)).to_be (nil) + expect (f (false, true, false, true)).to_be (nil) + - it recurses with remaining arguments if first argument is falsey: + expect (f (nil, true, 42, M.id)).to_be (42) + expect (f (nil, true, false, false, 42, M.id)).to_be (42) + + +- describe curry: + - before: + op = require "std.operator" + + f = M.curry + + - context with bad arguments: + badargs.diagnose (f, "std.functional.curry (func, int)") + + - it returns a zero argument function uncurried: + expect (f (f, 0)).to_be (f) + - it returns a one argument function uncurried: + expect (f (f, 1)).to_be (f) + - it curries a two argument function: + expect (f (f, 2)).not_to_be (f) + - it evaluates intermediate arguments one at a time: + expect (f (math.min, 3) (2) (3) (4)).to_equal (2) + - it returns a curried function that can be partially applied: + bin = f (op.pow, 2) (2) + expect (bin (2)).to_be (op.pow (2, 2)) + expect (bin (10)).to_be (op.pow (2, 10)) + + +- describe eval: + - before: + f = M.eval + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {"42"})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {"42"})).not_to_contain_error "was deprecated" + + - it diagnoses invalid lua: + # Some internal error when eval tries to call uncompilable "=" code. + expect (f "=").to_raise () + - it evaluates a string of lua code: + expect (f "math.min (2, 10)").to_be (math.min (2, 10)) + + +- describe filter: + - before: + elements = {"a", "b", "c", "d", "e"} + inverse = {a=1, b=2, c=3, d=4, e=5} + + f = M.filter + + - context with bad arguments: + badargs.diagnose (f, "std.functional.filter (func, [func], any*)") + + - it works with an empty table: + expect (f (M.id, pairs, {})).to_equal {} + - it iterates through element keys: + expect (f (M.id, base.ielems, elements)).to_equal {"a", "b", "c", "d", "e"} + expect (f (M.id, base.elems, inverse)).to_contain.a_permutation_of {1, 2, 3, 4, 5} + - it propagates nil arguments correctly: + t = {"a", nil, nil, "d", "e"} + expect (f (M.id, base.npairs, t)).to_equal (t) + - it passes all iteration result values to filter predicate: + t = {} + f (function (k, v) t[k] = v end, pairs, elements) + expect (t).to_equal (elements) + - it returns a list of filtered single return value iterator results: + expect (f (function (e) return e:match "[aeiou]" end, base.ielems, elements)). + to_equal {"a", "e"} + - it returns a table of filtered key:value iterator results: + t = {"first", second=2, last="three"} + expect (f (function (k, v) return type (v) == "string" end, pairs, t)). + to_equal {"first", last="three"} + expect (f (function (k, v) return k % 2 == 0 end, ipairs, elements)). + to_equal {[2]="b", [4]="d"} + - it defaults to pairs iteration: + t = {"first", second=2, last="three"} + expect (f (function (k, v) return type (v) == "string" end, t)). + to_equal {"first", last="three"} + + +- describe fold: + - before: + op = require "std.operator" + f = M.fold + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {M.id, 1, ipairs, {}})). + to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {M.id, 1, ipairs, {}})). + not_to_contain_error "was deprecated" + + - it works with an empty table: + expect (f (op.sum, 2, ipairs, {})).to_be (2) + - it calls a binary function over single return value iterator results: + expect (f (op.sum, 2, base.ielems, {3})). + to_be (2 + 3) + expect (f (op.prod, 2, base.ielems, {3, 4})). + to_be (2 * 3 * 4) + - it calls a binary function over key:value iterator results: + expect (f (op.sum, 2, ipairs, {3})).to_be (2 + 3) + expect (f (op.prod, 2, ipairs, {3, 4})).to_be (2 * 3 * 4) + - it folds elements from left to right: + expect (f (op.pow, 2, ipairs, {3, 4})).to_be ((2 ^ 3) ^ 4) + + +- describe foldl: + - before: + op = require "std.operator" + f = M.foldl + + - context with bad arguments: + badargs.diagnose (f, "std.functional.foldl (func, [any], table)") + + - it works with an empty table: + expect (f (op.sum, 10000, {})).to_be (10000) + - it folds a binary function through a table: + expect (f (op.sum, 10000, {1, 10, 100})).to_be (10111) + - it folds from left to right: + expect (f (op.pow, 2, {3, 4})).to_be ((2 ^ 3) ^ 4) + - it supports eliding init argument: + expect (f (op.pow, {2, 3, 4})).to_be ((2 ^ 3) ^ 4) + + +- describe foldr: + - before: + op = require "std.operator" + f = M.foldr + + - context with bad arguments: + badargs.diagnose (f, "std.functional.foldr (func, [any], table)") + + - it works with an empty table: + expect (f (op.sum, 1, {})).to_be (1) + - it folds a binary function through a table: + expect (f (op.sum, {10000, 100, 10, 1})).to_be (10111) + - it folds from right to left: + expect (f (op.quot, 10, {10000, 100})).to_be (10000 / (100 / 10)) + - it supports eliding init argument: + expect (f (op.quot, {10000, 100, 10})).to_be (10000 / (100 / 10)) + + +- describe id: + - before: + f = M.id + - it returns argument unchanged: + expect (f (true)).to_be (true) + expect (f {1, 1, 2, 3}).to_equal {1, 1, 2, 3} + - it returns multiple arguments unchanged: + expect ({f (1, "two", false)}).to_equal {1, "two", false} + + +- describe lambda: + - before: + f = M.lambda + + - context with bad arguments: + badargs.diagnose (f, "std.functional.lambda (string)") + + examples {["it diagnoses bad lambda string"] = function () + expect (select (2, f "foo")).to_be "invalid lambda string 'foo'" + end} + examples {["it diagnoses an uncompilable expression"] = function () + expect (select (2, f "||+")).to_be "invalid lambda string '||+'" + expect (select (2, f "=")).to_be "invalid lambda string '='" + end} + + - context with argument format: + - it returns a function: + expect (prototype (f "|x| 1+x")).to_be "function" + - it compiles to a working Lua function: + fn = f "||42" + expect (fn ()).to_be (42) + - it propagates argument values: + fn = f "|...| {...}" + expect (fn (1,2,3)).to_equal {1,2,3} + - context with expression format: + - it returns a function: + expect (prototype (f "_")).to_be "function" + - it compiles to a working Lua function: + fn = f "=42" + expect (fn ()).to_be (42) + - it sets auto-argument values: + fn = f "_*_" + expect (fn (42)).to_be (1764) + - it sets numeric auto-argument values: + fn = f "_1+_2+_3" + expect (fn (1, 2, 5)).to_be (8) + + +- describe map: + - before: + elements = {"a", "b", "c", "d", "e"} + inverse = {a=1, b=2, c=3, d=4, e=5} + + f = M.map + + - context with bad arguments: + badargs.diagnose (f, "std.functional.map (func, [func], any*)") + + - it works with an empty table: + expect (f (M.id, ipairs, {})).to_equal {} + - it iterates through elements: + expect (f (M.id, ipairs, elements)).to_equal (elements) + expect (f (M.id, pairs, inverse)).to_contain.a_permutation_of (elements) + - it propagates nil arguments correctly: + t = {"a", nil, nil, "d", "e"} + expect (f (M.id, base.npairs, t)).to_equal (t) + t = {nil, nil, 3, 4} + expect (f (M.id, base.npairs, t)).to_equal (t) + - it passes all iteration result values to map function: + t = {} + f (function (k, v) t[k] = v end, pairs, elements) + expect (t).to_equal (elements) + - it returns a list of mapped single return value iterator results: + expect (f (function (e) return e:match "[aeiou]" end, base.ielems, elements)). + to_equal {"a", "e"} + expect (f (function (e) return e .. "x" end, base.elems, elements)). + to_contain.a_permutation_of {"ax", "bx", "cx", "dx", "ex"} + - it returns a table of mapped key:value iterator results: + t = {"first", second=2, last="three"} + expect (f (function (k, v) return type (v) == "string" end, pairs, t)). + to_contain.a_permutation_of {true, false, true} + expect (f (function (k, v) return k % 2 == 0 end, ipairs, elements)). + to_equal {false, true, false, true, false} + - it supports key:value results from mapping function: + expect (f (function (k, v) return v, k end, pairs, elements)). + to_equal (inverse) + - it defaults to pairs iteration: + t = {"first", second=2, last="three"} + expect (f (function (k, v) return type (v) == "string" end, t)). + to_contain.a_permutation_of {true, false, true} + + +- describe map_with: + - before: + t = {{1, 2, 3}, {4, 5}} + fn = function (...) return select ("#", ...) end + + f = M.map_with + + - context with bad arguments: + badargs.diagnose (f, "std.functional.map_with (func, table of tables)") + + - it works for an empty table: + expect (f (fn, {})).to_equal ({}) + - it returns a table: + u = f (fn, t) + expect (type (u)).to_be "table" + - it creates a new table: + old = t + u = f (fn, t) + expect (t).to_equal (old) + expect (u).not_to_equal (old) + expect (t).to_equal {{1, 2, 3}, {4, 5}} + - it maps a function over a list of argument lists: + expect (f (fn, t)).to_equal {3, 2} + - it discards hash-part arguments: + expect (f (fn, {{1,x=2,3}, {4,5,y="z"}})).to_equal {2, 2} + - it maps a function over a table of argument lists: + expect (f (fn, {a={1,2,3}, b={4,5}})).to_equal {a=3, b=2} + + +- describe memoize: + - before: + f = M.memoize + + memfn = f (function (x) + if x then return {x} else return nil, "bzzt" end + end) + + - context with bad arguments: + badargs.diagnose (f, "std.functional.memoize (func, ?func)") + + - it propagates multiple return values: + expect (select (2, memfn (false))).to_be "bzzt" + - it returns the same object for the same arguments: + t = memfn (1) + expect (memfn (1)).to_be (t) + - it returns a different object for different arguments: + expect (memfn (1)).not_to_be (memfn (2)) + - it returns the same object for table valued arguments: + t = memfn {1, 2, 3} + expect (memfn {1, 2, 3}).to_be (t) + t = memfn {foo = "bar", baz = "quux"} + expect (memfn {foo = "bar", baz = "quux"}).to_be (t) + expect (memfn {baz = "quux", foo = "bar"}).to_be (t) + - it returns a different object for different table arguments: + expect (memfn {1, 2, 3}).not_to_be (memfn {1, 2}) + expect (memfn {1, 2, 3}).not_to_be (memfn {3, 1, 2}) + expect (memfn {1, 2, 3}).not_to_be (memfn {1, 2, 3, 4}) + - it accepts alternative normalization function: + normalize = function (...) return select ("#", ...) end + memfn = f (function (x) return {x} end, normalize) + expect (memfn "same").to_be (memfn "not same") + expect (memfn (1, 2)).to_be (memfn (false, "x")) + expect (memfn "one").not_to_be (memfn ("one", "two")) + + +- describe nop: + - before: + f = M.nop + - it accepts any number of arguments: + expect (f ()).to_be (nil) + expect (f (false)).to_be (nil) + expect (f (1, 2, 3, nil, "str", {}, f)).to_be (nil) + - it returns no values: + expect (f (1, "two", false)).to_be (nil) + + +- describe op: + - context with []: + - before: + f = M.op["[]"] + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {{2}, 1})). + to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {{2}, 1})). + not_to_contain_error "was deprecated" + + - it dereferences a table: + expect (f ({}, 1)).to_be (nil) + expect (f ({"foo", "bar"}, 1)).to_be "foo" + expect (f ({foo = "bar"}, "foo")).to_be "bar" + + - context with +: + - before: + f = M.op["+"] + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {2, 1})). + to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {2, 1})). + not_to_contain_error "was deprecated" + + - it returns the sum of its arguments: + expect (f (99, 2)).to_be (99 + 2) + + - context with -: + - before: + f = M.op["-"] + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {2, 1})). + to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {2, 1})). + not_to_contain_error "was deprecated" + + - it returns the difference of its arguments: + expect (f (99, 2)).to_be (99 - 2) + + - context with *: + - before: + f = M.op["*"] + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {2, 1})). + to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {2, 1})). + not_to_contain_error "was deprecated" + + - it returns the product of its arguments: + expect (f (99, 2)).to_be (99 * 2) + + - context with /: + - before: + f = M.op["/"] + + - it writes a deprecation warning on: + setdebug { deprecate = "nil" } + expect (capture (f, {2, 1})). + to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {2, 1})). + not_to_contain_error "was deprecated" + + - it returns the quotient of its arguments: + expect (f (99, 2)).to_be (99 / 2) + + - context with and: + - before: + f = M.op["and"] + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {true, false})). + to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {true, false})). + not_to_contain_error "was deprecated" + + - it returns the logical and of its arguments: + expect (f (false, false)).to_be (false) + expect (f (false, true)).to_be (false) + expect (f (true, false)).to_be (false) + expect (f (true, true)).to_be (true) + - it supports truthy and falsey arguments: + expect (f ()).to_be (nil) + expect (f (0)).to_be (nil) + expect (f (nil, 0)).to_be (nil) + expect (f (0, "false")).to_be ("false") + + - context with or: + - before: + f = M.op["or"] + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {true, false})). + to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {true, false})). + not_to_contain_error "was deprecated" + + - it returns the logical or of its arguments: + expect (f (false, false)).to_be (false) + expect (f (false, true)).to_be (true) + expect (f (true, false)).to_be (true) + expect (f (true, true)).to_be (true) + - it supports truthy and falsey arguments: + expect (f ()).to_be (nil) + expect (f (0)).to_be (0) + expect (f (nil, 0)).to_be (0) + expect (f (0, "false")).to_be (0) + + - context with not: + - before: + f = M.op["not"] + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {true})). + to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {true})). + not_to_contain_error "was deprecated" + + - it returns the logical not of its argument: + expect (f (false)).to_be (true) + expect (f (true)).to_be (false) + - it supports truthy and falsey arguments: + expect (f ()).to_be (true) + expect (f (0)).to_be (false) + + - context with ==: + - before: + f = M.op["=="] + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {2, 1})). + to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {2, 1})). + not_to_contain_error "was deprecated" + + - it returns true if the arguments are equal: + expect (f ()).to_be (true) + expect (f ("foo", "foo")).to_be (true) + - it returns false if the arguments are unequal: + expect (f (1)).to_be (false) + expect (f ("foo", "bar")).to_be (false) + + - context with ~=: + - before: + f = M.op["~="] + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {2, 1})). + to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {2, 1})). + not_to_contain_error "was deprecated" + + - it returns false if the arguments are equal: + expect (f (1, 1)).to_be (false) + expect (f ("foo", "foo")).to_be (false) + - it returns true if the arguments are unequal: + expect (f (1, 2)).to_be (true) + expect (f ("foo", "bar")).to_be (true) + expect (f ({}, {})).to_be (true) + + +- describe reduce: + - before: + op = require "std.operator" + + f = M.reduce + + - context with bad arguments: + badargs.diagnose (f, "std.functional.reduce (func, any, [func], any*)") + + - it works with an empty table: + expect (f (op.sum, 2, ipairs, {})).to_be (2) + - it calls a binary function over single return value iterator results: + expect (f (op.sum, 2, base.ielems, {3})). + to_be (2 + 3) + expect (f (op.prod, 2, base.ielems, {3, 4})). + to_be (2 * 3 * 4) + - it calls a binary function over key:value iterator results: + expect (f (op.sum, 2, base.ielems, {3})).to_be (2 + 3) + expect (f (op.prod, 2, base.ielems, {3, 4})).to_be (2 * 3 * 4) + - it propagates nil arguments correctly: + function set (t, k, v) t[k] = tostring (v) return t end + expect (f (set, {}, base.npairs, {1, nil, nil, "a", false})). + to_equal {"1", "nil", "nil", "a", "false"} + expect (f (set, {}, base.npairs, {nil, nil, "3"})). + to_equal {"nil", "nil", "3"} + - it reduces elements from left to right: + expect (f (op.pow, 2, base.ielems, {3, 4})).to_be ((2 ^ 3) ^ 4) + - it passes all iterator results to accumulator function: + expect (f (rawset, {}, {"one", two=5})).to_equal {"one", two=5} + + +- describe zip: + - before: + tt = {{1, 2}, {3, 4}, {5, 6}} + + f = M.zip + + - context with bad arguments: + badargs.diagnose (f, "std.functional.zip (table)") + + - it works for an empty table: + expect (f {}).to_equal {} + - it is the inverse of itself: + expect (f (f (tt))).to_equal (tt) + - it transposes rows and columns: + expect (f (tt)).to_equal {{1, 3, 5}, {2, 4, 6}} + expect (f {x={a=1, b=2}, y={a=3, b=4}, z={b=5}}). + to_equal {a={x=1, y=3}, b={x=2,y=4,z=5}} + + +- describe zip_with: + - before: + tt = {{1, 2}, {3, 4}, {5}} + fn = function (...) return tonumber (table.concat {...}) end + + f = M.zip_with + + - context with bad arguments: + badargs.diagnose (f, "std.functional.zip_with (function, table of tables)") + + - it works for an empty table: + expect (f (fn, {})).to_equal {} + - it returns a table: + expect (type (f (fn, tt))).to_be "table" + - it returns the result in a new table: + expect (f (fn, tt)).not_to_be (tt) + - it does not perturb the argument list: + m = f (fn, tt) + expect (tt).to_equal {{1, 2}, {3, 4}, {5}} + - it combines column entries with a function: + expect (f (fn, tt)).to_equal {135, 24} + - it discards hash-part arguments: + expect (f (fn, {{1,2}, x={3,4}, {[2]=5}})).to_equal {1, 25} + - it combines matching key entries with a function: + expect (f (fn, {{a=1,b=2}, {a=3,b=4}, {b=5}})). + to_equal {a=13, b=245} diff --git a/spec/io_spec.yaml b/spec/io_spec.yaml new file mode 100644 index 0000000..e46ca73 --- /dev/null +++ b/spec/io_spec.yaml @@ -0,0 +1,442 @@ +before: | + base_module = "io" + this_module = "std.io" + global_table = "_G" + + extend_base = { "catdir", "catfile", "die", "dirname", "monkey_patch", + "process_files", "readlines", "shell", "slurp", + "splitdir", "warn", "writelines" } + + dirsep = string.match (package.config, "^([^\n]+)\n") + + M = require (this_module) + + +specify std.io: +- context when required: + - context by name: + - it does not touch the global table: + expect (show_apis {added_to=global_table, by=this_module}). + to_equal {} + - it does not touch the core io table: + expect (show_apis {added_to=base_module, by=this_module}). + to_equal {} + - it contains apis from the core io table: + expect (show_apis {from=base_module, not_in=this_module}). + to_contain.a_permutation_of (extend_base) + + - context via the std module: + - it does not touch the global table: + expect (show_apis {added_to=global_table, by="std"}). + to_equal {} + - it does not touch the core io table: + expect (show_apis {added_to=base_module, by="std"}). + to_equal {} + + +- describe catdir: + - before: | + f = M.catdir + + - context with bad arguments: + badargs.diagnose (f, "std.io.catdir (string*)") + + - it treats initial empty string as root directory: + expect (f ("")).to_be (dirsep) + expect (f ("", "")).to_be (dirsep) + expect (f ("", "root")).to_be (dirsep .. "root") + - it returns a single argument unchanged: + expect (f ("hello")).to_be "hello" + - it joins multiple arguments with platform directory separator: + expect (f ("one", "two")).to_be ("one" .. dirsep .. "two") + expect (f ("1", "2", "3", "4", "5")). + to_be (table.concat ({"1", "2", "3", "4", "5"}, dirsep)) + + +- describe catfile: + - before: + f = M.catfile + + - context with bad arguments: + badargs.diagnose (f, "std.io.catfile (string*)") + + - it treats initial empty string as root directory: + expect (f ("", "")).to_be (dirsep) + expect (f ("", "root")).to_be (dirsep .. "root") + - it returns a single argument unchanged: + expect (f ("")).to_be "" + expect (f ("hello")).to_be "hello" + - it joins multiple arguments with platform directory separator: + expect (f ("one", "two")).to_be ("one" .. dirsep .. "two") + expect (f ("1", "2", "3", "4", "5")). + to_be (table.concat ({"1", "2", "3", "4", "5"}, dirsep)) + + +- describe die: + - before: | + script = [[require "std.io".die "By 'eck!"]] + + f = M.die + + - context with bad arguments: + badargs.diagnose (f, "std.io.die (string, ?any*)") + + - it outputs a message to stderr: | + expect (luaproc (script)).to_fail_while_matching ": By 'eck!\n" + - it ignores `prog.line` without `prog.file` or `prog.name`: | + script = [[prog = { line = 125 };]] .. script + expect (luaproc (script)).to_fail_while_matching ": By 'eck!\n" + - it ignores `opts.line` without `opts.program`: | + script = [[opts = { line = 99 };]] .. script + expect (luaproc (script)).to_fail_while_matching ": By 'eck!\n" + - it prefixes `prog.name` if any: | + script = [[prog = { name = "name" };]] .. script + expect (luaproc (script)).to_fail_while_matching ": name: By 'eck!\n" + - it appends `prog.line` if any, to `prog.name`: | + script = [[prog = { line = 125, name = "name" };]] .. script + expect (luaproc (script)).to_fail_while_matching ": name:125: By 'eck!\n" + - it prefixes `prog.file` if any: | + script = [[prog = { file = "file" };]] .. script + expect (luaproc (script)).to_fail_while_matching ": file: By 'eck!\n" + - it appends `prog.line` if any, to `prog.name`: | + script = [[prog = { file = "file", line = 125 };]] .. script + expect (luaproc (script)).to_fail_while_matching ": file:125: By 'eck!\n" + - it prefers `prog.name` to `prog.file` or `opts.program`: | + script = [[ + prog = { file = "file", name = "name" } + opts = { program = "program" } + ]] .. script + expect (luaproc (script)).to_fail_while_matching ": name: By 'eck!\n" + - it appends `prog.line` if any to `prog.name` over anything else: | + script = [[ + prog = { file = "file", line = 125, name = "name" } + opts = { line = 99, program = "program" } + ]] .. script + expect (luaproc (script)).to_fail_while_matching ": name:125: By 'eck!\n" + - it prefers `prog.file` to `opts.program`: | + script = [[ + prog = { file = "file" }; opts = { program = "program" } + ]] .. script + expect (luaproc (script)).to_fail_while_matching ": file: By 'eck!\n" + - it appends `prog.line` if any to `prog.file` over using `opts`: | + script = [[ + prog = { file = "file", line = 125 } + opts = { line = 99, program = "program" } + ]] .. script + expect (luaproc (script)).to_fail_while_matching ": file:125: By 'eck!\n" + - it prefixes `opts.program` if any: | + script = [[opts = { program = "program" };]] .. script + expect (luaproc (script)).to_fail_while_matching ": program: By 'eck!\n" + - it appends `opts.line` if any, to `opts.program`: | + script = [[opts = { line = 99, program = "program" };]] .. script + expect (luaproc (script)).to_fail_while_matching ": program:99: By 'eck!\n" + + +- describe dirname: + - before: + f = M.dirname + path = table.concat ({"", "one", "two", "three"}, dirsep) + + - context with bad arguments: + badargs.diagnose (f, "std.io.dirname (string)") + + - it removes final separator and following: + expect (f (path)).to_be (table.concat ({"", "one", "two"}, dirsep)) + + +- describe monkey_patch: + - before: + namespace = {} + + f = M.monkey_patch + + - context with bad arguments: + badargs.diagnose (f, "std.io.monkey_patch (?table)") + + # Ideally, `.to_be (M)`, except that M is cloned from a nested context + # by Specl to prevent us from affecting any other examples, thus the + # address is different by now. + - it returns std.io module table: + expect (f {}).to_equal (M) + - it injects std.io apis into the given namespace: + namespace = {} + f (namespace) + for _, api in ipairs (extend_base) do + expect (namespace.io[api]).to_be (M[api]) + end + - it installs file methods: + mt = { "file metatable" } + io = f { + io = { + stdin = setmetatable ({ "stdin" }, mt), + stdout = setmetatable ({ "stdout" }, mt), + stderr = setmetatable ({ "stderr" }, mt) + } + } + expect (mt.readlines).to_be (M.readlines) + expect (mt.writelines).to_be (M.writelines) + + +- describe process_files: + - before: + name = "Makefile" + names = {"Makefile", "NEWS.md", "README.md", "build-aux/config.ld"} + ascript = [[ + require "std.io".process_files (function (a) print (a) end) + ]] + lscript = [[ + require "std.io".process_files ("=print (_1)") + ]] + iscript = [[ + require "std.io".process_files (function (_, i) print (i) end) + ]] + catscript = [[ + require "std.io".process_files (function () io.write (io.input ():read "*a") end) + ]] + + f = M.process_files + + - context with bad arguments: | + badargs.diagnose (f, "std.io.process_files (func)") + + examples { + ["it diagnoses non-file 'arg' elements"] = function () + expect (luaproc (ascript, "not-an-existing-file")).to_contain_error.any_of { + "cannot open file 'not-an-existing-file'", -- Lua 5.2 + "bad argument #1 to 'input' (not-an-existing-file:", -- Lua 5.1 + } + end + } + + - it defaults to `-` if no arguments were passed: + expect (luaproc (ascript)).to_output "-\n" + - it iterates over arguments with supplied function: + expect (luaproc (ascript, name)).to_output (name .. "\n") + expect (luaproc (ascript, names)). + to_output (table.concat (names, "\n") .. "\n") + - it passes argument numbers to supplied function: + expect (luaproc (iscript, names)).to_output "1\n2\n3\n4\n" + - it sets each file argument as the default input: + expect (luaproc (catscript, name)).to_output (concat_file_content (name)) + expect (luaproc (catscript, names)). + to_output (concat_file_content (unpack (names))) + - it processes io.stdin if no arguments were passed: + ## FIXME: where does that closing newline come from?? + expect (luaproc (catscript, nil, "some\nlines\nof input")).to_output "some\nlines\nof input\n" + - it processes io.stdin for `-` argument: + ## FIXME: where does that closing newline come from?? + expect (luaproc (catscript, "-", "some\nlines\nof input")).to_output "some\nlines\nof input\n" + + +- describe readlines: + - before: | + name = "Makefile" + h = io.open (name) + lines = {} for l in h:lines () do lines[#lines + 1] = l end + h:close () + + defaultin = io.input () + + f, badarg = init (M, this_module, "readlines") + - after: + if io.type (defaultin) ~= "closed file" then io.input (defaultin) end + + - context with bad arguments: | + badargs.diagnose (f, "std.io.readlines (?file|string)") + + examples { + ["it diagnoses non-existent file"] = function () + expect (f "not-an-existing-file"). + to_raise "bad argument #1 to 'std.io.readlines' (" -- system dependent error message + end + } + closed = io.open (name, "r") closed:close () + examples { + ["it diagnoses closed file argument"] = function () + expect (f (closed)).to_raise (badarg (1, "?file|string", "closed file")) + end + } + + - it closes file handle upon completion: + h = io.open (name) + expect (io.type (h)).not_to_be "closed file" + f (h) + expect (io.type (h)).to_be "closed file" + - it reads lines from an existing named file: + expect (f (name)).to_equal (lines) + - it reads lines from an open file handle: + expect (f (io.open (name))).to_equal (lines) + - it reads from default input stream with no arguments: + io.input (name) + expect (f ()).to_equal (lines) + + +- describe shell: + - before: + f = M.shell + + - context with bad arguments: + badargs.diagnose (f, "std.io.shell (string)") + + - it returns the output from a shell command string: + expect (f [[printf '%s\n' 'foo' 'bar']]).to_be "foo\nbar\n" + + +- describe slurp: + - before: | + name = "Makefile" + h = io.open (name) + content = h:read "*a" + h:close () + + defaultin = io.input () + f, badarg = init (M, this_module, "slurp") + - after: + if io.type (defaultin) ~= "closed file" then io.input (defaultin) end + + - context with bad arguments: | + badargs.diagnose (f, "std.io.slurp (?file|string)") + + examples { + ["it diagnoses non-existent file"] = function () + expect (f "not-an-existing-file"). + to_raise "bad argument #1 to 'std.io.slurp' (" -- system dependent error message + end + } + closed = io.open (name, "r") closed:close () + examples { + ["it diagnoses closed file argument"] = function () + expect (f (closed)).to_raise (badarg (1, "?file|string", "closed file")) + end + } + + - it reads content from an existing named file: + expect (f (name)).to_be (content) + - it reads content from an open file handle: + expect (f (io.open (name))).to_be (content) + - it closes file handle upon completion: + h = io.open (name) + expect (io.type (h)).not_to_be "closed file" + f (h) + expect (io.type (h)).to_be "closed file" + - it reads from default input stream with no arguments: + io.input (name) + expect (f ()).to_be (content) + + +- describe splitdir: + - before: + f = M.splitdir + + - context with bad arguments: + badargs.diagnose (f, "std.io.splitdir (string)") + + - it returns a filename as a one element list: + expect (f ("hello")).to_equal {"hello"} + - it splits root directory in two empty elements: + expect (f (dirsep)).to_equal {"", ""} + - it returns initial empty string for absolute path: + expect (f (dirsep .. "root")).to_equal {"", "root"} + - it returns multiple components split at platform directory separator: + expect (f ("one" .. dirsep .. "two")).to_equal {"one", "two"} + expect (f (table.concat ({"1", "2", "3", "4", "5"}, dirsep))). + to_equal {"1", "2", "3", "4", "5"} + + +- describe warn: + - before: + script = [[require "std.io".warn "Ayup!"]] + f = M.warn + + - context with bad arguments: + badargs.diagnose (f, "std.io.warn (string, ?any*)") + + - it outputs a message to stderr: + expect (luaproc (script)).to_output_error "Ayup!\n" + - it ignores `prog.line` without `prog.file`, `prog.name` or `opts.program`: + script = [[prog = { line = 125 };]] .. script + expect (luaproc (script)).to_output_error "Ayup!\n" + - it prefixes `prog.name` if any: | + script = [[prog = { name = "name" };]] .. script + expect (luaproc (script)).to_output_error "name: Ayup!\n" + - it appends `prog.line` if any, to `prog.name`: | + script = [[prog = { line = 125, name = "name" };]] .. script + expect (luaproc (script)).to_output_error "name:125: Ayup!\n" + - it prefixes `prog.file` if any: | + script = [[prog = { file = "file" };]] .. script + expect (luaproc (script)).to_output_error "file: Ayup!\n" + - it appends `prog.line` if any, to `prog.name`: | + script = [[prog = { file = "file", line = 125 };]] .. script + expect (luaproc (script)).to_output_error "file:125: Ayup!\n" + - it prefers `prog.name` to `prog.file` or `opts.program`: | + script = [[ + prog = { file = "file", name = "name" } + opts = { program = "program" } + ]] .. script + expect (luaproc (script)).to_output_error "name: Ayup!\n" + - it appends `prog.line` if any to `prog.name` over anything else: | + script = [[ + prog = { file = "file", line = 125, name = "name" } + opts = { line = 99, program = "program" } + ]] .. script + expect (luaproc (script)).to_output_error "name:125: Ayup!\n" + - it prefers `prog.file` to `opts.program`: | + script = [[ + prog = { file = "file" }; opts = { program = "program" } + ]] .. script + expect (luaproc (script)).to_output_error "file: Ayup!\n" + - it appends `prog.line` if any to `prog.file` over using `opts`: | + script = [[ + prog = { file = "file", line = 125 } + opts = { line = 99, program = "program" } + ]] .. script + expect (luaproc (script)).to_output_error "file:125: Ayup!\n" + - it prefixes `opts.program` if any: | + script = [[opts = { program = "program" };]] .. script + expect (luaproc (script)).to_output_error "program: Ayup!\n" + - it appends `opts.line` if any, to `opts.program`: | + script = [[opts = { line = 99, program = "program" };]] .. script + expect (luaproc (script)).to_output_error "program:99: Ayup!\n" + + +- describe writelines: + - before: | + name = os.tmpname () + h = io.open (name, "w") + lines = M.readlines (io.open "Makefile") + + defaultout = io.output () + f, badarg = init (M, this_module, "writelines") + - after: + if io.type (defaultout) ~= "closed file" then io.output (defaultout) end + h:close () + os.remove (name) + + - context with bad arguments: + - 'it diagnoses argument #1 type not FILE*, string, number or nil': + expect (f (false)).to_raise (badarg (1, "?file|string|number", "boolean")) + - 'it diagnoses argument #2 type not string, number or nil': + expect (f (1, false)).to_raise (badarg (2, "string|number", "boolean")) + - 'it diagnoses argument #3 type not string, number or nil': + expect (f (1, 2, false)).to_raise (badarg (3, "string|number", "boolean")) + - it diagnoses closed file argument: | + closed = io.open (name, "r") closed:close () + expect (f (closed)).to_raise (badarg (1, "?file|string|number", "closed file")) + + - it does not close the file handle upon completion: + expect (io.type (h)).not_to_be "closed file" + f (h, "foo") + expect (io.type (h)).not_to_be "closed file" + - it writes lines to an open file handle: + f (h, unpack (lines)) + h:flush () + expect (M.readlines (io.open (name))).to_equal (lines) + - it accepts number valued arguments: + f (h, 1, 2, 3) + h:flush () + expect (M.readlines (io.open (name))).to_equal {"1", "2", "3"} + - it writes to default output stream with non-file first argument: + io.output (h) + f (unpack (lines)) + h:flush () + expect (M.readlines (io.open (name))).to_equal (lines) diff --git a/spec/list_spec.yaml b/spec/list_spec.yaml new file mode 100644 index 0000000..7a90aa0 --- /dev/null +++ b/spec/list_spec.yaml @@ -0,0 +1,1239 @@ +before: + this_module = "std.list" + global_table = "_G" + + exported_apis = { "append", "compare", "concat", "cons", "depair", + "elems", "enpair", "filter", "flatten", "foldl", + "foldr", "index_key", "index_value", "map", + "map_with", "project", "relems", "rep", "reverse", + "shape", "sub", "tail", "transpose", "zip_with" } + + M = require (this_module) + + List = M {} + l = List {"foo", "bar", "baz"} + + + +specify std.list: +- context when required: + - context by name: + - it does not touch the global table: + expect (show_apis {added_to="_G", by="std.list"}). + to_equal {} + - it exports the documented apis: + t = {} + for k in pairs (M) do t[#t + 1] = k end + expect (t).to_contain.a_permutation_of (exported_apis) + + - context via the std module: + - it does not touch the global table: + expect (show_apis {added_to=global_table, by="std"}). + to_equal {} + +- describe construction: + - context from List clone method: + - it constructs a new list: + l = List:clone {} + expect (l).not_to_be (List) + expect (prototype (l)).to_be "List" + - it reuses the List metatable: + l, m = List:clone {"l"}, List:clone {"m"} + expect (getmetatable (l)).to_be (getmetatable (m)) + - it initialises List with constructor parameters: + m = List:clone {"foo", "bar", "baz"} + expect (m).to_equal (l) + - it serves as a prototype for new instances: + m = l:clone {} + expect (prototype (m)).to_be "List" + expect (m).to_equal (l) + expect (getmetatable (m)).to_be (getmetatable (l)) + + # List {args} is just syntactic sugar for List:clone {args} + - context from List object prototype: + - it constructs a new List: + l = List {} + expect (l).not_to_be (List) + expect (prototype (l)).to_be "List" + - it reuses the List metatable: + l, m = List {"l"}, List {"m"} + expect (getmetatable (l)).to_be (getmetatable (m)) + - it initialises List with constructor parameters: + m = List {"foo", "bar", "baz"} + expect (m).to_equal (l) + - it serves as a prototype for new instances: + m = l {} + expect (prototype (m)).to_be "List" + expect (m).to_equal (l) + expect (getmetatable (m)).to_be (getmetatable (l)) + + +- describe metatable propagation: + - it reuses the metatable for List constructed objects: + m = List {"foo", "bar"} + expect (getmetatable (m)).to_be (getmetatable (l)) + + +- describe append: + - before: + f = M.append + + - context with bad arguments: + badargs.diagnose (f, "std.list.append (List, any)") + + - context as a module function: + - it returns a List object: + expect (prototype (f (l, "quux"))).to_be "List" + - it works for an empty List: + expect (f (List {}, "quux")).to_equal (List {"quux"}) + - it appends an item to a List: + expect (f (l, "quux")). + to_equal (List {"foo", "bar", "baz", "quux"}) + + - context as an object method: + - before: + f = l.append + + - it returns a List object: + expect (prototype (f (l, "quux"))).to_be "List" + - it works for an empty List: + expect (f (List {}, "quux")).to_equal (List {"quux"}) + - it appends an item to a List: + expect (f (l, "quux")). + to_equal (List {"foo", "bar", "baz", "quux"}) + + - context as a List metamethod: + - it returns a List object: + expect (prototype (l + "quux")).to_be "List" + - it works for an empty list: + expect (List {} + "quux").to_equal (List {"quux"}) + - it appends an item to a list: + expect (l + "quux"). + to_equal (List {"foo", "bar", "baz", "quux"}) + + +- describe compare: + - before: + a, b = List {"foo", "bar"}, List {"foo", "baz"} + + f = M.compare + + - context with bad arguments: + badargs.diagnose (f, "std.list.compare (List, List|table)") + + - context as a module function: + - it returns -1 when the first list is less than the second: + expect (f (a, {"foo", "baz"})).to_be (-1) + expect (f (a, List {"foo", "baz"})).to_be (-1) + - it returns -1 when the second list has additional elements: + expect (f (List {"foo"}, {"foo", "bar"})).to_be (-1) + expect (f (List {"foo"}, List {"foo", "bar"})).to_be (-1) + - it returns 0 when two lists are the same: + expect (f (a, {"foo", "bar"})).to_be (0) + expect (f (a, List {"foo", "bar"})).to_be (0) + - it returns +1 when the first list is greater than the second: + expect (f (a, {"baz", "quux"})).to_be (1) + expect (f (a, List {"baz", "quux"})).to_be (1) + - it returns +1 when the first list has additional elements: + expect (f (a, {"foo"})).to_be (1) + expect (f (a, List {"foo"})).to_be (1) + - it compares numerically when both arguments can be coerced: + a, b = List {"1", "2", "3"}, List {"1", "2", "10"} + expect (f (a, b)).to_be (-1) + + - context as an object method: + - before: + f = a.compare + + - it returns -1 when the first list is less than the second: + expect (f (a, {"foo", "baz"})).to_be (-1) + expect (f (a, List {"foo", "baz"})).to_be (-1) + - it returns -1 when the second list has additional elements: | + b = List {"foo"} + expect (f (b, {"foo", "bar"})).to_be (-1) + expect (List {"foo"}:compare (List {"foo", "bar"})).to_be (-1) + - it returns 0 when two lists are the same: + expect (f (a, {"foo", "bar"})).to_be (0) + expect (f (a, List {"foo", "bar"})).to_be (0) + - it returns +1 when the first list is greater than the second: + expect (f (a, {"baz", "quux"})).to_be (1) + expect (f (a, List {"baz", "quux"})).to_be (1) + - it returns +1 when the first list has additional elements: + expect (f (a, {"foo"})).to_be (1) + expect (f (a, List {"foo"})).to_be (1) + - it compares numerically when both arguments can be coerced: + a, b = List {"1", "2", "3"}, List {"1", "2", "10"} + expect (f (a, b)).to_be (-1) + + - context as a '<' List metamethod: + - it succeeds when the first list is less than the second: + expect (a < b).to_be (true) + - it fails when the first list is not less than the second: + expect (a < a).to_be (false) + expect (b < a).to_be (false) + - it compares numerically when both arguments can be coerced: + a, b = List {"1", "2", "3"}, List {"1", "2", "10"} + expect (a < b).to_be (true) + + - context as a '>' List metamethod: + - it succeeds when the first list is greater than the second: + expect (b > a).to_be (true) + - it fails when the first list is not greater than the second: + expect (b > b).to_be (false) + expect (a > b).to_be (false) + - it compares numerically when both arguments can be coerced: + a, b = List {"1", "2", "3"}, List {"1", "2", "10"} + expect (a > b).to_be (false) + + - context as a '<=' List metamethod: + - it succeeds when the first list is less than or equal to the second: + expect (a <= b).to_be (true) + expect (a <= a).to_be (true) + - it fails when the first list is not less than or equal to the second: + expect (b <= a).to_be (false) + - it compares numerically when both arguments can be coerced: + a, b = List {"1", "2", "3"}, List {"1", "2", "10"} + expect (a <= b).to_be (true) + + - context as a '>=' List metamethod: + - it succeeds when the first list is greater than or equal to the second: + expect (b >= a).to_be (true) + expect (b >= b).to_be (true) + - it fails when the first list is not greater than or equal to the second: + expect (a >= b).to_be (false) + - it compares numerically when both arguments can be coerced: + a, b = List {"1", "2", "3"}, List {"1", "2", "10"} + expect (a >= b).to_be (false) + + +- describe concat: + - before: + l = List {"foo", "bar"} + + f = M.concat + + - context with bad arguments: + badargs.diagnose (f, "std.list.concat (List, List|table*)") + + - context as a module function: + - it returns a List object: + expect (prototype (f (l, l))).to_be "List" + - it works for an empty List: + expect (f (List {}, {"baz"})).to_equal (List {"baz"}) + expect (f (List {}, List {"baz"})).to_equal (List {"baz"}) + - it concatenates Lists: + expect (f (l, {"baz", "quux"})). + to_equal (List {"foo", "bar", "baz", "quux"}) + expect (f (l, List {"baz", "quux"})). + to_equal (List {"foo", "bar", "baz", "quux"}) + expect (f (l, {"baz"}, {"quux"})). + to_equal (List {"foo", "bar", "baz", "quux"}) + expect (f (l, List {"baz"}, List {"quux"})). + to_equal (List {"foo", "bar", "baz", "quux"}) + + - context as an object method: + - before: + f = l.concat + + - it returns a List object: + expect (prototype (f (l, l))).to_be "List" + - it works for an empty List: + expect (f (List {}, {"baz"})).to_equal (List {"baz"}) + expect (f (List {}, List {"baz"})).to_equal (List {"baz"}) + - it concatenates Lists: + expect (f (l, {"baz", "quux"})). + to_equal (List {"foo", "bar", "baz", "quux"}) + expect (f (l, List {"baz", "quux"})). + to_equal (List {"foo", "bar", "baz", "quux"}) + expect (f (l, {"baz"}, {"quux"})). + to_equal (List {"foo", "bar", "baz", "quux"}) + expect (f (l, List {"baz"}, List {"quux"})). + to_equal (List {"foo", "bar", "baz", "quux"}) + + # Beware that .. operations are right associative + - context as a List metamethod: + - it returns a List object: + expect (prototype (l .. List {"baz"})).to_be "List" + - it works for an empty List: + expect (List {} .. {"baz"}).to_equal (List {"baz"}) + expect (List {} .. List {"baz"}).to_equal (List {"baz"}) + - it concatenates Lists: + expect (l .. {"baz", "quux"}). + to_equal (List {"foo", "bar", "baz", "quux"}) + expect (l .. List {"baz", "quux"}). + to_equal (List {"foo", "bar", "baz", "quux"}) + expect ({"baz"} .. {"quux"} .. l). + to_equal (List {"baz", "quux", "foo", "bar"}) + expect (l .. List {"baz"} .. List {"quux"}). + to_equal (List {"foo", "bar", "baz", "quux"}) + + +- describe cons: + - before: + f = M.cons + + - context with bad arguments: + badargs.diagnose (f, "std.list.cons (List, any)") + + - context as a module function: + - it returns a List object: + expect (prototype (f (l, "x"))).to_be "List" + - it prepends an item to a List: + expect (f (l, "x")).to_equal (List {"x", "foo", "bar", "baz"}) + - it works for empty Lists: + expect (f (List {}, "x")).to_equal (List {"x"}) + + - context as an object method: + - before: + f = l.cons + + - it returns a List object: + expect (prototype (f (l, "x"))).to_be "List" + - it prepends an item to a List: + expect (f (l, "x")).to_equal (List {"x", "foo", "bar", "baz"}) + - it works for empty Lists: + expect (f (List {}, "x")).to_equal (List {"x"}) + + +- describe depair: + - before: + l = List {List {1, "first"}, List {2, "second"}, List {"third", 4}} + t = {"first", "second", third = 4} + + - context as a module function: + - before: + f = M.depair + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {l})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {l})).not_to_contain_error "was deprecated" + + - it returns a primitive table: + expect (prototype (f (l))).to_be "table" + - it works with an empty List: + l = List {} + expect (f (l)).to_equal {} + - it is the inverse of enpair: + expect (f (l)).to_equal (t) + + - context as an object method: + - before: + f = l.depair + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {l})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {l})).not_to_contain_error "was deprecated" + + - it returns a primitive table: + expect (prototype (f (l))).to_be "table" + - it works with an empty List: + expect (f (List {})).to_equal {} + - it is the inverse of enpair: + expect (f (l)).to_equal (t) + + +- describe elems: + - context as a module function: + - before: + f = M.elems + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {{}})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {{}})).not_to_contain_error "was deprecated" + + - it is an iterator over List members: + t = {} + for e in f (l) do table.insert (t, e) end + expect (t).to_equal {"foo", "bar", "baz"} + - it works for an empty List: + t = {} + for e in f (List {}) do table.insert (t, e) end + expect (t).to_equal {} + + - context as an object method: + - before: + f = l.elems + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {l})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {l})).not_to_contain_error "was deprecated" + + - it is an iterator over List members: + t = {} + for e in l:elems () do table.insert (t, e) end + expect (t).to_equal {"foo", "bar", "baz"} + - it works for an empty List: + t, l = {}, List {} + for e in l:elems () do table.insert (t, e) end + expect (t).to_equal {} + + +- describe enpair: + - before: + t = {"first", "second", third = 4} + f = M.enpair + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {t})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {t})).not_to_contain_error "was deprecated" + + - context as a module function: + - it returns a List object: + expect (prototype (f (t))).to_be "List" + - it works for an empty table: + expect (f {}).to_equal (List {}) + - it turns a table into a List of pairs: + expect (f (t)). + to_equal (List {List {1, "first"}, List {2, "second"}, List {"third", 4}}) + + +- describe filter: + - before: + l = List {"foo", "bar", "baz", "quux"} + p = function (e) return (e:match "a" ~= nil) end + + - context as a module function: + - before: + f = M.filter + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {p, l})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {p, l})).not_to_contain_error "was deprecated" + + - it returns a List object: + expect (prototype (f (p, l))).to_be "List" + - it works for an empty List: + expect (f (p, List {})).to_equal (List {}) + - it filters a List according to a predicate: + expect (f (p, l)).to_equal (List {"bar", "baz"}) + + - context as an object method: + - before: + f = l.filter + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {l, p})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {l, p})).not_to_contain_error "was deprecated" + + - it returns a List object: + expect (prototype (f (l, p))).to_be "List" + - it works for an empty List: + expect (f (List {}, p)).to_equal (List {}) + - it filters a List according to a predicate: + expect (f (l, p)).to_equal (List {"bar", "baz"}) + + +- describe flatten: + - before: + l = List {List {List {"one"}}, "two", List {List {"three"}, "four"}} + + - context as a module function: + - before: + f = M.flatten + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {l})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {l})).not_to_contain_error "was deprecated" + + - it returns a List object: + expect (prototype (f (l))).to_be "List" + - it works for an empty List: + l = List {} + expect (f (l)).to_equal (List {}) + - it flattens a List: + expect (f (l)). + to_equal (List {"one", "two", "three", "four"}) + + - context as an object method: + - before: + f = l.flatten + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {l})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {l})).not_to_contain_error "was deprecated" + + - it returns a List object: + expect (prototype (f (l))).to_be "List" + - it works for an empty List: + l = List {} + expect (f (l)).to_equal (List {}) + - it flattens a List: + expect (f (l)). + to_equal (List {"one", "two", "three", "four"}) + + +- describe foldl: + - before: + op = require "std.operator" + l = List {3, 4} + + - context as a module function: + - before: + f = M.foldl + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {op.sum, 1, l})). + to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {op.sum, 1, l})). + not_to_contain_error "was deprecated" + + - context with a table: + - it works with an empty table: + expect (f (op.sum, 10000, {})).to_be (10000) + - it folds a binary function through a table: + expect (f (op.sum, 10000, {1, 10, 100})).to_be (10111) + - it folds from left to right: + expect (f (op.pow, 2, {3, 4})).to_be ((2 ^ 3) ^ 4) + + - context with a List: + - it works with an empty List: + expect (f (op.sum, 10000, List {})).to_be (10000) + - it folds a binary function through a List: + expect (f (op.sum, 10000, List {1, 10, 100})). + to_be (10111) + - it folds from left to right: + expect (f (op.pow, 2, List {3, 4})).to_be ((2 ^ 3) ^ 4) + + - context as an object method: + - before: + f = l.foldl + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {l, op.sum, 1})). + to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {l, op.sum, 1})). + not_to_contain_error "was deprecated" + + - it works with an empty List: + l = List {} + expect (f (l, op.sum, 2)).to_be (2) + - it folds a binary function through a List: + expect (f (l, op.sum, 2)).to_be (9) + - it folds from left to right: + expect (f (l, op.pow, 2)).to_be ((2 ^ 3) ^ 4) + + +- describe foldr: + - before: + op = require "std.operator" + l = List {10000, 100} + + - context as a module function: + - before: + f = M.foldr + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {op.sum, 1, {10}})). + to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {op.sum, 1, {10}})). + not_to_contain_error "was deprecated" + + - context with a table: + - it works with an empty table: + expect (f (op.sum, 10000, {})).to_be (10000) + - it folds a binary function through a table: + expect (f (op.sum, 10000, {1, 10, 100})).to_be (10111) + - it folds from right to left: + expect (f (op.quot, 10, {10000, 100})).to_be (10000 / (100 / 10)) + + - context with a List: + - it works with an empty List: + expect (f (op.sum, 10000, List {})).to_be (10000) + - it folds a binary function through a List: + expect (f (op.sum, 10000, List {1, 10, 100})). + to_be (10111) + - it folds from right to left: + expect (f (op.quot, 10, List {10000, 100})). + to_be (10000 / (100 / 10)) + + - context as an object method: + - before: + f = l.foldr + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {l, op.sum, 1})). + to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {l, op.sum, 1})). + not_to_contain_error "was deprecated" + + - it works with an empty List: + l = List {} + expect (f (l, op.sum, 10)).to_be (10) + - it folds a binary function through a List: + expect (f (l, op.sum, 10)).to_be (10110) + - it folds from right to left: + expect (f (l, op.quot, 10)).to_be (10000 / (100 / 10)) + + +- describe index_key: + - context as a module function: + - before: + f = M.index_key + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {1, List {{1}}})). + to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {1, List {{1}}})). + not_to_contain_error "was deprecated" + + - it makes a map of matched table field values to table List offsets: + l = List {{a = "b", c = "d"}, {e = "x", f = "g"}, {a = "x"}} + t = f ("a", l) + expect (t).to_equal {b = 1, x = 3} + for k, v in pairs (t) do + expect (k).to_equal (l[v]["a"]) + end + - it captures only the last matching List offset: + l = List {{a = "b"}, {a = "x"}, {a = "b"}} + t = f ("a", l) + expect (t.b).not_to_be (1) + expect (t.x).to_be (2) + expect (t.b).to_be (3) + - it produces incomplete indices when faced with repeated matching table values: + l = List {{1, 2, 3}, {2}, {2, 1, 3, 2, 1}} + expect (f (1, l)).to_equal {1, 3} + expect (f (2, l)).to_equal {3, 1} + expect (f (3, l)).to_equal {nil, nil, 3} + + - context as an object method: + - before: + f = l.index_key + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {l, 1})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {l, 1})).not_to_contain_error "was deprecated" + + - it makes a map of matched table field values to table List offsets: + l = List {{a = "b", c = "d"}, {e = "x", f = "g"}, {a = "x"}} + t = l:index_key "a" + expect (t).to_equal {b = 1, x = 3} + for k, v in pairs (t) do + expect (k).to_equal (l[v]["a"]) + end + - it captures only the last matching List offset: + l = List {{a = "b"}, {a = "x"}, {a = "b"}} + t = l:index_key "a" + expect (t.b).not_to_be (1) + expect (t.x).to_be (2) + expect (t.b).to_be (3) + - it produces incomplete indices when faced with repeated matching table values: + l = List {{1, 2, 3}, {2}, {2, 1, 3, 2, 1}} + expect (l:index_key (1)).to_equal {1, 3} + expect (l:index_key (2)).to_equal {3, 1} + expect (l:index_key (3)).to_equal {nil, nil, 3} + + +- describe index_value: + - context as a module function: + - before: + f = M.index_value + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {1, List {{1}}})). + to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {1, List {{1}}})). + not_to_contain_error "was deprecated" + + - it makes a table of matched table field values to table List references: + l = List {{a = "b", c = "d"}, {e = "x", f = "g"}, {a = "x"}} + t = f ("a", l) + expect (t).to_equal {b = l[1], x = l[3]} + for k, v in pairs (t) do + expect (k).to_equal (v["a"]) + end + - it captures only the last matching List offset: + l = List {{a = "b"}, {a = "x"}, {a = "b"}} + t = f ("a", l) + expect (t.b).not_to_be (l[1]) + expect (t.x).to_be (l[2]) + expect (t.b).to_be (l[3]) + - it produces incomplete indices when faced with repeated matching table values: + l = List {{1, 2, 3}, {2}, {2, 1, 3, 2, 1}} + expect (f (1, l)).to_equal {l[1], l[3]} + expect (f (2, l)).to_equal {l[3], l[1]} + expect (f (3, l)).to_equal {nil, nil, l[3]} + + - context as an object method: + - before: + l = List {{1}} + + f = l.index_value + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {l, 1})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {l, 1})).not_to_contain_error "was deprecated" + + - it makes a table of matched table field values to table List references: + l = List {{a = "b", c = "d"}, {e = "x", f = "g"}, {a = "x"}} + t = l:index_value "a" + expect (t).to_equal {b = l[1], x = l[3]} + for k, v in pairs (t) do + expect (k).to_equal (v["a"]) + end + - it captures only the last matching List offset: + l = List {{a = "b"}, {a = "x"}, {a = "b"}} + t = l:index_value "a" + expect (t.b).not_to_be (l[1]) + expect (t.x).to_be (l[2]) + expect (t.b).to_be (l[3]) + - it produces incomplete indices when faced with repeated matching table values: + l = List {{1, 2, 3}, {2}, {2, 1, 3, 2, 1}} + expect (l:index_value (1)).to_equal {l[1], l[3]} + expect (l:index_value (2)).to_equal {l[3], l[1]} + expect (l:index_value (3)).to_equal {nil, nil, l[3]} + + +- describe map: + - before: + l = List {1, 2, 3, 4, 5} + sq = function (n) return n * n end + + - context as a module function: + - before: + f, badarg = init (M, this_module, "map") + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {sq, l})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {sq, l})).not_to_contain_error "was deprecated" + + - it returns a List object: + expect (prototype (f (sq, l))).to_be "List" + - it works for an empty List: + expect (f (sq, List {})).to_equal (List {}) + - it creates a new List: + o = l + m = f (sq, l) + expect (l).to_equal (o) + expect (m).not_to_equal (o) + expect (l).to_equal (List {1, 2, 3, 4, 5}) + - it maps a function over a List: + expect (f (sq, l)).to_equal (List {1, 4, 9, 16, 25}) + + - context as an object method: + - before: + f = l.map + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {l, sq})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {l, sq})).not_to_contain_error "was deprecated" + + - it returns a List object: + m = f (l, sq) + expect (prototype (m)).to_be "List" + - it works for an empty List: + expect (f (List {}, sq)).to_equal (List {}) + - it creates a new List: + o = l + m = f (l, sq) + expect (l).to_equal (o) + expect (m).not_to_equal (o) + expect (l).to_equal (List {1, 2, 3, 4, 5}) + - it maps a function over a List: + expect (f (l, sq)).to_equal (List {1, 4, 9, 16, 25}) + + +- describe map_with: + - before: + l = List {List {1, 2, 3}, List {4, 5}} + fn = function (...) return select ("#", ...) end + + - context as a module function: + - before: + f = M.map_with + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {fn, l})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {fn, l})).not_to_contain_error "was deprecated" + + - it returns a List object: + m = f (fn, l) + expect (prototype (m)).to_be "List" + - it creates a new List: + o = l + m = f (fn, l) + expect (l).to_equal (o) + expect (m).not_to_equal (o) + expect (l).to_equal (List {List {1, 2, 3}, List {4, 5}}) + - it maps a function over a List: + expect (f (fn, l)).to_equal (List {3, 2}) + - it works for an empty List: + l = List {} + expect (f (fn, l)).to_equal (List {}) + + - context as an object method: + - before: + f = l.map_with + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {l, fn})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {l, fn})).not_to_contain_error "was deprecated" + + - it returns a List object: + m = f (l, fn) + expect (prototype (m)).to_be "List" + - it creates a new List: + o = l + m = f (l, fn) + expect (l).to_equal (o) + expect (m).not_to_equal (o) + expect (l).to_equal (List {List {1, 2, 3}, List {4, 5}}) + - it maps a function over a List: + expect (f (l, fn)).to_equal (List {3, 2}) + - it works for an empty List: + l = List {} + expect (f (l, fn)).to_equal (List {}) + + +- describe project: + - before: + l = List { + {first = false, second = true, third = true}, + {first = 1, second = 2, third = 3}, + {first = "1st", second = "2nd", third = "3rd"}, + } + + - context as a module function: + - before: + f = M.project + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {"third", l})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {"third", l})).not_to_contain_error "was deprecated" + + - it returns a List object: + expect (prototype (f ("third", l))).to_be "List" + - it works with an empty List: + expect (f ("third", List {})).to_equal (List {}) + - it projects a List of fields from a List of tables: + expect (f ("third", l)).to_equal (List {true, 3, "3rd"}) + - it projects fields with a falsey value correctly: + expect (f ("first", l)).to_equal (List {false, 1, "1st"}) + + - context as an object method: + - before: + f = l.project + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {l, "third"})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {l, "third"})).not_to_contain_error "was deprecated" + + - it returns a List object: + expect (prototype (f (l, "third"))).to_be "List" + - it works with an empty List: + expect (f (List {}, "third")).to_equal (List {}) + - it projects a List of fields from a List of tables: + expect (f (l, "third")).to_equal (List {true, 3, "3rd"}) + - it projects fields with a falsey value correctly: + expect (f (l, "first")).to_equal (List {false, 1, "1st"}) + + +- describe relems: + - context as a module function: + - before: + f = M.relems + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {l})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {l})).not_to_contain_error "was deprecated" + + - it is a reverse iterator over List members: + t = {} + for e in f (l) do table.insert (t, e) end + expect (t).to_equal {"baz", "bar", "foo"} + - it works for an empty List: + t = {} + for e in f (List {}) do table.insert (t, e) end + expect (t).to_equal {} + + - context as an object method: + - before: + f = l.relems + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {l})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {l})).not_to_contain_error "was deprecated" + + - it is a reverse iterator over List members: + t = {} + for e in l:relems () do table.insert (t, e) end + expect (t).to_equal {"baz", "bar", "foo"} + - it works for an empty List: + t, l = {}, List {} + for e in l:relems () do table.insert (t, e) end + expect (t).to_equal {} + + +- describe rep: + - before: + l = List {"foo", "bar"} + + f = M.rep + + - context with bad arguments: + badargs.diagnose (f, "std.list.rep (List, int)") + + - context as a module function: + - it returns a List object: + expect (prototype (f (l, 3))).to_be "List" + - it works for an empty List: + expect (f (List {}, 99)).to_equal (List {}) + - it repeats the contents of a List: + expect (f (l, 3)). + to_equal (List {"foo", "bar", "foo", "bar", "foo", "bar"}) + + - context as an object method: + - before: + f = l.rep + + - it returns a List object: + expect (prototype (f (l, 3))).to_be "List" + - it works for an empty List: + expect (f (List {}, 99)).to_equal (List {}) + - it repeats the contents of a List: + expect (f (l, 3)). + to_equal (List {"foo", "bar", "foo", "bar", "foo", "bar"}) + + +- describe reverse: + - before: + l = List {"foo", "bar", "baz", "quux"} + + - context as a module function: + - before: + f = M.reverse + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {{}})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {{}})).not_to_contain_error "was deprecated" + + - it returns a List object: + expect (prototype (f (l))).to_be "List" + - it works for an empty List: + l = List {} + expect (f (l)).to_equal (List {}) + - it makes a new reversed List: + m = l + expect (f (l)). + to_equal (List {"quux", "baz", "bar", "foo"}) + expect (l).to_equal (List {"foo", "bar", "baz", "quux"}) + expect (l).to_be (m) + + - context as an object method: + - before: + f = l.reverse + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {l})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {l})).not_to_contain_error "was deprecated" + + - it returns a List object: + expect (prototype (f (l))).to_be "List" + - it works for an empty List: + expect (f (List {})).to_equal (List {}) + - it makes a new reversed List: + m = l + expect (f (l)). + to_equal (List {"quux", "baz", "bar", "foo"}) + expect (l).to_equal (List {"foo", "bar", "baz", "quux"}) + expect (l).to_be (m) + + +- describe shape: + - before: + l = List {1, 2, 3, 4, 5, 6} + + - context as a module function: + - before: + f = M.shape + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {{0}, l})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {{0}, l})).not_to_contain_error "was deprecated" + + - it returns a List object: + expect (prototype (f ({2, 3}, l))).to_be "List" + - it works for an empty List: + expect (f ({0}, List {})).to_equal (List {}) + - it returns the result in a new List object: + expect (f ({2, 3}, l)).not_to_be (l) + - it does not perturb the argument List: + f ({2, 3}, l) + expect (l).to_equal (List {1, 2, 3, 4, 5, 6}) + - it reshapes a List according to given dimensions: + expect (f ({2, 3}, l)). + to_equal (List {List {1, 2, 3}, List {4, 5, 6}}) + expect (f ({3, 2}, l)). + to_equal (List {List {1, 2}, List {3, 4}, List {5, 6}}) + - it treats 0-valued dimensions as an indefinite number: + expect (f ({2, 0}, l)). + to_equal (List {List {1, 2, 3}, List {4, 5, 6}}) + expect (f ({0, 2}, l)). + to_equal (List {List {1, 2}, List {3, 4}, List {5, 6}}) + + - context as an object method: + - before: + f = l.shape + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {l, {0}})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {l, {0}})).not_to_contain_error "was deprecated" + + - it returns a List object: + expect (prototype (f (l, {2, 3}))).to_be "List" + - it works for an empty List: + expect (f (List {}, {0})).to_equal (List {}) + - it returns the result in a new List object: + expect (f (l, {2, 3})):not_to_be (l) + - it does not perturb the argument List: + f (l, {2, 3}) + expect (l).to_equal (List {1, 2, 3, 4, 5, 6}) + - it reshapes a List according to given dimensions: + expect (f (l, {2, 3})). + to_equal (List {List {1, 2, 3}, List {4, 5, 6}}) + expect (f (l, {3, 2})). + to_equal (List {List {1, 2}, List {3, 4}, List {5, 6}}) + - it treats 0-valued dimensions as an indefinite number: + expect (f (l, {2, 0})). + to_equal (List {List {1, 2, 3}, List {4, 5, 6}}) + expect (f (l, {0, 2})). + to_equal (List {List {1, 2}, List {3, 4}, List {5, 6}}) + + +- describe sub: + - before: + l = List {1, 2, 3, 4, 5, 6, 7} + + f = M.sub + + - context with bad arguments: + badargs.diagnose (f, "std.list.sub (List, ?int, ?int)") + + - context as a module function: + - it returns a List object: + expect (prototype (f (l, 1, 1))).to_be "List" + - it makes a List from a subrange of another List: + expect (f (l, 2, 5)).to_equal (List {2, 3, 4, 5}) + - it truncates the result if 'to' argument is too large: + expect (f (l, 5, 10)).to_equal (List {5, 6, 7}) + - it defaults 'to' to the end of the List: + expect (f (l, 5)).to_equal (List {5, 6, 7}) + - it defaults 'from' to the beginning of the List: + expect (f (l)).to_equal (l) + - it returns an empty List when 'from' is greater than 'to': + expect (f (l, 2, 1)).to_equal (List {}) + - it counts from the end of the List for a negative 'from' argument: + expect (f (l, -3)).to_equal (List {5, 6, 7}) + - it counts from the end of the List for a negative 'to' argument: + expect (f (l, -5, -2)).to_equal (List {3, 4, 5, 6}) + + - context as an object method: + - before: + f = l.sub + + - it returns a List object: + expect (prototype (f (l, 1, 1))).to_be "List" + - it makes a List from a subrange of another List: + expect (f (l, 2, 5)).to_equal (List {2, 3, 4, 5}) + - it truncates the result if 'to' argument is too large: + expect (f (l, 5, 10)).to_equal (List {5, 6, 7}) + - it defaults 'to' to the end of the List: + expect (f (l, 5)).to_equal (List {5, 6, 7}) + - it defaults 'from' to the beginning of the List: + expect (f (l)).to_equal (l) + - it returns an empty List when 'from' is greater than 'to': + expect (f (l, 2, 1)).to_equal (List {}) + - it counts from the end of the List for a negative 'from' argument: + expect (f (l, -3)).to_equal (List {5, 6, 7}) + - it counts from the end of the List for a negative 'to' argument: + expect (f (l, -5, -2)).to_equal (List {3, 4, 5, 6}) + + +- describe tail: + - before: + l = List {1, 2, 3, 4, 5, 6, 7} + + f = M.tail + + - context with bad arguments: + badargs.diagnose (f, "std.list.tail (List)") + + - context as a module function: + - it returns a List object: + expect (prototype (f (l))).to_be "List" + - it makes a new List with the first element removed: + expect (f (l)).to_equal (List {2, 3, 4, 5, 6, 7}) + - it works for an empty List: + expect (f (List {})).to_equal (List {}) + - it returns an empty List when passed a List with one element: + expect (f (List {1})).to_equal (List {}) + + - context as an object method: + - before: + f = l.tail + + - it returns a List object: + expect (prototype (f (l))).to_be "List" + - it makes a new List with the first element removed: + expect (f (l)).to_equal (List {2, 3, 4, 5, 6, 7}) + - it works for an empty List: + expect (f (List {})).to_equal (List {}) + - it returns an empty List when passed a List with one element: + expect (f (List {1})).to_equal (List {}) + + +- describe transpose: + - before: + l = List {List {1, 2}, List {3, 4}, List {5, 6}} + + - context as a module function: + - before: + f = M.transpose + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {l})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {l})).not_to_contain_error "was deprecated" + + - it returns a List object: + expect (prototype (f (l))).to_be "List" + - it works for an empty List: + expect (f (List {})).to_equal (List {}) + - it returns the result in a new List object: + expect (f (l)).not_to_be (l) + - it does not perturb the argument List: + m = f (l) + expect (l).to_equal (List {List {1, 2}, List {3, 4}, List {5, 6}}) + - it transposes rows and columns: + expect (f (l)).to_equal (List {List {1, 3, 5}, List {2, 4, 6}}) + + - context as an object method: + - before: + f = l.transpose + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {l})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {l})).not_to_contain_error "was deprecated" + + - it returns a List object: + expect (prototype (f (l))).to_be "List" + - it works for an empty List: + expect (f (List {})).to_equal (List {}) + - it returns the result in a new List object: + expect (f (l)).not_to_be (l) + - it does not perturb the argument List: + m = f (l) + expect (l).to_equal (List {List {1, 2}, List {3, 4}, List {5, 6}}) + - it transposes rows and columns: + expect (f (l)). + to_equal (List {List {1, 3, 5}, List {2, 4, 6}}) + + +- describe zip_with: + - before: + l = List {List {1, 2}, List {3, 4}, List {5}} + fn = function (...) return tonumber (table.concat {...}) end + + - context as a module function: + - before: + f = M.zip_with + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {l, fn})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {l, fn})).not_to_contain_error "was deprecated" + + - it returns a List object: + expect (prototype (f (l, fn))).to_be "List" + - it works for an empty List: + expect (f (List {}, fn)).to_equal (List {}) + - it returns the result in a new List object: + expect (f (l, fn)):not_to_be (l) + - it does not perturb the argument List: + m = f (l, fn) + expect (l).to_equal (List {List {1, 2}, List {3, 4}, List {5}}) + - it combines column entries with a function: + expect (f (l, fn)).to_equal (List {135, 24}) + + - context as an object method: + - before: + f = l.zip_with + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {l, fn})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {l, fn})).not_to_contain_error "was deprecated" + + - it returns a List object: + expect (prototype (f (l, fn))).to_be "List" + - it works for an empty List: + expect (f (List {}, fn)).to_equal (List {}) + - it returns the result in a new List object: + expect (f (l, fn)):not_to_be (l) + - it does not perturb the argument List: + m = f (l, fn) + expect (l).to_equal (List {List {1, 2}, List {3, 4}, List {5}}) + - it combines column entries with a function: + expect (f (l, fn)).to_equal (List {135, 24}) diff --git a/spec/math_spec.yaml b/spec/math_spec.yaml new file mode 100644 index 0000000..f198877 --- /dev/null +++ b/spec/math_spec.yaml @@ -0,0 +1,101 @@ +before: + base_module = "math" + this_module = "std.math" + global_table = "_G" + + extend_base = { "floor", "monkey_patch", "round" } + + M = require (this_module) + + +specify std.math: +- context when required: + - context by name: + - it does not touch the global table: + expect (show_apis {added_to=global_table, by=this_module}). + to_equal {} + - it does not touch the core math table: + expect (show_apis {added_to=base_module, by=this_module}). + to_equal {} + - it contains apis from the core math table: + expect (show_apis {from=base_module, not_in=this_module}). + to_contain.a_permutation_of (extend_base) + + - context via the std module: + - it does not touch the global table: + expect (show_apis {added_to=global_table, by="std"}). + to_equal {} + - it does not touch the core math table: + expect (show_apis {added_to=base_module, by="std"}). + to_equal {} + + +- describe floor: + - before: + f = M.floor + + - context with bad arguments: + badargs.diagnose (f, "std.math.floor (number, ?int)") + + - it rounds to the nearest smaller integer: + expect (f (1.2)).to_be (1) + expect (f (1.9)).to_be (1) + expect (f (999e-2)).to_be (9) + expect (f (999e-3)).to_be (0) + - it rounds down to specified number of decimal places: + expect (f (1.2345, 0)).to_be (1.0) + expect (f (1.2345, 1)).to_be (1.2) + expect (f (1.2345, 2)).to_be (1.23) + expect (f (9.9999, 2)).to_be (9.99) + expect (f (99999e-3, 3)).to_be (99999e-3) + expect (f (99999e-4, 3)).to_be (9999e-3) + expect (f (99999e-5, 3)).to_be (999e-3) + + +- describe monkey_patch: + - before: + f = M.monkey_patch + + - context with bad arguments: + badargs.diagnose (f, "std.math.monkey_patch (?table)") + + # Ideally, `.to_be (M)`, except that M is cloned from a nested context + # by Specl to prevent us from affecting any other examples, thus the + # address is different by now. + - it returns std.math module table: + expect (f {}).to_equal (M) + - it injects std.math apis into the given namespace: + namespace = {} + f (namespace) + for _, api in ipairs (extend_base) do + expect (namespace.math[api]).to_be (M[api]) + end + + +- describe round: + - before: + f = M.round + + - context with bad arguments: + badargs.diagnose (f, "std.math.round (number, ?int)") + + - it rounds to the nearest integer: + expect (f (1.2)).to_be (1) + expect (f (1.9)).to_be (2) + expect (f (949e-2)).to_be (9) + expect (f (999e-2)).to_be (10) + - it rounds to specified number of decimal places: + expect (f (1.234, 0)).to_be (1.0) + expect (f (5.678, 0)).to_be (6.0) + expect (f (1.234, 1)).to_be (1.2) + expect (f (5.678, 1)).to_be (5.7) + expect (f (1.234, 2)).to_be (1.23) + expect (f (5.678, 2)).to_be (5.68) + expect (f (9.999, 2)).to_be (10) + expect (f (11111e-2, 3)).to_be (11111e-2) + expect (f (99999e-2, 3)).to_be (99999e-2) + expect (f (11111e-3, 3)).to_be (11111e-3) + expect (f (99999e-3, 3)).to_be (99999e-3) + expect (f (11111e-4, 3)).to_be (1111e-3) + expect (f (99999e-4, 3)).to_be (10) + expect (f (99999e-5, 3)).to_be (1) diff --git a/spec/object_spec.yaml b/spec/object_spec.yaml new file mode 100644 index 0000000..a4d6603 --- /dev/null +++ b/spec/object_spec.yaml @@ -0,0 +1,303 @@ +before: + Object = require "std.object" + obj = Object {"foo", "bar", baz="quux"} + prototype = Object.prototype + + function copy (t) + local r = {} + for k, v in pairs (t) do r[k] = v end + return r + end + +specify std.object: +- context when required: + - context by name: + - it does not touch the global table: + expect (show_apis {added_to="_G", by="std.object"}). + to_equal {} + +- describe construction: + - context from Object clone method: + - it constructs a new object: + obj = Object:clone {} + expect (obj).not_to_be (Object) + expect (type (obj)).to_be "table" + expect (prototype (obj)).to_be "Object" + - it reuses the Object metatable: + o = obj:clone {"o"} + p = o:clone {"p"} + expect (p).not_to_be (o) + expect (getmetatable (o)).to_be (getmetatable (p)) + - it sets object fields from arguments: + expect (obj:clone {}).to_copy (obj) + - it serves as a prototype for new instances: + o = obj:clone {} + expect (prototype (o)).to_be "Object" + expect (o).to_copy (obj) + expect (getmetatable (o)).to_be (getmetatable (obj)) + - it separates '_' prefixed fields: + expect (Object:clone {foo="bar", _baz="quux"}). + to_equal (Object:clone {foo="bar"}) + - it puts '_' prefixed fields in a new metatable: + obj = Object:clone {foo="bar", _baz="quux"} + expect (getmetatable (obj)).not_to_be (getmetatable (Object)) + expect (getmetatable (obj)._baz).to_be "quux" + +- describe prototype: + - before: o = Object {} + + - context when called from the object module: + - it reports the prototype stored in the object's metatable: + expect (prototype (o)).to_be "Object" + - it reports the type of a cloned object: + expect (prototype (o {})).to_be "Object" + - it reports the type of a derived object: + Example = Object {_type = "Example"} + expect (prototype (Example)).to_be "Example" + - it reports the type of a cloned derived object: + Portal = Object {_type = "Demon"} + p = Portal {} + expect (prototype (p)).to_be "Demon" + expect (prototype (p {})).to_be "Demon" + - it recognizes a file object: + h = io.open (os.tmpname ()) + expect (prototype (h)).to_be "file" + h:close () + expect (prototype (h)).to_be "closed file" + - it recognizes a primitive object: + expect (prototype (nil)).to_be "nil" + expect (prototype (false)).to_be "boolean" + expect (prototype (0.0)).to_be "number" + expect (prototype "0.0").to_be "string" + expect (prototype (function () end)).to_be "function" + expect (prototype {}).to_be "table" + - context when called as an object method: + - it reports the type stored in the object's metatable: + expect (o:prototype ()).to_be "Object" + - it reports the type of a cloned object: + expect ((o {}):prototype ()).to_be "Object" + - it reports the type of a subclassed object: + Example = Object {_type = "Example"} + expect (Example:prototype ()).to_be "Example" + - it reports the type of a cloned subclassed object: + Portal = Object {_type = "Demon"} + p = Portal {} + expect (p:prototype ()).to_be "Demon" + expect ((p {}):prototype ()).to_be "Demon" + - context backwards compatibility: + - it reports the prototype stored in the object's metatable: + expect (Object.type (o)).to_be "Object" + - it reports the type stored in the object's metatable: + expect (o:type ()).to_be "Object" + + +- describe instantiation from a prototype: + - context when _init is nil: + - before: + Array = Object { + _type = "Array", + "foo", "bar", "baz", + } + Array._init = nil + + - it contains user-defined fields: + expect (copy (Array)). + to_equal {"foo", "bar", "baz"} + - it sets array part of instance object from positional parameters: + array = Array {"first", "second", "third"} + expect (copy (array)). + to_equal {"first", "second", "third"} + - it uses prototype values for missing positional parameters: + array = Array {"first", "second"} + expect (copy (array)). + to_equal {"first", "second", "baz"} + - it merges surplas positional parameters: + array = Array {"first", "second", "third", "fourth"} + expect (copy (array)). + to_equal {"first", "second", "third", "fourth"} + + - context when _init is an empty table: + - before: + Prototype = Object { + _type = "Prototype"; + _init = {}, + "first", "second", "third", + } + - it contains user-defined fields: + expect (copy (Prototype)). + to_equal {"first", "second", "third"} + - it ignores positional parameters: | + instance = Prototype {"foo", "bar"} + expect (instance).to_copy (Prototype) + + - context when _init is a table of field names: + - before: + Process = Object { + _type = "Process", + _init = {"status", "output", "errout"}, + status = -1, + output = "empty", + errout = "no errors", + } + - it contains user-defined fields: + expect (copy (Process)). + to_equal {status = -1, output = "empty", errout = "no errors"} + - it sets user-defined fields from positional parameters: + proc = Process {0, "output", "diagnostics"} + expect (copy (proc)). + to_equal {status = 0, output = "output", errout = "diagnostics"} + - it uses prototype values for missing positional parameters: + proc = Process {0, "output"} + expect (copy (proc)). + to_equal {status = 0, output = "output", errout = "no errors"} + - it discards surplus positional parameters: + proc = Process {0, "output", "diagnostics", "garbage"} + expect (copy (proc)). + to_equal { status = 0, output = "output", errout = "diagnostics" } + + - context when _init is a function: + - before: + Prototype = Object { + _type = "Prototype", + f1 = "proto1", f2 = "proto2", + _init = function (self, ...) + self.args = unpack {...} + return self + end, + } + - it passes user defined fields to custom _init function: + instance = Prototype {"param1", "param2"} + expect ({instance.f1, instance.f2, instance.args}). + to_equal {"proto1", "proto2", {"param1", "param2"}} + +- describe field access: + - before: + Prototype = Object { + _type = "Prototype", + _init = { "field", "method"}, + field = "in prototype", + method = function (self, ...) + return prototype (self) .. " class, " .. + table.concat ({...}, ", ") + end, + } + instance = Prototype {"in object", function (self, ...) + return prototype (self) .. " instance, " .. + table.concat ({...}, ", ") + end, + } + + - it provides object field access with dot notation: + expect (instance.field).to_be "in object" + - it provides class field acces with dot notation: + expect (Prototype.field).to_be "in prototype" + - it provides object method acces with colon notation: + expect (instance:method "object method call"). + to_be "Prototype instance, object method call" + - it provides class method access with class dot notation: + expect (Prototype.method (instance, "class method call")). + to_be "Prototype class, class method call" + - it allows new instance fields to be added: + instance.newfield = "new" + expect (instance.newfield).to_be "new" + - it allows new instance methods to be added: + instance.newmethod = function (self) + return prototype (self) .. ", new instance method" + end + expect (instance:newmethod ()).to_be "Prototype, new instance method" + - it allows new class methods to be added: + Prototype.newmethod = function (self) + return prototype (self) .. ", new class method" + end + expect (Prototype.newmethod (instance)). + to_be "Prototype, new class method" + + +- describe object method propagation: + - context with no custom instance methods: + # :prototype is a method defined by the root object + - it inherits prototype object methods: + instance = Object {} + expect (instance:prototype ()).to_be "Object" + - it propagates prototype methods to derived instances: + Derived = Object {_type = "Derived"} + instance = Derived {} + expect (instance:prototype ()).to_be "Derived" + - context with custom object methods: + - before: + bag = Object { + _type = "bag", + __index = { + add = function (self, item) + self[item] = (self[item] or 0) + 1 + return self + end, + }, + } + # :prototype is a method defined by the root object + - it inherits prototype object methods: + expect (bag:prototype ()).to_be "bag" + - it propagates prototype methods to derived instances: + instance = bag {} + expect (instance:prototype ()).to_be "bag" + - it supports method calls: + expect (bag:add "foo").to_be (bag) + expect (bag.foo).to_be (1) + + +# Metatable propagation is an important property of Object cloning, +# because Lua will only call __lt and __le metamethods when both +# arguments share the same metatable - i.e. the previous behaviour +# of making each object its own metatable precluded ever being able +# to use __lt and __le! +- describe object metatable propagation: + - before: root_mt = getmetatable (Object) + + - context with no custom metamethods: + - it inherits prototype object metatable: + instance = Object {} + expect (getmetatable (instance)).to_be (root_mt) + - it propagates prototype metatable to derived instances: + Derived = Object {_type = "Derived"} + instance = Derived {} + expect (getmetatable (Derived)).not_to_be (root_mt) + expect (getmetatable (instance)).to_be (getmetatable (Derived)) + - context with custom metamethods: + - before: + bag = Object { + _type = "bag", + __lt = function (a, b) return a[1] < b[1] end, + } + - it has it's own metatable: + expect (getmetatable (bag)).not_to_be (root_mt) + - it propagates prototype metatable to derived instances: + instance = bag {} + expect (getmetatable (instance)).to_be (getmetatable (bag)) + - it supports __lt calls: | + a, b = bag {"a"}, bag {"b"} + expect (a < b).to_be (true) + expect (a < a).to_be (false) + expect (a > b).to_be (false) + + +- describe __tostring: + - before: + obj = Object {_type = "Derived", "one", "two", "three"} + - it returns a string: + expect (type (tostring (obj))).to_be "string" + - it contains the type: + expect (tostring (Object {})).to_contain "Object" + expect (tostring (obj)).to_contain (prototype (obj)) + - it contains the ordered array part elements: + expect (tostring (obj)).to_contain "one, two, three" + - it contains the ordered dictionary part elements: + expect (tostring (Object {one = true, two = true, three = true})). + to_contain "one=true, three=true, two=true" + expect (tostring (obj {one = true, two = true, three = true})). + to_contain "one=true, three=true, two=true" + - it contains a ';' separator only when object has array and dictionary parts: + expect (tostring (obj)).not_to_contain ";" + expect (tostring (Object {one = true, two = true, three = true})). + not_to_contain ";" + expect (tostring (obj {one = true, two = true, three = true})). + to_contain ";" diff --git a/spec/operator_spec.yaml b/spec/operator_spec.yaml new file mode 100644 index 0000000..1b66084 --- /dev/null +++ b/spec/operator_spec.yaml @@ -0,0 +1,227 @@ +before: | + this_module = "std.operator" + global_table = "_G" + + M = require (this_module) + +specify std.operator: +- context when required: + - context by name: + - it does not touch the global table: + expect (show_apis {added_to=global_table, by=this_module}). + to_equal {} + + - context via the std module: + - it does not touch the global table: + expect (show_apis {added_to=global_table, by="std"}). + to_equal {} + + +- describe concat: + - before: + f = M.concat + + - it stringifies its arguments: + expect (f (1, "")).to_be "1" + expect (f ("", 2)).to_be "2" + - it concatenates its arguments: + expect (f (1, 2)).to_be "12" + +- describe get: + - before: + f = M.get + + - it dereferences a table: + expect (f ({}, 1)).to_be (nil) + expect (f ({"foo", "bar"}, 1)).to_be "foo" + expect (f ({foo = "bar"}, "foo")).to_be "bar" + +- describe set: + - before: + f = M.set + + - it sets a table entry: + expect (f ({}, 1, 42)).to_equal {42} + expect (f ({}, "foo", 42)).to_equal {foo=42} + - it overwrites an existing entry: + expect (f ({1, 2}, 1, 42)).to_equal {42, 2} + expect (f ({foo="bar", baz="quux"}, "foo", 42)). + to_equal {foo=42, baz="quux"} + +- describe sum: + - before: + f = M.sum + + - it returns the sum of its arguments: + expect (f (99, 2)).to_be (99 + 2) + +- describe diff: + - before: + f = M.diff + + - it returns the difference of its arguments: + expect (f (99, 2)).to_be (99 - 2) + +- describe prod: + - before: + f = M.prod + + - it returns the product of its arguments: + expect (f (99, 2)).to_be (99 * 2) + +- describe quot: + - before: + f = M.quot + + - it returns the quotient of its arguments: + expect (f (99, 2)).to_be (99 / 2) + +- describe mod: + - before: + f = M.mod + + - it returns the modulus of its arguments: + expect (f (99, 2)).to_be (99 % 2) + +- describe pow: + - before: + f = M.pow + + - it returns the power of its arguments: + expect (f (99, 2)).to_be (99 ^ 2) + +- describe conj: + - before: + f = M.conj + + - it returns the logical and of its arguments: + expect (f (false, false)).to_be (false) + expect (f (false, true)).to_be (false) + expect (f (true, false)).to_be (false) + expect (f (true, true)).to_be (true) + - it supports truthy and falsey arguments: + expect (f ()).to_be (nil) + expect (f (0)).to_be (nil) + expect (f (nil, 0)).to_be (nil) + expect (f (0, "false")).to_be ("false") + +- describe disj: + - before: + f = M.disj + + - it returns the logical or of its arguments: + expect (f (false, false)).to_be (false) + expect (f (false, true)).to_be (true) + expect (f (true, false)).to_be (true) + expect (f (true, true)).to_be (true) + - it supports truthy and falsey arguments: + expect (f ()).to_be (nil) + expect (f (0)).to_be (0) + expect (f (nil, 0)).to_be (0) + expect (f (0, "false")).to_be (0) + +- describe neg: + - before: + f = M.neg + + - it returns the logical not of its argument: + expect (f (false)).to_be (true) + expect (f (true)).to_be (false) + - it supports truthy and falsey arguments: + expect (f ()).to_be (true) + expect (f (0)).to_be (false) + +- describe eq: + - before: + f = M.eq + + - it returns true if the arguments are equal: + expect (f ()).to_be (true) + expect (f ("foo", "foo")).to_be (true) + - it returns false if the arguments are unequal: + expect (f (1)).to_be (false) + expect (f ("foo", "bar")).to_be (false) + +- describe neq: + - before: + f = M.neq + + - it returns false if the arguments are equal: + expect (f (1, 1)).to_be (false) + expect (f ("foo", "foo")).to_be (false) + - it returns true if the arguments are unequal: + expect (f (1)).to_be (true) + expect (f ("foo", "bar")).to_be (true) + expect (f ({}, {})).to_be (true) + +- describe lt: + - before: + f = M.lt + + - it returns true if the arguments are in ascending order: + expect (f (1, 2)).to_be (true) + expect (f ("a", "b")).to_be (true) + - it returns false if the arguments are not in ascending order: + expect (f (2, 2)).to_be (false) + expect (f (3, 2)).to_be (false) + expect (f ("b", "b")).to_be (false) + expect (f ("c", "b")).to_be (false) + - it supports __lt metamethods: + List = require "std.list" {} + expect (f (List {1, 2, 3}, List {1, 2, 3, 4})).to_be (true) + expect (f (List {1, 2, 3}, List {1, 2, 3})).to_be (false) + expect (f (List {1, 2, 4}, List {1, 2, 3})).to_be (false) + +- describe lte: + - before: + f = M.lte + + - it returns true if the arguments are not in descending order: + expect (f (1, 2)).to_be (true) + expect (f (2, 2)).to_be (true) + expect (f ("a", "b")).to_be (true) + expect (f ("b", "b")).to_be (true) + - it returns false if the arguments are in descending order: + expect (f (3, 2)).to_be (false) + expect (f ("c", "b")).to_be (false) + - it supports __lte metamethods: + List = require "std.list" {} + expect (f (List {1, 2, 3}, List {1, 2, 3, 4})).to_be (true) + expect (f (List {1, 2, 3}, List {1, 2, 3})).to_be (true) + expect (f (List {1, 2, 4}, List {1, 2, 3})).to_be (false) + +- describe gt: + - before: + f = M.gt + + - it returns true if the arguments are in descending order: + expect (f (2, 1)).to_be (true) + expect (f ("b", "a")).to_be (true) + - it returns false if the arguments are not in descending order: + expect (f (2, 2)).to_be (false) + expect (f (2, 3)).to_be (false) + expect (f ("b", "b")).to_be (false) + expect (f ("b", "c")).to_be (false) + - it supports __lt metamethods: + List = require "std.list" {} + expect (f (List {1, 2, 3, 4}, List {1, 2, 3})).to_be (true) + expect (f (List {1, 2, 3}, List {1, 2, 3})).to_be (false) + expect (f (List {1, 2, 3}, List {1, 2, 4})).to_be (false) + +- describe gte: + - before: + f = M.gte + + - it returns true if the arguments are not in ascending order: + expect (f (2, 1)).to_be (true) + expect (f (2, 2)).to_be (true) + expect (f ("b", "a")).to_be (true) + expect (f ("b", "b")).to_be (true) + - it returns false if the arguments are in ascending order: + expect (f (2, 3)).to_be (false) + expect (f ("b", "c")).to_be (false) + - it supports __lte metamethods: + List = require "std.list" {} + expect (f (List {1, 2, 3, 4}, List {1, 2, 3})).to_be (true) + expect (f (List {1, 2, 3}, List {1, 2, 3})).to_be (true) + expect (f (List {1, 2, 3}, List {1, 2, 4})).to_be (false) diff --git a/spec/optparse_spec.yaml b/spec/optparse_spec.yaml new file mode 100644 index 0000000..70f874e --- /dev/null +++ b/spec/optparse_spec.yaml @@ -0,0 +1,461 @@ +before: + hell = require "specl.shell" + +specify std.optparse: +- before: | + OptionParser = require "std.optparse" + + help = [[ + parseme (stdlib spec) 0α1 + + Copyright © 2015 Gary V. Vaughan + This test program comes with ABSOLUTELY NO WARRANTY. + + Usage: parseme [] ... + + Banner text. + + Long description. + + Options: + + -h, --help display this help, then exit + --version display version information, then exit + -b a short option with no long option + --long a long option with no short option + --another-long a long option with internal hypen + --true a Lua keyword as an option name + -v, --verbose a combined short and long option + -n, --dryrun, --dry-run several spellings of the same option + -u, --name=USER require an argument + -o, --output=[FILE] accept an optional argument + -- end of options + + Footer text. + + Please report bugs at . + ]] + + -- strip off the leading whitespace required for YAML + parser = OptionParser (help:gsub ("^ ", "")) + +- context when required: + - context by name: + - it does not touch the global table: + expect (show_apis {added_to="_G", by="std.optparse"}). + to_equal {} + +- describe OptionParser: + - it recognises the program name: + expect (parser.program).to_be "parseme" + - it recognises the version number: + expect (parser.version).to_be "0α1" + - it recognises the version text: + expect (parser.versiontext). + to_match "^parseme .*Copyright .*NO WARRANTY%." + - it recognises the help text: | + expect (parser.helptext). + to_match ("^Usage: parseme .*Banner .*Long .*Options:.*" .. + "Footer .*/issues>%.") + - it diagnoses incorrect input text: + expect (OptionParser "garbage in").to_raise "argument must match" + +- describe parser: + - before: | + code = [[ + package.path = "]] .. package.path .. [[" + local OptionParser = require 'std.optparse' + local help = [=[]] .. help .. [[]=] + help = help:match ("^[%s\n]*(.-)[%s\n]*$") + + local parser = OptionParser (help) + local arg, opts = parser:parse (_G.arg) + + o = {} + for k, v in pairs (opts) do + table.insert (o, k .. " = " .. tostring (v)) + end + if #o > 0 then + table.sort (o) + print ("opts = { " .. table.concat (o, ", ") .. " }") + end + if #arg > 0 then + print ("args = { " .. table.concat (arg, ", ") .. " }") + end + ]] + parse = bind (luaproc, {code}) + + - it responds to --version with version text: + expect (parse {"--version"}). + to_match_output "^%s*parseme .*Copyright .*NO WARRANTY%.\n$" + - it responds to --help with help text: | + expect (parse {"--help"}). + to_match_output ("^%s*Usage: parseme .*Banner.*Long.*" .. + "Options:.*Footer.*/issues>%.\n$") + - it leaves behind unrecognised short options: + expect (parse {"-x"}).to_output "args = { -x }\n" + - it recognises short options: + expect (parse {"-b"}).to_output "opts = { b = true }\n" + - it leaves behind unrecognised options: + expect (parse {"--not-an-option"}). + to_output "args = { --not-an-option }\n" + - it recognises long options: + expect (parse {"--long"}).to_output "opts = { long = true }\n" + - it recognises long options with hyphens: + expect (parse {"--another-long"}). + to_output "opts = { another_long = true }\n" + - it recognises long options named after Lua keywords: + expect (parse {"--true"}).to_output "opts = { true = true }\n" + - it recognises combined short and long option specs: + expect (parse {"-v"}).to_output "opts = { verbose = true }\n" + expect (parse {"--verbose"}).to_output "opts = { verbose = true }\n" + - it recognises options with several spellings: + expect (parse {"-n"}).to_output "opts = { dry_run = true }\n" + expect (parse {"--dry-run"}).to_output "opts = { dry_run = true }\n" + expect (parse {"--dryrun"}).to_output "opts = { dry_run = true }\n" + - it recognises end of options marker: + expect (parse {"-- -n"}).to_output "args = { -n }\n" + - context given an unhandled long option: + - it leaves behind unmangled argument: + expect (parse {"--not-an-option=with-an-argument"}). + to_output "args = { --not-an-option=with-an-argument }\n" + - context given an option with a required argument: + - it records an argument to a long option following an '=' delimiter: + expect (parse {"--name=Gary"}). + to_output "opts = { name = Gary }\n" + - it records an argument to a short option without a space: + expect (parse {"-uGary"}). + to_output "opts = { name = Gary }\n" + - it records an argument to a long option following a space: + expect (parse {"--name Gary"}). + to_output "opts = { name = Gary }\n" + - it records an argument to a short option following a space: + expect (parse {"-u Gary"}). + to_output "opts = { name = Gary }\n" + - it diagnoses a missing argument: + expect (parse {"--name"}). + to_contain_error "'--name' requires an argument" + expect (parse {"-u"}). + to_contain_error "'-u' requires an argument" + - context given an option with an optional argument: + - it records an argument to a long option following an '=' delimiter: + expect (parse {"--output=filename"}). + to_output "opts = { output = filename }\n" + - it records an argument to a short option without a space: + expect (parse {"-ofilename"}). + to_output "opts = { output = filename }\n" + - it records an argument to a long option following a space: + expect (parse {"--output filename"}). + to_output "opts = { output = filename }\n" + - it records an argument to a short option following a space: + expect (parse {"-o filename"}). + to_output "opts = { output = filename }\n" + - it doesn't consume the following option: + expect (parse {"--output -v"}). + to_output "opts = { output = true, verbose = true }\n" + expect (parse {"-o -v"}). + to_output "opts = { output = true, verbose = true }\n" + - context when splitting combined short options: + - it separates non-argument options: + expect (parse {"-bn"}). + to_output "opts = { b = true, dry_run = true }\n" + expect (parse {"-vbn"}). + to_output "opts = { b = true, dry_run = true, verbose = true }\n" + - it stops separating at a required argument option: + expect (parse {"-vuname"}). + to_output "opts = { name = name, verbose = true }\n" + expect (parse {"-vuob"}). + to_output "opts = { name = ob, verbose = true }\n" + - it stops separating at an optional argument option: + expect (parse {"-vofilename"}). + to_output "opts = { output = filename, verbose = true }\n" + expect (parse {"-vobn"}). + to_output "opts = { output = bn, verbose = true }\n" + - it leaves behind unsplittable short options: + expect (parse {"-xvb"}).to_output "args = { -xvb }\n" + expect (parse {"-vxb"}).to_output "args = { -vxb }\n" + expect (parse {"-vbx"}).to_output "args = { -vbx }\n" + - it separates short options before unsplittable options: + expect (parse {"-vb -xvb"}). + to_output "opts = { b = true, verbose = true }\nargs = { -xvb }\n" + expect (parse {"-vb -vxb"}). + to_output "opts = { b = true, verbose = true }\nargs = { -vxb }\n" + expect (parse {"-vb -vbx"}). + to_output "opts = { b = true, verbose = true }\nargs = { -vbx }\n" + - it separates short options after unsplittable options: + expect (parse {"-xvb -vb"}). + to_output "opts = { b = true, verbose = true }\nargs = { -xvb }\n" + expect (parse {"-vxb -vb"}). + to_output "opts = { b = true, verbose = true }\nargs = { -vxb }\n" + expect (parse {"-vbx -vb"}). + to_output "opts = { b = true, verbose = true }\nargs = { -vbx }\n" + + - context with option defaults: + - before: | + function main (arg) + local OptionParser = require "std.optparse" + local parser = OptionParser ("program 0\nUsage: program\n" .. + " -x set x\n" .. + " -y set y\n" .. + " -z set z\n") + local state = { arg = {}, opts = { x={"t"}, y=false }} + state.arg, state.opts = parser:parse (arg, state.opts) + return state + end + - it prefers supplied argument: + expect (main {"-x", "-y"}). + to_equal { arg = {}, opts = { x=true, y=true }} + expect (main {"-x", "-y", "-z"}). + to_equal { arg = {}, opts = { x=true, y=true, z=true }} + expect (main {"-w", "-x", "-y"}). + to_equal { arg = {"-w"}, opts = { x=true, y=true }} + - it defers to default value: + expect (main {}). + to_equal { arg = {}, opts = { x={"t"}, y=false }} + expect (main {"-z"}). + to_equal { arg = {}, opts = { x={"t"}, y=false, z=true }} + expect (main {"-w"}). + to_equal { arg = {"-w"}, opts = { x={"t"}, y=false }} + + - context with io.die: + - before: | + runscript = function (code) + return luaproc ([[ + local OptionParser = require "std.optparse" + local parser = OptionParser ("program 0\nUsage: program\n") + _G.arg, _G.opts = parser:parse (_G.arg) + ]] .. code .. [[ + require "std.io".die "By 'eck!" + ]]) + end + - it prefers `prog.name` to `opts.program`: | + code = [[prog = { file = "file", name = "name" }]] + expect (runscript (code)).to_fail_while_matching ": name: By 'eck!\n" + - it prefers `prog.file` to `opts.program`: | + code = [[prog = { file = "file" }]] + expect (runscript (code)).to_fail_while_matching ": file: By 'eck!\n" + - it appends `prog.line` if any to `prog.file` over using `opts`: | + code = [[ + prog = { file = "file", line = 125 }; opts.line = 99]] + expect (runscript (code)). + to_fail_while_matching ": file:125: By 'eck!\n" + - it prefixes `opts.program` if any: | + expect (runscript ("")).to_fail_while_matching ": program: By 'eck!\n" + - it appends `opts.line` if any, to `opts.program`: | + code = [[opts.line = 99]] + expect (runscript (code)). + to_fail_while_matching ": program:99: By 'eck!\n" + + - context with io.warn: + - before: | + runscript = function (code) + return luaproc ([[ + local OptionParser = require "std.optparse" + local parser = OptionParser ("program 0\nUsage: program\n") + _G.arg, _G.opts = parser:parse (_G.arg) + ]] .. code .. [[ + require "std.io".warn "Ayup!" + ]]) + end + - it prefers `prog.name` to `opts.program`: | + code = [[prog = { file = "file", name = "name" }]] + expect (runscript (code)).to_output_error "name: Ayup!\n" + - it prefers `prog.file` to `opts.program`: | + code = [[prog = { file = "file" }]] + expect (runscript (code)).to_output_error "file: Ayup!\n" + - it appends `prog.line` if any to `prog.file` over using `opts`: | + code = [[ + prog = { file = "file", line = 125 }; opts.line = 99]] + expect (runscript (code)). + to_output_error "file:125: Ayup!\n" + - it prefixes `opts.program` if any: | + expect (runscript ("")).to_output_error "program: Ayup!\n" + - it appends `opts.line` if any, to `opts.program`: | + code = [[opts.line = 99]] + expect (runscript (code)). + to_output_error "program:99: Ayup!\n" + +- describe parser:on: + - before: | + function parseargs (onargstr, arglist) + code = [[ + package.path = "]] .. package.path .. [[" + local OptionParser = require 'std.optparse' + local help = [=[]] .. help .. [[]=] + help = help:match ("^[%s\n]*(.-)[%s\n]*$") + + local parser = OptionParser (help) + + parser:on (]] .. onargstr .. [[) + + _G.arg, _G.opts = parser:parse (_G.arg) + + o = {} + for k, v in pairs (opts) do + table.insert (o, k .. " = " .. tostring (v)) + end + if #o > 0 then + table.sort (o) + print ("opts = { " .. table.concat (o, ", ") .. " }") + end + if #arg > 0 then + print ("args = { " .. table.concat (arg, ", ") .. " }") + end + ]] + + return luaproc (code, arglist) + end + + - it recognises short options: + expect (parseargs ([["x"]], {"-x"})). + to_output "opts = { x = true }\n" + - it recognises long options: + expect (parseargs([["something"]], {"--something"})). + to_output "opts = { something = true }\n" + - it recognises long options with hyphens: + expect (parseargs([["some-thing"]], {"--some-thing"})). + to_output "opts = { some_thing = true }\n" + - it recognises long options named after Lua keywords: + expect (parseargs ([["if"]], {"--if"})). + to_output "opts = { if = true }\n" + - it recognises combined short and long option specs: + expect (parseargs ([[{"x", "if"}]], {"-x"})). + to_output "opts = { if = true }\n" + expect (parseargs ([[{"x", "if"}]], {"--if"})). + to_output "opts = { if = true }\n" + - it recognises options with several spellings: + expect (parseargs ([[{"x", "blah", "if"}]], {"-x"})). + to_output "opts = { if = true }\n" + expect (parseargs ([[{"x", "blah", "if"}]], {"--blah"})). + to_output "opts = { if = true }\n" + expect (parseargs ([[{"x", "blah", "if"}]], {"--if"})). + to_output "opts = { if = true }\n" + - it recognises end of options marker: + expect (parseargs ([["x"]], {"--", "-x"})). + to_output "args = { -x }\n" + - context given an option with a required argument: + - it records an argument to a short option without a space: + expect (parseargs ([["x", parser.required]], {"-y", "-xarg", "-b"})). + to_contain_output "opts = { b = true, x = arg }" + - it records an argument to a short option following a space: + expect (parseargs ([["x", parser.required]], {"-y", "-x", "arg", "-b"})). + to_contain_output "opts = { b = true, x = arg }\n" + - it records an argument to a long option following a space: + expect (parseargs ([["this", parser.required]], {"--this", "arg"})). + to_output "opts = { this = arg }\n" + - it records an argument to a long option following an '=' delimiter: + expect (parseargs ([["this", parser.required]], {"--this=arg"})). + to_output "opts = { this = arg }\n" + - it diagnoses a missing argument: + expect (parseargs ([[{"x", "this"}, parser.required]], {"-x"})). + to_contain_error "'-x' requires an argument" + expect (parseargs ([[{"x", "this"}, parser.required]], {"--this"})). + to_contain_error "'--this' requires an argument" + - context with a boolean handler function: + - it records a truthy argument: + for _, optarg in ipairs {"1", "TRUE", "true", "yes", "Yes", "y"} + do + expect (parseargs ([["x", parser.required, parser.boolean]], + {"-x", optarg})). + to_output "opts = { x = true }\n" + end + - it records a falsey argument: + for _, optarg in ipairs {"0", "FALSE", "false", "no", "No", "n"} + do + expect (parseargs ([["x", parser.required, parser.boolean]], + {"-x", optarg})). + to_output "opts = { x = false }\n" + end + - context with a file handler function: + - it records an existing file: + expect (parseargs ([["x", parser.required, parser.file]], + {"-x", "/dev/null"})). + to_output "opts = { x = /dev/null }\n" + - it diagnoses a missing file: | + expect (parseargs ([["x", parser.required, parser.file]], + {"-x", "/this/file/does/not/exist"})). + to_contain_error "error: /this/file/does/not/exist: " + - context with a custom handler function: + - it calls the handler: + expect (parseargs ([["x", parser.required, function (p,o,a) + return "custom" + end + ]], {"-x", "ignored"})). + to_output "opts = { x = custom }\n" + - it diagnoses a missing argument: + expect (parseargs ([["x", parser.required, function (p,o,a) + return "custom" + end + ]], {"-x"})). + to_contain_error "option '-x' requires an argument" + - context given an option with an optional argument: + - it records an argument to a short option without a space: + expect (parseargs ([["x", parser.optional]], {"-y", "-xarg", "-b"})). + to_contain_output "opts = { b = true, x = arg }" + - it records an argument to a short option following a space: + expect (parseargs ([["x", parser.optional]], {"-y", "-x", "arg", "-b"})). + to_contain_output "opts = { b = true, x = arg }\n" + - it records an argument to a long option following a space: + expect (parseargs ([["this", parser.optional]], {"--this", "arg"})). + to_output "opts = { this = arg }\n" + - it records an argument to a long option following an '=' delimiter: + expect (parseargs ([["this", parser.optional]], {"--this=arg"})). + to_output "opts = { this = arg }\n" + - it does't consume the following option: + expect (parseargs ([[{"x", "this"}, parser.optional]], {"-x", "-b"})). + to_output "opts = { b = true, this = true }\n" + expect (parseargs ([[{"x", "this"}, parser.optional]], {"--this", "-b"})). + to_output "opts = { b = true, this = true }\n" + - context with a boolean handler function: + - it records a truthy argument: + for _, optarg in ipairs {"1", "TRUE", "true", "yes", "Yes", "y"} + do + expect (parseargs ([["x", parser.optional, parser.boolean]], + {"-x", optarg})). + to_output "opts = { x = true }\n" + end + - it records a falsey argument: + for _, optarg in ipairs {"0", "FALSE", "false", "no", "No", "n"} + do + expect (parseargs ([["x", parser.optional, parser.boolean]], + {"-x", optarg})). + to_output "opts = { x = false }\n" + end + - it defaults to a truthy value: + expect (parseargs ([["x", parser.optional, parser.boolean]], + {"-x", "-b"})). + to_output "opts = { b = true, x = true }\n" + - context with a file handler function: + - it records an existing file: + expect (parseargs ([["x", parser.optional, parser.file]], + {"-x", "/dev/null"})). + to_output "opts = { x = /dev/null }\n" + - it diagnoses a missing file: | + expect (parseargs ([["x", parser.optional, parser.file]], + {"-x", "/this/file/does/not/exist"})). + to_contain_error "error: /this/file/does/not/exist: " + - context with a custom handler function: + - it calls the handler: + expect (parseargs ([["x", parser.optional, function (p,o,a) + return "custom" + end + ]], {"-x", "ignored"})). + to_output "opts = { x = custom }\n" + - it does not consume a following option: + expect (parseargs ([["x", parser.optional, function (p,o,a) + return a or "default" + end + ]], {"-x", "-b"})). + to_output "opts = { b = true, x = default }\n" + - context when splitting combined short options: + - it separates non-argument options: + expect (parseargs ([["x"]], {"-xb"})). + to_output "opts = { b = true, x = true }\n" + expect (parseargs ([["x"]], {"-vxb"})). + to_output "opts = { b = true, verbose = true, x = true }\n" + - it stops separating at a required argument option: + expect (parseargs ([[{"x", "this"}, parser.required]], {"-bxbit"})). + to_output "opts = { b = true, this = bit }\n" + - it stops separating at an optional argument option: + expect (parseargs ([[{"x", "this"}, parser.optional]], {"-bxbit"})). + to_output "opts = { b = true, this = bit }\n" diff --git a/spec/package_spec.yaml b/spec/package_spec.yaml new file mode 100644 index 0000000..170cd5b --- /dev/null +++ b/spec/package_spec.yaml @@ -0,0 +1,193 @@ +before: | + base_module = "package" + this_module = "std.package" + global_table = "_G" + + extend_base = { "dirsep", "execdir", "find", "igmark", "insert", + "mappath", "normalize", "pathsep", "path_mark", + "remove" } + + M = require (this_module) + + path = M.normalize ("begin", "middle", "end") + + function catfile (...) return table.concat ({...}, M.dirsep) end + function catpath (...) return table.concat ({...}, M.pathsep) end + + +specify std.package: +- context when required: + - context by name: + - it does not touch the global table: + expect (show_apis {added_to=global_table, by=this_module}). + to_equal {} + - it does not touch the core package table: + expect (show_apis {added_to=base_module, by=this_module}). + to_equal {} + - it contains apis from the core package table: + expect (show_apis {from=base_module, not_in=this_module}). + to_contain.a_permutation_of (extend_base) + + - context via the std module: + - it does not touch the global table: + expect (show_apis {added_to=global_table, by="std"}). + to_equal {} + - it does not touch the core package table: + expect (show_apis {added_to=base_module, by="std"}). + to_equal {} + + +- describe find: + - before: | + path = table.concat ({"begin", "m%ddl.", "end"}, M.pathsep) + + f = M.find + + - context with bad arguments: + badargs.diagnose (f, "std.package.find (string, string, ?int, ?boolean|:plain)") + + - it returns nil for unmatched element: + expect (f (path, "unmatchable")).to_be (nil) + - it returns the element index for a matched element: + expect (f (path, "end")).to_be (3) + - it returns the element text for a matched element: + i, element = f (path, "e.*n") + expect ({i, element}).to_equal {1, "begin"} + - it accepts a search start element argument: + i, element = f (path, "e.*n", 2) + expect ({i, element}).to_equal {3, "end"} + - it works with plain text search strings: + expect (f (path, "m%ddl.")).to_be (nil) + i, element = f (path, "%ddl.", 1, ":plain") + expect ({i, element}).to_equal {2, "m%ddl."} + + +- describe insert: + - before: + f = M.insert + + - context with bad arguments: + badargs.diagnose (f, "std.package.insert (string, [int], string)") + + - it appends by default: + expect (f (path, "new")). + to_be (M.normalize ("begin", "middle", "end", "new")) + - it prepends with pos set to 1: + expect (f (path, 1, "new")). + to_be (M.normalize ("new", "begin", "middle", "end")) + - it can insert in the middle too: + expect (f (path, 2, "new")). + to_be (M.normalize ("begin", "new", "middle", "end")) + expect (f (path, 3, "new")). + to_be (M.normalize ("begin", "middle", "new", "end")) + - it normalizes the returned path: + path = table.concat ({"begin", "middle", "end"}, M.pathsep) + expect (f (path, "new")). + to_be (M.normalize ("begin", "middle", "end", "new")) + expect (f (path, 1, "./x/../end")). + to_be (M.normalize ("end", "begin", "middle")) + + +- describe mappath: + - before: | + expected = require "std.string".split (path, M.pathsep) + + f = M.mappath + + - context with bad arguments: + badargs.diagnose (f, "std.package.mappath (string, function, ?any*)") + + - it calls a function with each path element: + t = {} + f (path, function (e) t[#t + 1] = e end) + expect (t).to_equal (expected) + - it passes additional arguments through: | + reversed = {} + for i = #expected, 1, -1 do + table.insert (reversed, expected[i]) + end + t = {} + f (path, function (e, pos) table.insert (t, pos, e) end, 1) + expect (t).to_equal (reversed) + + +- describe normalize: + - before: + f = M.normalize + + - context with bad arguments: + badargs.diagnose (f, "std.package.normalize (string*)") + + - context with a single element: + - it strips redundant . directories: + expect (f "./x/./y/.").to_be (catfile (".", "x", "y")) + - it strips redundant .. directories: + expect (f "../x/../y/z/..").to_be (catfile ("..", "y")) + expect (f "../x/../y/z/..").to_be (catfile ("..", "y")) + expect (f "../../x/../y/z/..").to_be (catfile ("..", "..", "y")) + expect (f "../../x/../y/./..").to_be (catfile ("..", "..")) + expect (f "../../w/x/../../y/z/..").to_be (catfile ("..", "..", "y")) + expect (f "../../w/./../.././z/..").to_be (catfile ("..", "..", "..")) + - it leaves leading .. directories unmolested: + expect (f "../../1").to_be (catfile ("..", "..", "1")) + expect (f "./../../1").to_be (catfile ("..", "..", "1")) + - it normalizes / to platform dirsep: + expect (f "/foo/bar").to_be (catfile ("", "foo", "bar")) + - it normalizes ? to platform path_mark: + expect (f "?.lua"). + to_be (catfile (".", M.path_mark .. ".lua")) + - it strips redundant trailing /: + expect (f "/foo/bar/").to_be (catfile ("", "foo", "bar")) + - it inserts missing ./ for relative paths: + for _, path in ipairs {"x", "./x"} do + expect (f (path)).to_be (catfile (".", "x")) + end + - context with multiple elements: + - it strips redundant . directories: + expect (f ("./x/./y/.", "x")). + to_be (catpath (catfile (".", "x", "y"), catfile (".", "x"))) + - it strips redundant .. directories: + expect (f ("../x/../y/z/..", "x")). + to_be (catpath (catfile ("..", "y"), catfile (".", "x"))) + - it normalizes / to platform dirsep: + expect (f ("/foo/bar", "x")). + to_be (catpath (catfile ("", "foo", "bar"), catfile (".", "x"))) + - it normalizes ? to platform path_mark: + expect (f ("?.lua", "x")). + to_be (catpath (catfile (".", M.path_mark .. ".lua"), catfile (".", "x"))) + - it strips redundant trailing /: + expect (f ("/foo/bar/", "x")). + to_be (catpath (catfile ("", "foo", "bar"), catfile (".", "x"))) + - it inserts missing ./ for relative paths: + for _, path in ipairs {"x", "./x"} do + expect (f (path, "a")). + to_be (catpath (catfile (".", "x"), catfile (".", "a"))) + end + - it eliminates all but the first equivalent elements: + expect (f (catpath ("1", "x", "2", "./x", "./2", "./x/../x"))). + to_be (catpath ("./1", "./x", "./2")) + + +- describe remove: + - before: + f = M.remove + + - context with bad arguments: + badargs.diagnose (f, "std.package.remove (string, ?int)") + + - it removes the last item by default: + expect (f (path)).to_be (M.normalize ("begin", "middle")) + - it pops the first item with pos set to 1: + expect (f (path, 1)).to_be (M.normalize ("middle", "end")) + - it can remove from the middle too: + expect (f (path, 2)).to_be (M.normalize ("begin", "end")) + - it does not normalize the returned path: + path = table.concat ({"begin", "middle", "end"}, M.pathsep) + expect (f (path)). + to_be (table.concat ({"begin", "middle"}, M.pathsep)) + + +- it splits package.config up: + expect (string.format ("%s\n%s\n%s\n%s\n%s\n", + M.dirsep, M.pathsep, M.path_mark, M.execdir, M.igmark) + ).to_contain (package.config) diff --git a/spec/set_spec.yaml b/spec/set_spec.yaml new file mode 100644 index 0000000..ac87faf --- /dev/null +++ b/spec/set_spec.yaml @@ -0,0 +1,292 @@ +before: + Set = require "std.set" + prototype = require "std.object".prototype + s = Set {"foo", "bar", "bar"} + +specify std.set: +- describe require: + - it does not perturb the global namespace: + expect (show_apis {added_to="_G", by="std.set"}). + to_equal {} + + +- describe construction: + - it constructs a new set: + s = Set {} + expect (s).not_to_be (Set) + expect (prototype (s)).to_be "Set" + - it initialises set with constructor parameters: + t = Set {"foo", "bar", "bar"} + expect (t).to_equal (s) + - it serves as a prototype for new instances: + obj = s {} + expect (prototype (obj)).to_be "Set" + expect (obj).to_equal (s) + expect (getmetatable (obj)).to_be (getmetatable (s)) + + +- describe delete: + - context when called as a Set module function: + - before: + s = Set {"foo", "bar", "baz"} + - it returns a set object: + expect (prototype (Set.delete (s, "foo"))).to_be "Set" + - it is destructive: + Set.delete (s, "bar") + expect (s).not_to_have_member "bar" + - it returns the modified set: + expect (Set.delete (s, "baz")).not_to_have_member "baz" + - it ignores removal of non-members: | + clone = s {} + expect (Set.delete (s, "quux")).to_equal (clone) + - it deletes a member from the set: + expect (s).to_have_member "bar" + Set.delete (s, "bar") + expect (s).not_to_have_member "bar" + - it works with an empty set: + expect (Set.delete (Set {}, "quux")).to_equal (Set {}) + + +- describe difference: + - before: + r = Set {"foo", "bar", "baz"} + s = Set {"bar", "baz", "quux"} + + - context when called as a Set module function: + - it returns a set object: + expect (prototype (Set.difference (r, s))).to_be "Set" + - it is non-destructive: + Set.difference (r, s) + expect (r).to_equal (Set {"foo", "bar", "baz"}) + expect (s).to_equal (Set {"bar", "baz", "quux"}) + - it returns a set containing members of the first that are not in the second: + expect (Set.difference (r, s)).to_equal (Set {"foo"}) + - context when called as a set metamethod: + - it returns a set object: + expect (prototype (r - s)).to_be "Set" + - it is non-destructive: + q = r - s + expect (r).to_equal (Set {"foo", "bar", "baz"}) + expect (s).to_equal (Set {"bar", "baz", "quux"}) + - it returns a set containing members of the first that are not in the second: + expect (r - s).to_equal (Set {"foo"}) + + +- describe elems: + - before: + s = Set {"foo", "bar", "baz"} + + - context when called as a Set module function: + - it is an iterator over set members: + t = {} + for e in Set.elems (s) do table.insert (t, e) end + table.sort (t) + expect (t).to_equal {"bar", "baz", "foo"} + - it works for an empty set: + t = {} + for e in Set.elems (Set {}) do table.insert (t, e) end + expect (t).to_equal {} + + +- describe insert: + - context when called as a Set module function: + - before: + s = Set {"foo"} + - it returns a set object: + expect (prototype (Set.insert (s, "bar"))).to_be "Set" + - it is destructive: + Set.insert (s, "bar") + expect (s).to_have_member "bar" + - it returns the modified set: + expect (Set.insert (s, "baz")).to_have_member "baz" + - it ignores insertion of existing members: + expect (Set.insert (s, "foo")).to_equal (Set {"foo"}) + - it inserts a new member into the set: + expect (s).not_to_have_member "bar" + Set.insert (s, "bar") + expect (s).to_have_member "bar" + - it works with an empty set: + expect (Set.insert (Set {}, "foo")).to_equal (s) + - context when called as a set metamethod: + - before: + s = Set {"foo"} + - it returns a set object: + s["bar"] = true + expect (prototype (s)).to_be "Set" + - it is destructive: + s["bar"] = true + expect (s).to_have_member "bar" + - it ignores insertion of existing members: + s["foo"] = true + expect (s).to_equal (Set {"foo"}) + - it inserts a new member into the set: + expect (s).not_to_have_member "bar" + s["bar"] = true + expect (s).to_have_member "bar" + - it works with an empty set: + s = Set {} + s.foo = true + expect (s).to_equal (s) + + +- describe intersection: + - before: + r = Set {"foo", "bar", "baz"} + s = Set {"bar", "baz", "quux"} + + - context when called as a Set module function: + - it returns a set object: + expect (prototype (Set.intersection (r, s))).to_be "Set" + - it is non-destructive: + Set.intersection (r, s) + expect (r).to_equal (Set {"foo", "bar", "baz"}) + expect (s).to_equal (Set {"bar", "baz", "quux"}) + - it returns a set containing members common to both arguments: + expect (Set.intersection (r, s)). + to_equal (Set {"bar", "baz"}) + - context when called as a set metamethod: + - it returns a set object: + q = r * s + expect (prototype (q)).to_be "Set" + - it is non-destructive: + q = r * s + expect (r).to_equal (Set {"foo", "bar", "baz"}) + expect (s).to_equal (Set {"bar", "baz", "quux"}) + - it returns a set containing members common to both arguments: + expect (r * s).to_equal (Set {"bar", "baz"}) + + +- describe member: + - before: s = Set {"foo", "bar"} + + - context when called as a Set module function: + - it succeeds when set contains the given member: + expect (Set.member (s, "foo")).to_be (true) + - it fails when set does not contain the given member: + expect (Set.member (s, "baz")).not_to_be (true) + - it works with the empty set: + s = Set {} + expect (Set.member (s, "foo")).not_to_be (true) + - context when called as a set metamethod: + - it succeeds when set contains the given member: + expect (s["foo"]).to_be (true) + - it fails when set does not contain the given member: + expect (s["baz"]).not_to_be (true) + - it works with the empty set: + s = Set {} + expect (s["foo"]).not_to_be (true) + + +- describe proper_subset: + - before: + r = Set {"foo", "bar", "baz"} + s = Set {"bar", "baz"} + + - context when called as a Set module function: + - it succeeds when set contains all elements of another: + expect (Set.proper_subset (s, r)).to_be (true) + - it fails when two sets are equal: + r = s {} + expect (Set.proper_subset (s, r)).to_be (false) + - it fails when set does not contain all elements of another: + s = s + Set {"quux"} + expect (Set.proper_subset (r, s)).to_be (false) + - context when called as a set metamethod: + - it succeeds when set contains all elements of another: + expect (s < r).to_be (true) + - it fails when two sets are equal: + r = s {} + expect (s < r).to_be (false) + - it fails when set does not contain all elements of another: + s = s + Set {"quux"} + expect (r < s).to_be (false) + + +- describe subset: + - before: + r = Set {"foo", "bar", "baz"} + s = Set {"bar", "baz"} + + - context when called as a Set module function: + - it succeeds when set contains all elements of another: + expect (Set.subset (s, r)).to_be (true) + - it succeeds when two sets are equal: + r = s {} + expect (Set.subset (s, r)).to_be (true) + - it fails when set does not contain all elements of another: + s = s + Set {"quux"} + expect (Set.subset (r, s)).to_be (false) + - context when called as a set metamethod: + - it succeeds when set contains all elements of another: + expect (s <= r).to_be (true) + - it succeeds when two sets are equal: + r = s {} + expect (s <= r).to_be (true) + - it fails when set does not contain all elements of another: + s = s + Set {"quux"} + expect (r <= s).to_be (false) + + +- describe symmetric_difference: + - before: + r = Set {"foo", "bar", "baz"} + s = Set {"bar", "baz", "quux"} + + - context when called as a Set module function: + - it returns a set object: + expect (prototype (Set.symmetric_difference (r, s))). + to_be "Set" + - it is non-destructive: + Set.symmetric_difference (r, s) + expect (r).to_equal (Set {"foo", "bar", "baz"}) + expect (s).to_equal (Set {"bar", "baz", "quux"}) + - it returns a set containing members in only one argument set: + expect (Set.symmetric_difference (r, s)). + to_equal (Set {"foo", "quux"}) + - context when called as a set metamethod: + - it returns a set object: + expect (prototype (r / s)).to_be "Set" + - it is non-destructive: + q = r / s + expect (r).to_equal (Set {"foo", "bar", "baz"}) + expect (s).to_equal (Set {"bar", "baz", "quux"}) + - it returns a set containing members in only one argument set: + expect (r / s).to_equal (Set {"foo", "quux"}) + + +- describe union: + - before: + r = Set {"foo", "bar", "baz"} + s = Set {"bar", "baz", "quux"} + + - context when called as a Set module function: + - it returns a set object: + expect (prototype (Set.union (r, s))).to_be "Set" + - it is non-destructive: + Set.union (r, s) + expect (r).to_equal (Set {"foo", "bar", "baz"}) + expect (s).to_equal (Set {"bar", "baz", "quux"}) + - it returns a set containing members in only one argument set: + expect (Set.union (r, s)). + to_equal (Set {"foo", "bar", "baz", "quux"}) + - context when called as a set metamethod: + - it returns a set object: + expect (prototype (r + s)).to_be "Set" + - it is non-destructive: + q = r + s + expect (r).to_equal (Set {"foo", "bar", "baz"}) + expect (s).to_equal (Set {"bar", "baz", "quux"}) + - it returns a set containing members in only one argument set: + expect (r + s).to_equal (Set {"foo", "bar", "baz", "quux"}) + + +- describe __tostring: + - before: + s = Set {"foo", "bar", "baz"} + + - it returns a string: + expect (type (tostring (s))).to_be "string" + - it shows the type name: + expect (tostring (s)).to_contain "Set" + - it contains the ordered set elements: + expect (tostring (s)).to_contain "bar, baz, foo" diff --git a/spec/spec_helper.lua b/spec/spec_helper.lua new file mode 100644 index 0000000..006657d --- /dev/null +++ b/spec/spec_helper.lua @@ -0,0 +1,307 @@ +local inprocess = require "specl.inprocess" +local hell = require "specl.shell" +local std = require "specl.std" + +badargs = require "specl.badargs" + +local top_srcdir = os.getenv "top_srcdir" or "." +local top_builddir = os.getenv "top_builddir" or "." + +package.path = std.package.normalize ( + top_builddir .. "/lib/?.lua", + top_builddir .. "/lib/?/init.lua", + top_srcdir .. "/lib/?.lua", + top_srcdir .. "/lib/?/init.lua", + package.path + ) + + +-- Allow user override of LUA binary used by hell.spawn, falling +-- back to environment PATH search for "lua" if nothing else works. +local LUA = os.getenv "LUA" or "lua" + + +-- Tweak _DEBUG without tripping over Specl nested environments. +setdebug = require "std.debug"._setdebug + + +-- Make sure we have a maxn even when _VERSION ~= 5.1 +-- @fixme remove this when we get unpack from specl.std +maxn = table.maxn or function (t) + local n = 0 + for k in pairs (t) do + if type (k) == "number" and k > n then n = k end + end + return n +end + + +-- Take care to always unpack upto the highest numeric index, for +-- consistency across Lua versions. +local _unpack = table.unpack or unpack + +-- @fixme pick this up from specl.std with the next release +function unpack (t, i, j) + return _unpack (t, i or 1, j or maxn (t)) +end + + +-- In case we're not using a bleeding edge release of Specl... +badargs.result = badargs.result or function (fname, i, want, got) + if want == nil then i, want = i - 1, i end -- numbers only for narg error + + if got == nil and type (want) == "number" then + local s = "bad result #%d from '%s' (no more than %d result%s expected, got %d)" + return s:format (i + 1, fname, i, i == 1 and "" or "s", want) + end + + local function showarg (s) + return ("|" .. s .. "|"): + gsub ("|%?", "|nil|"): + gsub ("|nil|", "|no value|"): + gsub ("|any|", "|any value|"): + gsub ("|#", "|non-empty "): + gsub ("|func|", "|function|"): + gsub ("|file|", "|FILE*|"): + gsub ("^|", ""): + gsub ("|$", ""): + gsub ("|([^|]+)$", "or %1"): + gsub ("|", ", ") + end + + return string.format ("bad result #%d from '%s' (%s expected, got %s)", + i, fname, showarg (want), got or "no value") +end + + +-- Wrap up badargs function in a succinct single call. +function init (M, mname, fname) + local name = (mname .. "." .. fname):gsub ("^%.", "") + return M[fname], + function (...) return badargs.format (name, ...) end, + function (...) return badargs.result (name, ...) end +end + + +-- A copy of base.lua:prototype, so that an unloadable base.lua doesn't +-- prevent everything else from working. +function prototype (o) + return (getmetatable (o) or {})._type or io.type (o) or type (o) +end + + +function nop () end + + +-- Error message specifications use this to shorten argument lists. +-- Copied from functional.lua to avoid breaking all tests if functional +-- cannot be loaded correctly. +function bind (f, fix) + return function (...) + local arg = {} + for i, v in pairs (fix) do + arg[i] = v + end + local i = 1 + for _, v in pairs {...} do + while arg[i] ~= nil do i = i + 1 end + arg[i] = v + end + return f (unpack (arg)) + end +end + + +local function mkscript (code) + local f = os.tmpname () + local h = io.open (f, "w") + h:write (code) + h:close () + return f +end + + +--- Run some Lua code with the given arguments and input. +-- @string code valid Lua code +-- @tparam[opt={}] string|table arg single argument, or table of +-- arguments for the script invocation. +-- @string[opt] stdin standard input contents for the script process +-- @treturn specl.shell.Process|nil status of resulting process if +-- execution was successful, otherwise nil +function luaproc (code, arg, stdin) + local f = mkscript (code) + if type (arg) ~= "table" then arg = {arg} end + local cmd = {LUA, f, unpack (arg)} + -- inject env and stdin keys separately to avoid truncating `...` in + -- cmd constructor + cmd.env = { LUA_PATH=package.path, LUA_INIT="", LUA_INIT_5_2="" } + cmd.stdin = stdin + local proc = hell.spawn (cmd) + os.remove (f) + return proc +end + + +--- Concatenate the contents of listed existing files. +-- @string ... names of existing files +-- @treturn string concatenated contents of those files +function concat_file_content (...) + local t = {} + for _, name in ipairs {...} do + local h = io.open (name) + t[#t + 1] = h:read "*a" + end + return table.concat (t) +end + + +local function tabulate_output (code) + local proc = luaproc (code) + if proc.status ~= 0 then return error (proc.errout) end + local r = {} + proc.output:gsub ("(%S*)[%s]*", + function (x) + if x ~= "" then r[x] = true end + end) + return r +end + + +--- Show changes to tables wrought by a require statement. +-- There are a few modes to this function, controlled by what named +-- arguments are given. Lists new keys in T1 after `require "import"`: +-- +-- show_apis {added_to=T1, by=import} +-- +-- List keys returned from `require "import"`, which have the same +-- value in T1: +-- +-- show_apis {from=T1, used_by=import} +-- +-- List keys from `require "import"`, which are also in T1 but with +-- a different value: +-- +-- show_apis {from=T1, enhanced_by=import} +-- +-- List keys from T2, which are also in T1 but with a different value: +-- +-- show_apis {from=T1, enhanced_in=T2} +-- +-- @tparam table argt one of the combinations above +-- @treturn table a list of keys according to criteria above +function show_apis (argt) + local added_to, from, not_in, enhanced_in, enhanced_after, by = + argt.added_to, argt.from, argt.not_in, argt.enhanced_in, + argt.enhanced_after, argt.by + + if added_to and by then + return tabulate_output ([[ + local before, after = {}, {} + for k in pairs (]] .. added_to .. [[) do + before[k] = true + end + + local M = require "]] .. by .. [[" + for k in pairs (]] .. added_to .. [[) do + after[k] = true + end + + for k in pairs (after) do + if not before[k] then print (k) end + end + ]]) + + elseif from and not_in then + return tabulate_output ([[ + local from = ]] .. from .. [[ + local M = require "]] .. not_in .. [[" + + for k in pairs (M) do + -- M[1] is typically the module namespace name, don't match + -- that! + if k ~= 1 and from[k] ~= M[k] then print (k) end + end + ]]) + + elseif from and enhanced_in then + return tabulate_output ([[ + local from = ]] .. from .. [[ + local M = require "]] .. enhanced_in .. [[" + + for k, v in pairs (M) do + if from[k] ~= M[k] and from[k] ~= nil then print (k) end + end + ]]) + + elseif from and enhanced_after then + return tabulate_output ([[ + local before, after = {}, {} + local from = ]] .. from .. [[ + + for k, v in pairs (from) do before[k] = v end + ]] .. enhanced_after .. [[ + for k, v in pairs (from) do after[k] = v end + + for k, v in pairs (before) do + if after[k] ~= nil and after[k] ~= v then print (k) end + end + ]]) + end + + assert (false, "missing argument to show_apis") +end + + +-- Stub inprocess.capture if necessary; new in Specl 12. +capture = inprocess.capture or + function (f, arg) return nil, nil, f (unpack (arg or {})) end + + +do + -- Custom matcher for set size and set membership. + + local util = require "specl.util" + local matchers = require "specl.matchers" + + local Matcher, matchers, q = + matchers.Matcher, matchers.matchers, matchers.stringify + + matchers.have_size = Matcher { + function (self, actual, expect) + local size = 0 + for _ in pairs (actual) do size = size + 1 end + return size == expect + end, + + actual = "table", + + format_expect = function (self, expect) + return " a table containing " .. expect .. " elements, " + end, + + format_any_of = function (self, alternatives) + return " a table with any of " .. + util.concat (alternatives, util.QUOTED) .. " elements, " + end, + } + + matchers.have_member = Matcher { + function (self, actual, expect) + return actual[expect] ~= nil + end, + + actual = "set", + + format_expect = function (self, expect) + return " a set containing " .. q (expect) .. ", " + end, + + format_any_of = function (self, alternatives) + return " a set containing any of " .. + util.concat (alternatives, util.QUOTED) .. ", " + end, + } + + -- Alias that doesn't tickle sc_error_message_uppercase. + matchers.raise = matchers.error +end diff --git a/spec/std_spec.yaml b/spec/std_spec.yaml new file mode 100644 index 0000000..6a88d8d --- /dev/null +++ b/spec/std_spec.yaml @@ -0,0 +1,591 @@ +before: | + this_module = "std" + global_table = "_G" + + exported_apis = { "assert", "barrel", "elems", "eval", "getmetamethod", + "ielems", "ipairs", "ireverse", "monkey_patch", "npairs", + "pairs", "require", "ripairs", "rnpairs", "tostring", + "version" } + + -- Tables with iterator metamethods used by various examples. + __pairs = setmetatable ({ content = "a string" }, { + __pairs = function (t) + return function (x, n) + if n < #x.content then + return n+1, string.sub (x.content, n+1, n+1) + end + end, t, 0 + end, + }) + __index = setmetatable ({ content = "a string" }, { + __index = function (t, n) + if n <= #t.content then + return t.content:sub (n, n) + end + end, + __len = function (t) return #t.content end, + }) + + M = require (this_module) + + +specify std: +- context when required: + - it does not touch the global table: + expect (show_apis {added_to=global_table, by=this_module}). + to_equal {} + - it exports the documented apis: + t = {} + for k in pairs (M) do t[#t + 1] = k end + expect (t).to_contain.a_permutation_of (exported_apis) + +- context when lazy loading: + - it has no submodules on initial load: + for _, v in pairs (M) do + expect (type (v)).not_to_be "table" + end + - it loads submodules on demand: + lazy = M.set + expect (lazy).to_be (require "std.set") + - it loads submodule functions on demand: + expect (M.object.prototype (M.set {"Lazy"})). + to_be "Set" + +- describe assert: + - before: + f = M.assert + + - context with bad arguments: + badargs.diagnose (f, "std.assert (?any, ?string, ?any*)") + + - context when it does not trigger: + - it has a truthy initial argument: + expect (f (1)).not_to_raise "any error" + expect (f (true)).not_to_raise "any error" + expect (f "yes").not_to_raise "any error" + expect (f (false == false)).not_to_raise "any error" + - it returns the initial argument: + expect (f (1)).to_be (1) + expect (f (true)).to_be (true) + expect (f "yes").to_be "yes" + expect (f (false == false)).to_be (true) + - context when it triggers: + - it has a falsey initial argument: + expect (f ()).to_raise () + expect (f (false)).to_raise () + expect (f (1 == 0)).to_raise () + - it throws an optional error string: + expect (f (false, "ah boo")).to_raise "ah boo" + - it plugs specifiers with string.format: | + expect (f (nil, "%s %d: %q", "here", 42, "a string")). + to_raise (string.format ("%s %d: %q", "here", 42, "a string")) + + +- describe barrel: + - before: + mt = { "file metatable" } + namespace = { + io = { + stdin = setmetatable ({}, mt), + stdout = setmetatable ({}, mt), + stderr = setmetatable ({}, mt), + }, + } + + f = M.barrel + + f (namespace) + + - context with bad arguments: + badargs.diagnose (f, "std.barrel (?table)") + + # Ideally, `.to_be (M)`, except that M is cloned from a nested context + # by Specl to prevent us from affecting any other examples, thus the + # address is different by now. Also, we have to force lazy-loading in + # our copy of M to match apis already loaded in barrel. + - it returns std module table: + t = f(namespace) + for k, v in pairs(t) do + if type(v) ~= 'function' then + _ = M[k] + end + end + expect (f (namespace)).to_equal (M) + - it installs std monkey patches: + for _, api in ipairs (exported_apis) do + if type (M[api]) == "function" and + api ~= "barrel" and api ~= "monkey_patch" + then + expect (namespace[api]).to_be (M[api]) + end + end + - it installs std.io monkey patches: + for _, api in ipairs { "catdir", "catfile", "die", "monkey_patch", + "process_files", "readlines", "shell", "slurp", "splitdir", "warn", + "writelines" } + do + expect (namespace.io[api]).to_be (M.io[api]) + end + expect (mt.readlines).to_be (M.io.readlines) + expect (mt.writelines).to_be (M.io.writelines) + - it installs std.math monkey patches: + for _, api in ipairs { "floor", "monkey_patch", "round" } do + expect (namespace.math[api]).to_be (M.math[api]) + end + - it installs std.string monkey patches: + # FIXME: string metatable monkey-patches leak out! + mt = getmetatable "" + expect (mt.__concat).to_be (M.string.__concat) + expect (mt.__index).to_be (M.string.__index) + + for _, api in ipairs { "__concat", "__index", "caps", "chomp", + "escape_pattern", "escape_shell", "finds", "format", "ltrim", + "monkey_patch", "numbertosi", "ordinal_suffix", "pad", "pickle", + "prettytostring", "render", "rtrim", "split", "tfind", "trim", + "wrap" } + do + expect (namespace.string[api]).to_be (M.string[api]) + end + - it installs std.table monkey patches: + for _, api in ipairs { "clone", "clone_select", "depair", "empty", + "enpair", "flatten", "insert", "invert", "keys", "len", "maxn", + "merge", "merge_select", "monkey_patch", "new", "pack", "project", + "shape", "size", "sort", "values" } + do + expect (namespace.table[api]).to_be (M.table[api]) + end + - it scribbles backwards compatibility apis: + for api, fn in pairs { + bind = M.functional.bind, + collect = M.functional.collect, + compose = M.functional.compose, + curry = M.functional.curry, + die = M.io.die, + filter = M.functional.filter, + fold = M.functional.fold, + id = M.functional.id, + ileaves = M.tree.ileaves, + inodes = M.tree.inodes, + leaves = M.tree.leaves, + map = M.functional.map, + metamethod = M.getmetamethod, + nodes = M.tree.nodes, + op = M.operator, + pack = M.table.pack, + pickle = M.string.pickle, + prettytostring = M.string.prettytostring, + render = M.string.render, + require_version = M.require, + warn = M.io.warn, + } do + expect (namespace[api]).to_be (fn) + end + - context when lazy loading: + - it has no submodules on initial load: + for _, v in pairs (M) do + expect (type (v)).not_to_be "table" + end + - it loads submodules on demand: + lazy = M.strbuf + expect (lazy).to_be (require "std.strbuf") + - it loads submodule functions on demand: + expect (M.object.prototype (M.strbuf {"Lazy"})). + to_be "StrBuf" + + +- describe elems: + - before: + f = M.elems + + - context with bad arguments: + badargs.diagnose (f, "std.elems (table)") + + - it is an iterator over table values: + t = {} + for e in f {"foo", bar = "baz", 42} do + t[#t + 1] = e + end + expect (t).to_contain.a_permutation_of {"foo", "baz", 42} + - it respects __pairs metamethod: | + t = {} + for v in f (__pairs) do t[#t + 1] = v end + expect (t). + to_contain.a_permutation_of {"a", " ", "s", "t", "r", "i", "n", "g"} + - it works for an empty list: + t = {} + for e in f {} do t[#t + 1] = e end + expect (t).to_equal {} + + +- describe eval: + - before: + f = M.eval + + - context with bad arguments: + badargs.diagnose (f, "std.eval (string)") + + - it diagnoses invalid lua: + # Some internal error when eval tries to call uncompilable "=" code. + expect (f "=").to_raise () + - it evaluates a string of lua code: + expect (f "math.min (2, 10)").to_be (math.min (2, 10)) + + +- describe getmetamethod: + - before: + f = M.getmetamethod + + - context with bad arguments: + badargs.diagnose (f, "std.getmetamethod (?any, string)") + + - context with a table: + - before: + method = function () end + t = setmetatable ({}, { _type = "table", _method = method }) + - it returns nil for missing metamethods: + expect (f (t, "not a metamethod on t")).to_be (nil) + - it returns nil for non-function metatable entries: + expect (f (t, "_type")).to_be (nil) + - it returns a method from the metatable: + expect (f (t, "_method")).to_be (method) + + - context with an object: + - before: + Object = require "std.object" + objmethod = function () end + obj = Object { + _type = "DerivedObject", + _method = objmethod, + } + - it returns nil for missing metamethods: + expect (f (obj, "not a metamethod on obj")).to_be (nil) + - it returns nil for non-function metatable entries: + expect (f (obj, "_type")).to_be (nil) + - it returns a method from the metatable: + expect (f (obj, "_method")).to_be (objmethod) + + +- describe ielems: + - before: + f = M.ielems + + - context with bad arguments: + badargs.diagnose (f, "std.ielems (table)") + + - it is an iterator over integer-keyed table values: + t = {} + for e in f {"foo", 42} do + t[#t + 1] = e + end + expect (t).to_equal {"foo", 42} + - it ignores the dictionary part of a table: + t = {} + for e in f {"foo", 42; bar = "baz", qux = "quux"} do + t[#t + 1] = e + end + expect (t).to_equal {"foo", 42} + - it respects __len metamethod: + t = {} + for v in f (__index) do t[#t + 1] = v end + expect (t).to_equal {"a", " ", "s", "t", "r", "i", "n", "g"} + - it works for an empty list: + t = {} + for e in f {} do t[#t + 1] = e end + expect (t).to_equal {} + + +- describe ipairs: + - before: + f = M.ipairs + + - context with bad arguments: + badargs.diagnose (f, "std.ipairs (table)") + + - it is an iterator over integer-keyed table values: + t = {} + for i, v in f {"foo", 42} do + t[i] = v + end + expect (t).to_equal {"foo", 42} + - it ignores the dictionary part of a table: + t = {} + for i, v in f {"foo", 42; bar = "baz", qux = "quux"} do + t[i] = v + end + expect (t).to_equal {"foo", 42} + - it respects __len metamethod: + t = {} + for k, v in f (__index) do t[k] = v end + expect (t).to_equal {"a", " ", "s", "t", "r", "i", "n", "g"} + - it works for an empty list: + t = {} + for i, v in f {} do t[i] = v end + expect (t).to_equal {} + + +- describe ireverse: + - before: + f = M.ireverse + + - context with bad arguments: + badargs.diagnose (f, "std.ireverse (table)") + + - it returns a new list: + t = {1, 2, 5} + expect (f (t)).not_to_be (t) + - it reverses the elements relative to the original list: + expect (f {1, 2, "five"}).to_equal {"five", 2, 1} + - it ignores the dictionary part of a table: + expect (f {1, 2, "five"; a = "b", c = "d"}).to_equal {"five", 2, 1} + - it respects __len metamethod: + expect (f (__index)).to_equal {"g", "n", "i", "r", "t", "s", " ", "a"} + - it works for an empty list: + expect (f {}).to_equal {} + + +- describe monkey_patch: + - before: + io_mt = {} + t = { + io = { + stdin = setmetatable ({}, io_mt), + stdout = setmetatable ({}, io_mt), + stderr = setmetatable ({}, io_mt), + }, + math = {}, + table = {}, + } + + f = M.monkey_patch + + f (t) + + - context with bad arguments: + badargs.diagnose (f, "std.monkey_patch (?table)") + + # Ideally, `.to_be (M)`, except that M is cloned from a nested context + # by Specl to prevent us from affecting any other examples, thus the + # address is different by now. + - it returns std module table: + expect (f (t)).to_equal (M) + - it installs std module functions: + for _, v in ipairs (exported_apis) do + if type (M[v]) == "function" and v ~= "barrel" and v ~= "monkey_patch" then + expect (t[v]).to_be (M[v]) + end + end + + +- describe npairs: + - before: + f = M.npairs + + - context with bad arguments: + badargs.diagnose (f, "std.npairs (table)") + + - it is an iterator over integer-keyed table values: + t = {} + for i, v in f {"foo", 42, nil, nil, "five"} do + t[i] = v + end + expect (t).to_equal {"foo", 42, nil, nil, "five"} + - it ignores the dictionary part of a table: + t = {} + for i, v in f {"foo", 42, nil, nil, "five"; bar = "baz", qux = "quux"} do + t[i] = v + end + expect (t).to_equal {"foo", 42, nil, nil, "five"} + - it works for an empty list: + t = {} + for i, v in f {} do t[i] = v end + expect (t).to_equal {} + + +- describe pairs: + - before: + f = M.pairs + + - context with bad arguments: + badargs.diagnose (f, "std.pairs (table)") + + - it is an iterator over all table values: + t = {} + for k, v in f {"foo", bar = "baz", 42} do + t[k] = v + end + expect (t).to_equal {"foo", bar = "baz", 42} + - it respects __pairs metamethod: | + t = {} + for k, v in f (__pairs) do t[k] = v end + expect (t). + to_contain.a_permutation_of {"a", " ", "s", "t", "r", "i", "n", "g"} + - it works for an empty list: + t = {} + for k, v in f {} do t[k] = v end + expect (t).to_equal {} + + +- describe require: + - before: + f = M.require + + - context with bad arguments: + badargs.diagnose (f, "std.require (string, ?string, ?string, ?string)") + + - it diagnoses non-existent module: + expect (f ("module-not-exists", "", "")).to_raise "module-not-exists" + - it diagnoses module too old: + expect (f ("std", "9999", "9999")). + to_raise "require 'std' with at least version 9999," + - it diagnoses module too new: + expect (f ("std", "0", "0")). + to_raise "require 'std' with version less than 0," + - context when the module version is compatible: + - it returns the module table: + expect (f ("std", "0", "9999")).to_be (require "std") + - it places no upper bound by default: + expect (f ("std", "41")).to_be (require "std") + - it places no lower bound by default: + expect (f "std").to_be (require "std") + - it uses _VERSION when version field is nil: + std = require "std" + M._VERSION, M.version = M.version, nil + expect (f ("std", "41", "9999")).to_be (require "std") + M._VERSION, M.version = nil, M._VERSION + - context with semantic versioning: + - before: + std = require "std" + ver = std.version + std.version = "1.2.3" + - after: + std.version = ver + - it diagnoses module too old: + expect (f ("std", "1.2.4")). + to_raise "require 'std' with at least version 1.2.4," + expect (f ("std", "1.3")). + to_raise "require 'std' with at least version 1.3," + expect (f ("std", "2.1.2")). + to_raise "require 'std' with at least version 2.1.2," + expect (f ("std", "2")). + to_raise "require 'std' with at least version 2," + expect (f ("std", "1.2.10")). + to_raise "require 'std' with at least version 1.2.10," + - it diagnoses module too new: + expect (f ("std", nil, "1.2.2")). + to_raise "require 'std' with version less than 1.2.2," + expect (f ("std", nil, "1.1")). + to_raise "require 'std' with version less than 1.1," + expect (f ("std", nil, "1.1.2")). + to_raise "require 'std' with version less than 1.1.2," + expect (f ("std", nil, "1")). + to_raise "require 'std' with version less than 1," + - it returns modules with version in range: + expect (f ("std")).to_be (std) + expect (f ("std", "1")).to_be (std) + expect (f ("std", "1.2.3")).to_be (std) + expect (f ("std", nil, "2")).to_be (std) + expect (f ("std", nil, "1.3")).to_be (std) + expect (f ("std", nil, "1.2.10")).to_be (std) + expect (f ("std", "1.2.3", "1.2.4")).to_be (std) + - context with several numbers in version string: + - before: + std = require "std" + ver = std.version + std.version = "standard library for Lua 5.3 / 41.0.0" + - after: + std.version = ver + - it diagnoses module too old: + expect (f ("std", "42")).to_raise () + - it diagnoses module too new: + expect (f ("std", nil, "40")).to_raise () + - it returns modules with version in range: + expect (f ("std")).to_be (std) + expect (f ("std", "1")).to_be (std) + expect (f ("std", "41")).to_be (std) + expect (f ("std", nil, "42")).to_be (std) + expect (f ("std", "41", "42")).to_be (std) + + +- describe ripairs: + - before: + f = M.ripairs + + - context with bad arguments: + badargs.diagnose (f, "std.ripairs (table)") + + - it returns a function, the table and a number: + fn, t, i = f {1, 2, 3} + expect ({type (fn), t, type (i)}).to_equal {"function", {1, 2, 3}, "number"} + - it iterates over the array part of a table: + t, u = {1, 2, 3; a=4, b=5, c=6}, {} + for i, v in f (t) do u[i] = v end + expect (u).to_equal {1, 2, 3} + - it returns elements in reverse order: + t, u = {"one", "two", "five"}, {} + for _, v in f (t) do u[#u + 1] = v end + expect (u).to_equal {"five", "two", "one"} + - it respects __len metamethod: + t = {} + for i, v in f (__index) do t[i] = v end + expect (t).to_equal {"a", " ", "s", "t", "r", "i", "n", "g"} + t = {} + for _, v in f (__index) do t[#t + 1] = v end + expect (t).to_equal {"g", "n", "i", "r", "t", "s", " ", "a"} + - it works with the empty list: + t = {} + for k, v in f {} do t[k] = v end + expect (t).to_equal {} + + +- describe rnpairs: + - before: + f = M.rnpairs + + - context with bad arguments: + badargs.diagnose (f, "std.rnpairs (table)") + + - it returns a function, the table and a number: + fn, t, i = f {1, 2, nil, nil, 3} + expect ({type (fn), t, type (i)}). + to_equal {"function", {1, 2, nil, nil, 3}, "number"} + - it iterates over the array part of a table: + t, u = {1, 2, nil, nil, 3; a=4, b=5, c=6}, {} + for i, v in f (t) do u[i] = v end + expect (u).to_equal {1, 2, nil, nil, 3} + - it returns elements in reverse order: + t, u, i = {"one", "two", nil, nil, "five"}, {}, 1 + for _, v in f (t) do u[i], i = v, i + 1 end + expect (u).to_equal {"five", nil, nil, "two", "one"} + - it works with the empty list: + t = {} + for k, v in f {} do t[k] = v end + expect (t).to_equal {} + + +- describe tostring: + - before: + f = M.tostring + + - context with bad arguments: + badargs.diagnose (f, "std.tostring (?any)") + + - it renders primitives exactly like system tostring: + expect (f (nil)).to_be (tostring (nil)) + expect (f (false)).to_be (tostring (false)) + expect (f (42)).to_be (tostring (42)) + expect (f (f)).to_be (tostring (f)) + expect (f "a string").to_be "a string" + - it renders empty tables as a pair of braces: + expect (f {}).to_be ("{}") + - it renders table array part compactly: + expect (f {"one", "two", "five"}). + to_be '{1=one,2=two,3=five}' + - it renders a table dictionary part compactly: + expect (f { one = true, two = 2, three = {3}}). + to_be '{one=true,three={1=3},two=2}' + - it renders table keys in table.sort order: + expect (f { one = 3, two = 5, three = 4, four = 2, five = 1 }). + to_be '{five=1,four=2,one=3,three=4,two=5}' + - it renders keys with invalid symbol names compactly: + expect (f { _ = 0, word = 0, ["?"] = 1, ["a-key"] = 1, ["[]"] = 1 }). + to_be '{?=1,[]=1,_=0,a-key=1,word=0}' diff --git a/spec/strbuf_spec.yaml b/spec/strbuf_spec.yaml new file mode 100644 index 0000000..88bbc07 --- /dev/null +++ b/spec/strbuf_spec.yaml @@ -0,0 +1,121 @@ +before: + object = require "std.object" + StrBuf = require "std.strbuf" + b = StrBuf {"foo", "bar"} + +specify std.strbuf: +- describe require: + - it does not perturb the global namespace: + expect (show_apis {added_to="_G", by="std.strbuf"}). + to_equal {} + + +- describe construction: + - context from StrBuf clone method: + - it constructs a new strbuf: + b = StrBuf:clone {} + expect (b).not_to_be (StrBuf) + expect (object.type (b)).to_be "StrBuf" + - it reuses the StrBuf metatable: + a, b = StrBuf:clone {"a"}, StrBuf:clone {"b"} + expect (getmetatable (a)).to_be (getmetatable (b)) + - it initialises strbuf with constructor parameters: + a = StrBuf:clone {"foo", "bar"} + expect (a).to_equal (b) + - it serves as a prototype for new instances: + obj = b:clone {} + expect (object.type (obj)).to_be "StrBuf" + expect (obj).to_equal (b) + expect (getmetatable (obj)).to_be (getmetatable (b)) + + # StrBuf {args} is just syntactic sugar for StrBuf:clone {args} + - context from StrBuf object prototype: + - it constructs a new strbuf: + b = StrBuf {} + expect (b).not_to_be (StrBuf) + expect (object.type (b)).to_be "StrBuf" + - it reuses the StrBuf metatable: + a, b = StrBuf {"a"}, StrBuf {"b"} + expect (getmetatable (a)).to_be (getmetatable (b)) + - it initialises strbuf with constructor parameters: + a = StrBuf:clone {"foo", "bar"} + expect (a).to_equal (b) + - it serves as a prototype for new instances: + obj = b {} + expect (object.type (obj)).to_be "StrBuf" + expect (obj).to_equal (b) + expect (getmetatable (obj)).to_be (getmetatable (b)) + + +- describe tostring: + - context as a module function: + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (StrBuf.tostring, {b})). + to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (StrBuf.tostring, {b})). + not_to_contain_error "was deprecated" + - it returns buffered string: + expect (StrBuf.tostring (b)).to_be "foobar" + + - context as an object method: + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (b.tostring, {b})). + to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (b.tostring, {b})). + not_to_contain_error "was deprecated" + - it returns buffered string: + expect (b:tostring ()).to_be "foobar" + + - context as a metamethod: + - it returns buffered string: + expect (tostring (b)).to_be "foobar" + + +- describe concat: + - before: + a = StrBuf {"foo", "bar"} + b = StrBuf {"baz", "quux"} + + - context as a module function: + - it appends a string: + a = StrBuf.concat (a, "baz") + expect (object.type (a)).to_be "StrBuf" + expect (tostring (a)).to_be "foobarbaz" + - it appends a StrBuf: + a = StrBuf.concat (a, b) + expect (object.type (a)).to_be "StrBuf" + expect (tostring (a)).to_be "foobarbazquux" + - context as an object method: + - it appends a string: + a = a:concat "baz" + expect (object.type (a)).to_be "StrBuf" + expect (tostring(a)).to_be "foobarbaz" + - it appends a StrBuf: + a = a:concat (b) + expect (object.type (a)).to_be "StrBuf" + expect (tostring (a)).to_be "foobarbazquux" + - context as a metamethod: + - it appends a string: + a = a .. "baz" + expect (object.type (a)).to_be "StrBuf" + expect (tostring (a)).to_be "foobarbaz" + - it appends a StrBuf: + a = a .. b + expect (object.type (a)).to_be "StrBuf" + expect (tostring (a)).to_be "foobarbazquux" + - it stringifies lazily: + a = StrBuf {1} + b = StrBuf {a, "five"} + a = a:concat (2) + expect (tostring (b)).to_be "12five" + b = StrBuf {tostring (a), "five"} + a = a:concat (3) + expect (tostring (b)).to_be "12five" + - it can be non-destructive: + a = StrBuf {1} + b = a {} .. 2 + expect (tostring (a)).to_be "1" diff --git a/spec/string_spec.yaml b/spec/string_spec.yaml new file mode 100644 index 0000000..7ac10ba --- /dev/null +++ b/spec/string_spec.yaml @@ -0,0 +1,745 @@ +before: + base_module = "string" + this_module = "std.string" + global_table = "_G" + + extend_base = { "__concat", "__index", + "caps", "chomp", "escape_pattern", "escape_shell", + "finds", "format", "ltrim", "monkey_patch", + "numbertosi", "ordinal_suffix", "pad", "pickle", + "prettytostring", "render", "rtrim", "split", + "tfind", "trim", "wrap" } + deprecations = { "assert", "require_version", "tostring" } + + M = require (this_module) + getmetatable ("").__concat = M.__concat + getmetatable ("").__index = M.__index + +specify std.string: +- before: + subject = "a string \n\n" + +- context when required: + - context by name: + - it does not touch the global table: + expect (show_apis {added_to=global_table, by=this_module}). + to_equal {} + - it does not touch the core string table: + expect (show_apis {added_to=base_module, by=this_module}). + to_equal {} + - it contains apis from the core string table: + apis = require "std.base".copy (extend_base) + for _, v in ipairs (deprecations) do + apis[#apis + 1] = v + end + expect (show_apis {from=base_module, not_in=this_module}). + to_contain.a_permutation_of (apis) + + - context via the std module: + - it does not touch the global table: + expect (show_apis {added_to=global_table, by="std"}). + to_equal {} + - it does not touch the core string table: + expect (show_apis {added_to=base_module, by="std"}). + to_equal {} + +- describe ..: + - it concatenates string arguments: + target = "a string \n\n another string" + expect (subject .. " another string").to_be (target) + - it stringifies non-string arguments: + argument = { "a table" } + expect (subject .. argument). + to_be (string.format ("%s%s", subject, require "std".tostring (argument))) + - it stringifies nil arguments: + argument = nil + expect (subject .. argument). + to_be (string.format ("%s%s", subject, require "std".tostring (argument))) + - it does not perturb the original subject: + original = subject + newstring = subject .. " concatenate something" + expect (subject).to_be (original) + + +- describe assert: + - before: + f = M.assert + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {"std.string"})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {"std.string"})).not_to_contain_error "was deprecated" + + - context when it does not trigger: + - it has a truthy initial argument: + expect (f (1)).not_to_raise "any error" + expect (f (true)).not_to_raise "any error" + expect (f "yes").not_to_raise "any error" + expect (f (false == false)).not_to_raise "any error" + - it returns the initial argument: + expect (f (1)).to_be (1) + expect (f (true)).to_be (true) + expect (f "yes").to_be "yes" + expect (f (false == false)).to_be (true) + - context when it triggers: + - it has a falsey initial argument: + expect (f ()).to_raise () + expect (f (false)).to_raise () + expect (f (1 == 0)).to_raise () + - it throws an optional error string: + expect (f (false, "ah boo")).to_raise "ah boo" + - it plugs specifiers with string.format: | + expect (f (nil, "%s %d: %q", "here", 42, "a string")). + to_raise (string.format ("%s %d: %q", "here", 42, "a string")) + + +- describe caps: + - before: + f = M.caps + + - context with bad arguments: + badargs.diagnose (f, "std.string.caps (string)") + + - it capitalises words of a string: + target = "A String \n\n" + expect (f (subject)).to_be (target) + - it changes only the first letter of each word: + expect (f "a stRiNg").to_be "A StRiNg" + - it is available as a string metamethod: + expect (("a stRiNg"):caps ()).to_be "A StRiNg" + - it does not perturb the original subject: + original = subject + newstring = f (subject) + expect (subject).to_be (original) + + +- describe chomp: + - before: + target = "a string \n" + f = M.chomp + + - context with bad arguments: + badargs.diagnose (f, "std.string.chomp (string)") + + - it removes a single trailing newline from a string: + expect (f (subject)).to_be (target) + - it does not change a string with no trailing newline: + subject = "a string " + expect (f (subject)).to_be (subject) + - it is available as a string metamethod: + expect (subject:chomp ()).to_be (target) + - it does not perturb the original subject: + original = subject + newstring = f (subject) + expect (subject).to_be (original) + + +- describe escape_pattern: + - before: + magic = {} + meta = "^$()%.[]*+-?" + for i = 1, string.len (meta) do + magic[meta:sub (i, i)] = true + end + f = M.escape_pattern + + - context with bad arguments: + badargs.diagnose (f, "std.string.escape_pattern (string)") + + - context with each printable ASCII char: + - before: + subject, target = "", "" + for c = 32, 126 do + s = string.char (c) + subject = subject .. s + if magic[s] then target = target .. "%" end + target = target .. s + end + - "it inserts a % before any non-alphanumeric in a string": + expect (f (subject)).to_be (target) + - it is available as a string metamethod: + expect (subject:escape_pattern ()).to_be (target) + - it does not perturb the original subject: + original = subject + newstring = f (subject) + expect (subject).to_be (original) + + +- describe escape_shell: + - before: + f = M.escape_shell + + - context with bad arguments: + badargs.diagnose (f, "std.string.escape_shell (string)") + + - context with each printable ASCII char: + - before: + subject, target = "", "" + for c = 32, 126 do + s = string.char (c) + subject = subject .. s + if s:match ("[][ ()\\\"']") then target = target .. "\\" end + target = target .. s + end + - "it inserts a \\ before any shell metacharacters": + expect (f (subject)).to_be (target) + - it is available as a string metamethod: + expect (subject:escape_shell ()).to_be (target) + - it does not perturb the original subject: + original = subject + newstring = f (subject) + expect (subject).to_be (original) + - "it diagnoses non-string arguments": + expect (f ()).to_raise ("string expected") + expect (f {"a table"}).to_raise ("string expected") + + +- describe finds: + - before: + subject = "abcd" + f = M.finds + + - context with bad arguments: + badargs.diagnose (f, "std.string.finds (string, string, ?int, ?boolean|:plain)") + + - context given a complex nested list: + - before: + target = { { 1, 2; capt = { "a", "b" } }, { 3, 4; capt = { "c", "d" } } } + - it creates a list of pattern captures: + expect ({f (subject, "(.)(.)")}).to_equal ({ target }) + - it is available as a string metamethod: + expect ({subject:finds ("(.)(.)")}).to_equal ({ target }) + - it creates an empty list where no captures are matched: + target = {} + expect ({f (subject, "(x)")}).to_equal ({ target }) + - it creates an empty list for a pattern without captures: + target = { { 1, 1; capt = {} } } + expect ({f (subject, "a")}).to_equal ({ target }) + - it starts the search at a specified index into the subject: + target = { { 8, 9; capt = { "a", "b" } }, { 10, 11; capt = { "c", "d" } } } + expect ({f ("garbage" .. subject, "(.)(.)", 8)}).to_equal ({ target }) + - it does not perturb the original subject: + original = subject + newstring = f (subject, "...") + expect (subject).to_be (original) + + +- describe format: + - before: + subject = "string=%s, number=%d" + + f = M.format + + - context with bad arguments: + badargs.diagnose (f, "std.string.format (string, ?any*)") + + - it returns a single argument without attempting formatting: + expect (f (subject)).to_be (subject) + - it is available as a string metamethod: + expect (subject:format ()).to_be (subject) + - it does not perturb the original subject: + original = subject + newstring = f (subject) + expect (subject).to_be (original) + + +- describe ltrim: + - before: + subject = " \t\r\n a short string \t\r\n " + + f = M.ltrim + + - context with bad arguments: + badargs.diagnose (f, "std.string.ltrim (string, ?string)") + + - it removes whitespace from the start of a string: + target = "a short string \t\r\n " + expect (f (subject)).to_equal (target) + - it supports custom removal patterns: + target = "\r\n a short string \t\r\n " + expect (f (subject, "[ \t\n]+")).to_equal (target) + - it is available as a string metamethod: + target = "\r\n a short string \t\r\n " + expect (subject:ltrim ("[ \t\n]+")).to_equal (target) + - it does not perturb the original subject: + original = subject + newstring = f (subject, "%W") + expect (subject).to_be (original) + + +- describe monkey_patch: + - before: + f = M.monkey_patch + + - context with bad arguments: + badargs.diagnose (f, "std.string.monkey_patch (?table)") + + # Ideally, `.to_be (M)`, except that M is cloned from a nested context + # by Specl to prevent us from affecting any other examples, thus the + # address is different by now. + - it returns std.string module table: + expect (f {}).to_equal (M) + - it injects std.string apis into given namespace: + namespace = {} + f (namespace) + for _, api in ipairs (extend_base) do + expect (namespace.string[api]).to_be (M[api]) + end + - it installs string metamethods: + # FIXME: string metatable monkey-patches leak out! + mt = getmetatable "" + expect (mt.__concat).to_be (M.__concat) + expect (mt.__index).to_be (M.__index) + + +- describe numbertosi: + - before: + f = M.numbertosi + + - context with bad arguments: + badargs.diagnose (f, "std.string.numbertosi (number|string)") + + - it returns a number using SI suffixes: + target = {"1e-9", "1y", "1z", "1a", "1f", "1p", "1n", "1mu", "1m", "1", + "1k", "1M", "1G", "1T", "1P", "1E", "1Z", "1Y", "1e9"} + subject = {} + for n = -28, 28, 3 do + m = 10 * (10 ^ n) + table.insert (subject, f (m)) + end + expect (subject).to_equal (target) + - it coerces string arguments to a number: + expect (f "1000").to_be "1k" + + +- describe ordinal_suffix: + - before: + f = M.ordinal_suffix + + - context with bad arguments: + badargs.diagnose (f, "std.string.ordinal_suffix (int|string)") + + - it returns the English suffix for a number: + subject, target = {}, {} + for n = -120, 120 do + suffix = "th" + m = math.abs (n) % 10 + if m == 1 and math.abs (n) % 100 ~= 11 then suffix = "st" + elseif m == 2 and math.abs (n) % 100 ~= 12 then suffix = "nd" + elseif m == 3 and math.abs (n) % 100 ~= 13 then suffix = "rd" + end + table.insert (target, n .. suffix) + table.insert (subject, n .. f (n)) + end + expect (subject).to_equal (target) + - it coerces string arguments to a number: + expect (f "-91").to_be "st" + + +- describe pad: + - before: + width = 20 + + f = M.pad + + - context with bad arguments: + badargs.diagnose (f, "std.string.pad (string, int, ?string)") + + - context when string is shorter than given width: + - before: + subject = "short string" + - it right pads a string to the given width with spaces: + target = "short string " + expect (f (subject, width)).to_be (target) + - it left pads a string to the given negative width with spaces: + width = -width + target = " short string" + expect (f (subject, width)).to_be (target) + - it is available as a string metamethod: + target = "short string " + expect (subject:pad (width)).to_be (target) + + - context when string is longer than given width: + - before: + subject = "a string that's longer than twenty characters" + - it truncates a string to the given width: + target = "a string that's long" + expect (f (subject, width)).to_be (target) + - it left pads a string to given width with spaces: + width = -width + target = "an twenty characters" + expect (f (subject, width)).to_be (target) + - it is available as a string metamethod: + target = "a string that's long" + expect (subject:pad (width)).to_be (target) + + - it does not perturb the original subject: + original = subject + newstring = f (subject, width) + expect (subject).to_be (original) + + +- describe pickle: + - before: + loadstring = loadstring or load + function unpickle (s) return loadstring ("return " .. s) () end + t = {1, {{2, 3}, 4, {5}}} + f = M.pickle + - it converts a primitive to a representative string: + expect (f (nil)).to_be "nil" + expect (f (false)).to_be "false" + expect (f (42)).to_be "42" + expect (f "string").to_be '"string"' + - it returns a loadable string that results in the original value: + expect (unpickle (f (nil))).to_be (nil) + expect (unpickle (f (false))).to_be (false) + expect (unpickle (f (42))).to_be (42) + expect (unpickle (f "string")).to_be "string" + - it converts a table to a representative string: + expect (f {"table", 42}).to_be '{[1]="table",[2]=42}' + - it returns a loadable string that results in the original table: + expect (unpickle (f {"table", 42})).to_equal {"table", 42} + - it converts a nested table to a representative string: + expect (f (t)). + to_be "{[1]=1,[2]={[1]={[1]=2,[2]=3},[2]=4,[3]={[1]=5}}}" + - it returns a loadable string that results in the original nested table: + expect (unpickle (f (t))).to_equal (t) + + +- describe prettytostring: + - before: + f = M.prettytostring + + - context with bad arguments: + badargs.diagnose (f, "std.string.prettytostring (?any, ?string, ?string)") + + - it renders nil exactly like system tostring: + expect (f (nil)).to_be (tostring (nil)) + - it renders booleans exactly like system tostring: + expect (f (true)).to_be (tostring (true)) + expect (f (false)).to_be (tostring (false)) + - it renders numbers exactly like system tostring: + n = 8723643 + expect (f (n)).to_be (tostring (n)) + - it renders functions exactly like system tostring: + expect (f (f)).to_be (tostring (f)) + - it renders strings with format "%q" styling: + s = "a string" + expect (f (s)).to_be (string.format ("%q", s)) + - it renders empty tables as a pair of braces: + expect (f {}).to_be ("{\n}") + - it renders an array prettily: + a = {"one", "two", "three"} + expect (f (a, "")). + to_be '{\n[1] = "one",\n[2] = "two",\n[3] = "three",\n}' + - it renders a table prettily: + t = { one = true, two = 2, three = {3}} + expect (f (t, "")). + to_be '{\none = true,\nthree =\n{\n[1] = 3,\n},\ntwo = 2,\n}' + - it renders table keys in table.sort order: + t = { one = 3, two = 5, three = 4, four = 2, five = 1 } + expect (f (t, "")). + to_be '{\nfive = 1,\nfour = 2,\none = 3,\nthree = 4,\ntwo = 5,\n}' + - it renders keys with invalid symbol names in long hand: + t = { _ = 0, word = 0, ["?"] = 1, ["a-key"] = 1, ["[]"] = 1 } + expect (f (t, "")). + to_be '{\n["?"] = 1,\n["[]"] = 1,\n_ = 0,\n["a-key"] = 1,\nword = 0,\n}' + + +- describe render: + - before: + term = function (s) return function () return s end end + pair = function (_, _, _, i, v) return i .. "=" .. v end + sep = function (_, i, _, j) return (i and j) and "," or "" end + r = function (x) + return M.render (x, term "{", term "}", tostring, pair, sep) + end + t = {1, {{2, 3}, 4, {5}}} + + f = M.render + + - context with bad arguments: + badargs.diagnose (f, "std.string.render (?any, func, func, func, func, func, ?table)") + + - it converts a primitive to a representative string: + expect (r (nil)).to_be "nil" + expect (r (false)).to_be "false" + expect (r (42)).to_be "42" + expect (r ("string")).to_be "string" + - it converts a table to a representative string: + expect (r ({"table", 42})).to_be '{1=table,2=42}' + - it converts a nested table to a representative string: + expect (r (t)). + to_be "{1=1,2={1={1=2,2=3},2=4,3={1=5}}}" + - it converts a recursive table to a representative string: + t[1] = t + expect (r (t)). + to_be ("{1="..tostring (t)..",2={1={1=2,2=3},2=4,3={1=5}}}") + + +- describe require_version: + - before: + f = M.require_version + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {"std.string"})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {"std.string"})).not_to_contain_error "was deprecated" + + - it diagnoses non-existent module: + expect (f ("module-not-exists", "", "")).to_raise "module-not-exists" + - it diagnoses module too old: + expect (f ("std", "9999", "9999")).to_raise () + - it diagnoses module too new: + expect (f ("std", "0", "0")).to_raise () + - context when the module version is compatible: + - it returns the module table: + expect (f ("std", "0", "9999")).to_be (require "std") + - it places no upper bound by default: + expect (f ("std", "41")).to_be (require "std") + - it places no lower bound by default: + expect (f "std").to_be (require "std") + - it uses _VERSION when version field is nil: + std = require "std" + std._VERSION, std.version = std.version, nil + expect (f ("std", "41", "9999")).to_be (require "std") + std._VERSION, std.version = nil, std._VERSION + - context with semantic versioning: + - before: + std = require "std" + ver = std.version + std.version = "1.2.3" + - after: + std.version = ver + - it diagnoses module too old: + expect (f ("std", "1.2.4")).to_raise () + expect (f ("std", "1.3")).to_raise () + expect (f ("std", "2.1.2")).to_raise () + expect (f ("std", "2")).to_raise () + expect (f ("std", "1.2.10")).to_raise () + - it diagnoses module too new: + expect (f ("std", nil, "1.2.2")).to_raise () + expect (f ("std", nil, "1.1")).to_raise () + expect (f ("std", nil, "1.1.2")).to_raise () + expect (f ("std", nil, "1")).to_raise () + - it returns modules with version in range: + expect (f ("std")).to_be (std) + expect (f ("std", "1")).to_be (std) + expect (f ("std", "1.2.3")).to_be (std) + expect (f ("std", nil, "2")).to_be (std) + expect (f ("std", nil, "1.3")).to_be (std) + expect (f ("std", nil, "1.2.10")).to_be (std) + expect (f ("std", "1.2.3", "1.2.4")).to_be (std) + + +- describe rtrim: + - before: + subject = " \t\r\n a short string \t\r\n " + + f = M.rtrim + + - context with bad arguments: + badargs.diagnose (f, "std.string.rtrim (string, ?string)") + + - it removes whitespace from the end of a string: + target = " \t\r\n a short string" + expect (f (subject)).to_equal (target) + - it supports custom removal patterns: + target = " \t\r\n a short string \t\r" + expect (f (subject, "[ \t\n]+")).to_equal (target) + - it is available as a string metamethod: + target = " \t\r\n a short string \t\r" + expect (subject:rtrim ("[ \t\n]+")).to_equal (target) + - it does not perturb the original subject: + original = subject + newstring = f (subject, "%W") + expect (subject).to_be (original) + + +- describe split: + - before: + target = { "first", "the second one", "final entry" } + subject = table.concat (target, ", ") + + f = M.split + + - context with bad arguments: + badargs.diagnose (f, "std.string.split (string, ?string)") + + - it falls back to "%s+" when no pattern is given: + expect (f (subject)). + to_equal {"first,", "the", "second", "one,", "final", "entry"} + - it returns a one-element list for an empty string: + expect (f ("", ", ")).to_equal {""} + - it makes a table of substrings delimited by a separator: + expect (f (subject, ", ")).to_equal (target) + - it returns n+1 elements for n separators: + expect (f (subject, "zero")).to_have_size (1) + expect (f (subject, "c")).to_have_size (2) + expect (f (subject, "s")).to_have_size (3) + expect (f (subject, "t")).to_have_size (4) + expect (f (subject, "e")).to_have_size (5) + - it returns an empty string element for consecutive separators: + expect (f ("xyzyzxy", "yz")).to_equal {"x", "", "xy"} + - it returns an empty string element when starting with separator: + expect (f ("xyzyzxy", "xyz")).to_equal {"", "yzxy"} + - it returns an empty string element when ending with separator: + expect (f ("xyzyzxy", "zxy")).to_equal {"xyzy", ""} + - it returns a table of 1-character strings for "" separator: + expect (f ("abcdef", "")).to_equal {"", "a", "b", "c", "d", "e", "f", ""} + - it is available as a string metamethod: + expect (subject:split ", ").to_equal (target) + expect (("/foo/bar/baz.quux"):split "/"). + to_equal {"", "foo", "bar", "baz.quux"} + - it does not perturb the original subject: + original = subject + newstring = f (subject, "e") + expect (subject).to_be (original) + - it takes a Lua pattern as a separator: + expect (f (subject, "%s+")). + to_equal {"first,", "the", "second", "one,", "final", "entry"} + + +- describe tfind: + - before: + subject = "abc" + + f = M.tfind + + - context with bad arguments: + badargs.diagnose (f, "std.string.tfind (string, string, ?int, ?boolean|:plain)") + + - it creates a list of pattern captures: + target = { 1, 3, { "a", "b", "c" } } + expect ({f (subject, "(.)(.)(.)")}).to_equal (target) + - it creates an empty list where no captures are matched: + target = { nil, nil, {} } + expect ({f (subject, "(x)(y)(z)")}).to_equal (target) + - it creates an empty list for a pattern without captures: + target = { 1, 1, {} } + expect ({f (subject, "a")}).to_equal (target) + - it starts the search at a specified index into the subject: + target = { 8, 10, { "a", "b", "c" } } + expect ({f ("garbage" .. subject, "(.)(.)(.)", 8)}).to_equal (target) + - it is available as a string metamethod: + target = { 8, 10, { "a", "b", "c" } } + expect ({("garbage" .. subject):tfind ("(.)(.)(.)", 8)}).to_equal (target) + - it does not perturb the original subject: + original = subject + newstring = f (subject, "...") + expect (subject).to_be (original) + + +- describe tostring: + - before: + f = M.tostring + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {"std.string"})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {"std.string"})).not_to_contain_error "was deprecated" + + - it renders primitives exactly like system tostring: + expect (f (nil)).to_be (tostring (nil)) + expect (f (false)).to_be (tostring (false)) + expect (f (42)).to_be (tostring (42)) + expect (f (f)).to_be (tostring (f)) + expect (f "a string").to_be "a string" + - it renders empty tables as a pair of braces: + expect (f {}).to_be ("{}") + - it renders table array part compactly: + expect (f {"one", "two", "five"}). + to_be '{1=one,2=two,3=five}' + - it renders a table dictionary part compactly: + expect (f { one = true, two = 2, three = {3}}). + to_be '{one=true,three={1=3},two=2}' + - it renders table keys in table.sort order: + expect (f { one = 3, two = 5, three = 4, four = 2, five = 1 }). + to_be '{five=1,four=2,one=3,three=4,two=5}' + - it renders keys with invalid symbol names compactly: + expect (f { _ = 0, word = 0, ["?"] = 1, ["a-key"] = 1, ["[]"] = 1 }). + to_be '{?=1,[]=1,_=0,a-key=1,word=0}' + + +- describe trim: + - before: + subject = " \t\r\n a short string \t\r\n " + + f = M.trim + + - context with bad arguments: + badargs.diagnose (f, "std.string.trim (string, ?string)") + + - it removes whitespace from each end of a string: + target = "a short string" + expect (f (subject)).to_equal (target) + - it supports custom removal patterns: + target = "\r\n a short string \t\r" + expect (f (subject, "[ \t\n]+")).to_equal (target) + - it is available as a string metamethod: + target = "\r\n a short string \t\r" + expect (subject:trim ("[ \t\n]+")).to_equal (target) + - it does not perturb the original subject: + original = subject + newstring = f (subject, "%W") + expect (subject).to_be (original) + + +- describe wrap: + - before: + subject = "This is a collection of Lua libraries for Lua 5.1 " .. + "and 5.2. The libraries are copyright by their authors 2000" .. + "-2015 (see the AUTHORS file for details), and released und" .. + "er the MIT license (the same license as Lua itself). There" .. + " is no warranty." + + f = M.wrap + + - context with bad arguments: + badargs.diagnose (f, "std.string.wrap (string, ?int, ?int, ?int)") + + - it inserts newlines to wrap a string: + target = "This is a collection of Lua libraries for Lua 5.1 a" .. + "nd 5.2. The libraries are\ncopyright by their authors 2000" .. + "-2015 (see the AUTHORS file for details), and\nreleased un" .. + "der the MIT license (the same license as Lua itself). Ther" .. + "e is no\nwarranty." + expect (f (subject)).to_be (target) + - it honours a column width parameter: + target = "This is a collection of Lua libraries for Lua 5.1 a" .. + "nd 5.2. The libraries\nare copyright by their authors 2000" .. + "-2015 (see the AUTHORS file for\ndetails), and released un" .. + "der the MIT license (the same license as Lua\nitself). The" .. + "re is no warranty." + expect (f (subject, 72)).to_be (target) + - it supports indenting by a fixed number of columns: + target = " This is a collection of Lua libraries for L" .. + "ua 5.1 and 5.2. The\n libraries are copyright by th" .. + "eir authors 2000-2015 (see the\n AUTHORS file for d" .. + "etails), and released under the MIT license\n (the " .. + "same license as Lua itself). There is no warranty." + expect (f (subject, 72, 8)).to_be (target) + - context given a long unwrapped string: + - before: + target = " This is a collection of Lua libraries for Lua 5" .. + ".1 and 5.2.\n The libraries are copyright by their author" .. + "s 2000-2015 (see\n the AUTHORS file for details), and rel" .. + "eased under the MIT\n license (the same license as Lua it" .. + "self). There is no\n warranty." + - it can indent the first line differently: + expect (f (subject, 64, 2, 4)).to_be (target) + - it is available as a string metamethod: + expect (subject:wrap (64, 2, 4)).to_be (target) + - it does not perturb the original subject: + original = subject + newstring = f (subject, 55, 5) + expect (subject).to_be (original) + - it diagnoses indent greater than line width: + expect (f (subject, 10, 12)).to_raise ("less than the line width") + expect (f (subject, 99, 99)).to_raise ("less than the line width") + - it diagnoses non-string arguments: + expect (f ()).to_raise ("string expected") + expect (f {"a table"}).to_raise ("string expected") diff --git a/spec/table_spec.yaml b/spec/table_spec.yaml new file mode 100644 index 0000000..6ff2cad --- /dev/null +++ b/spec/table_spec.yaml @@ -0,0 +1,772 @@ +before: | + base_module = "table" + this_module = "std.table" + global_table = "_G" + + extend_base = { "clone", "clone_select", "depair", "empty", + "enpair", "flatten", "insert", "invert", "keys", + "len", "maxn", "merge", "merge_select", + "monkey_patch", "new", "okeys", "pack", "project", + "remove", "shape", "size", "sort", "unpack", + "values" } + deprecations = { "clone_rename", "metamethod", "ripairs", "totable" } + + M = require "std.table" + + +specify std.table: +- context when required: + - context by name: + - it does not touch the global table: + expect (show_apis {added_to=global_table, by=this_module}). + to_equal {} + - it does not touch the core table table: + expect (show_apis {added_to=base_module, by=this_module}). + to_equal {} + - it contains apis from the core table table: + apis = require "std.base".copy (extend_base) + for _, v in ipairs (deprecations) do + apis[#apis + 1] = v + end + expect (show_apis {from=base_module, not_in=this_module}). + to_contain.a_permutation_of (apis) + + - context via the std module: + - it does not touch the global table: + expect (show_apis {added_to=global_table, by="std"}).to_equal {} + - it does not touch the core table table: + expect (show_apis {added_to=base_module, by="std"}).to_equal {} + + +- describe clone: + - before: + subject = { k1 = {"v1"}, k2 = {"v2"}, k3 = {"v3"} } + withmt = setmetatable (M.clone (subject), {"meta!"}) + + f = M.clone + + - context with bad arguments: + badargs.diagnose (f, "std.table.clone (table, [table], ?boolean|:nometa)") + + - it does not just return the subject: + expect (f (subject)).not_to_be (subject) + - it does copy the subject: + expect (f (subject)).to_equal (subject) + - it only makes a shallow copy of field values: + expect (f (subject).k1).to_be (subject.k1) + - it does not perturb the original subject: + target = { k1 = subject.k1, k2 = subject.k2, k3 = subject.k3 } + copy = f (subject) + expect (subject).to_equal (target) + expect (subject).to_be (subject) + + - context with metatables: + - it copies the metatable by default: + expect (getmetatable (f (withmt))).to_be (getmetatable (withmt)) + - it treats non-table arg2 as nometa parameter: + expect (getmetatable (f (withmt, ":nometa"))).to_be (nil) + - it treats table arg2 as a map parameter: + expect (getmetatable (f (withmt, {}))).to_be (getmetatable (withmt)) + - it supports 3 arguments with nometa as arg3: + expect (getmetatable (f (withmt, {}, ":nometa"))).to_be (nil) + + - context when renaming some keys: + - it renames during cloning: + target = { newkey = subject.k1, k2 = subject.k2, k3 = subject.k3 } + expect (f (subject, {k1 = "newkey"})).to_equal (target) + - it does not perturb the value in the renamed key field: + expect (f (subject, {k1 = "newkey"}).newkey).to_be (subject.k1) + + +- describe clone_rename: + - before: + subject = { k1 = {"v1"}, k2 = {"v2"}, k3 = {"v3"} } + + fname = "clone_rename" + f = M[fname] + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {{}, subject})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {{}, subject})).not_to_contain_error "was deprecated" + + - it copies the subject: + expect (f ({}, subject)).to_copy (subject) + - it only makes a shallow copy: + expect (f ({}, subject).k2).to_be (subject.k2) + + - context when renaming some keys: + - before: + target = { newkey = subject.k1, k2 = subject.k2, k3 = subject.k3 } + - it renames during cloning: + expect (f ({k1 = "newkey"}, subject)).to_equal (target) + - it does not perturb the value in the renamed key field: + expect (f ({k1 = "newkey"}, subject).newkey).to_be (subject.k1) + + - it diagnoses non-table arguments: + expect (f {}).to_raise ("table expected") + expect (f ({}, "foo")).to_raise ("table expected") + + +- describe clone_select: + - before: + subject = { k1 = {"v1"}, k2 = {"v2"}, k3 = {"v3"} } + withmt = setmetatable (M.clone (subject), {"meta!"}) + + f = M.clone_select + + - context with bad arguments: + badargs.diagnose (f, "std.table.clone_select (table, [table], ?boolean|:nometa)") + + - it does not just return the subject: + expect (f (subject)).not_to_be (subject) + - it copies the keys selected: + expect (f (subject, {"k1", "k2"})).to_equal ({ k1 = {"v1"}, k2 = {"v2"} }) + - it does copy the subject when supplied with a full list of keys: + expect (f (subject, {"k1", "k2", "k3"})).to_equal (subject) + - it only makes a shallow copy: + expect (f (subject, {"k1"}).k1).to_be (subject.k1) + - it does not perturb the original subject: + target = { k1 = subject.k1, k2 = subject.k2, k3 = subject.k3 } + copy = f (subject, {"k1", "k2", "k3"}) + expect (subject).to_equal (target) + expect (subject).to_be (subject) + + - context with metatables: + - it treats non-table arg2 as nometa parameter: + expect (getmetatable (f (withmt, ":nometa"))).to_be (nil) + - it treats table arg2 as a map parameter: + expect (getmetatable (f (withmt, {}))).to_be (getmetatable (withmt)) + expect (getmetatable (f (withmt, {"k1"}))).to_be (getmetatable (withmt)) + - it supports 3 arguments with nometa as arg3: + expect (getmetatable (f (withmt, {}, ":nometa"))).to_be (nil) + expect (getmetatable (f (withmt, {"k1"}, ":nometa"))).to_be (nil) + + +- describe depair: + - before: + t = {"first", "second", third = 4} + l = M.enpair (t) + + f = M.depair + + - context with bad arguments: + badargs.diagnose (f, "std.table.depair (list of lists)") + + - it returns a primitive table: + expect (prototype (f (l))).to_be "table" + - it works with an empty table: + expect (f {}).to_equal {} + - it is the inverse of enpair: + expect (f (l)).to_equal (t) + + +- describe empty: + - before: + f = M.empty + + - context with bad arguments: + badargs.diagnose (f, "std.table.empty (table)") + + - it returns true for an empty table: + expect (f {}).to_be (true) + expect (f {nil}).to_be (true) + - it returns false for a non-empty table: + expect (f {"stuff"}).to_be (false) + expect (f {{}}).to_be (false) + expect (f {false}).to_be (false) + + +- describe enpair: + - before: + t = {"first", "second", third = 4} + l = M.enpair (t) + + f = M.enpair + + - context with bad arguments: + badargs.diagnose (f, "std.table.enpair (table)") + + - it returns a table: + expect (prototype (f (t))).to_be "table" + - it works for an empty table: + expect (f {}).to_equal {} + - it turns a table into a table of pairs: + expect (f (t)).to_equal {{1, "first"}, {2, "second"}, {"third", 4}} + - it is the inverse of depair: + expect (f (t)).to_equal (l) + + +- describe flatten: + - before: + t = {{{"one"}}, "two", {{"three"}, "four"}} + + f = M.flatten + + - context with bad arguments: + badargs.diagnose (f, "std.table.flatten (table)") + + - it returns a table: + expect (type (f (t))).to_be "table" + - it works for an empty table: + expect (f {}).to_equal {} + - it flattens a nested table: + expect (f (t)).to_equal {"one", "two", "three", "four"} + + +- describe insert: + - before: + f, badarg = init (M, this_module, "insert") + + - context with bad arguments: + badargs.diagnose (f, "std.table.insert (table, [int], any)") + + examples { + ["it diagnoses more than 2 arguments with no pos"] = function () + pending "#issue 76" + expect (f ({}, false, false)).to_raise (badarg (3)) + end + } + examples { + ["it diagnoses out of bounds pos arguments"] = function () + expect (f ({}, 0, "x")).to_raise "position 0 out of bounds" + expect (f ({}, 2, "x")).to_raise "position 2 out of bounds" + expect (f ({1}, 5, "x")).to_raise "position 5 out of bounds" + end + } + + - it returns the modified table: + t = {} + expect (f (t, 1)).to_be (t) + - it append a new element at the end by default: + expect (f ({1, 2}, "x")).to_equal {1, 2, "x"} + - it fills holes by default: + expect (f ({1, 2, [5]=3}, "x")).to_equal {1, 2, "x", [5]=3} + - it respects __len when appending: + t = setmetatable ({1, 2, [5]=3}, {__len = function () return 42 end}) + expect (f (t, "x")).to_equal {1, 2, [5]=3, [43]="x"} + - it moves other elements up if necessary: + expect (f ({1, 2}, 1, "x")).to_equal {"x", 1, 2} + expect (f ({1, 2}, 2, "x")).to_equal {1, "x", 2} + expect (f ({1, 2}, 3, "x")).to_equal {1, 2, "x"} + - it inserts a new element according to pos argument: + expect (f ({}, 1, "x")).to_equal {"x"} + + +- describe invert: + - before: + subject = { k1 = 1, k2 = 2, k3 = 3 } + + f = M.invert + + - context with bad arguments: + badargs.diagnose (f, "std.table.invert (table)") + + - it returns a new table: + expect (f (subject)).not_to_be (subject) + - it inverts keys and values in the returned table: + expect (f (subject)).to_equal { "k1", "k2", "k3" } + - it is reversible: + expect (f (f (subject))).to_equal (subject) + - it seems to copy a list of 1..n numbers: + subject = { 1, 2, 3 } + expect (f (subject)).to_copy (subject) + + +- describe keys: + - before: + subject = { k1 = 1, k2 = 2, k3 = 3 } + + f = M.keys + + - context with bad arguments: + badargs.diagnose (f, "std.table.keys (table)") + + - it returns an empty list when subject is empty: + expect (f {}).to_equal {} + - it makes a list of table keys: + cmp = function (a, b) return a < b end + expect (M.sort (f (subject), cmp)).to_equal {"k1", "k2", "k3"} + - it does not guarantee stable ordering: + subject = {} + -- is this a good test? there is a vanishingly small possibility the + -- returned table will have all 10000 keys in the same order... + for i = 10000, 1, -1 do table.insert (subject, i) end + expect (f (subject)).not_to_equal (subject) + + +- describe len: + - before: + f = M.len + + - context with bad arguments: + badargs.diagnose (f, "std.table.len (table)") + + - it returns the length of a table: + expect (f {"a", "b", "c"}).to_be (3) + expect (f {1, 2, 5, a=10, 3}).to_be (4) + - it works with an empty table: + expect (f {}).to_be (0) + - it ignores elements after a hole: + expect (f {1, 2, [5]=3}).to_be (2) + - it respects __len metamethod: + t = setmetatable ({1, 2, [5]=3}, {__len = function () return 42 end}) + expect (f (t)).to_be (42) + + +- describe maxn: + - before: + f = M.maxn + + - context with bad arguments: + badargs.diagnose (f, "std.table.maxn (table)") + + - it returns the largest numeric key of a table: + expect (f {"a", "b", "c"}).to_be (3) + expect (f {1, 2, 5, a=10, 3}).to_be (4) + - it works with an empty table: + expect (f {}).to_be (0) + - it ignores holes: + expect (f {1, 2, [5]=3}).to_be (5) + - it ignores __len metamethod: + t = setmetatable ({1, 2, [5]=3}, {__len = function () return 42 end}) + expect (f (t)).to_be (5) + + +- describe merge: + - before: | + -- Additional merge keys which are moderately unusual + t1 = { k1 = {"v1"}, k2 = "if", k3 = {"?"} } + t2 = { ["if"] = true, [{"?"}] = false, _ = "underscore", k3 = t1.k1 } + t1mt = setmetatable (M.clone (t1), {"meta!"}) + target = {} + for k, v in pairs (t1) do target[k] = v end + for k, v in pairs (t2) do target[k] = v end + + f, badarg = init (M, this_module, "merge") + + - context with bad arguments: + badargs.diagnose (f, "std.table.merge (table, table, [table], ?boolean|:nometa)") + + examples { + ["it diagnoses more than 2 arguments with no pos"] = function () + pending "#issue 76" + expect (f ({}, {}, ":nometa", false)).to_raise (badarg (4)) + end + } + + - it does not create a whole new table: + expect (f (t1, t2)).to_be (t1) + - it does not change t1 when t2 is empty: + expect (f (t1, {})).to_be (t1) + - it copies t2 when t1 is empty: + expect (f ({}, t1)).to_copy (t1) + - it merges keys from t2 into t1: + expect (f (t1, t2)).to_equal (target) + - it gives precedence to values from t2: + original = M.clone (t1) + m = f (t1, t2) -- Merge is destructive, do it once only. + expect (m.k3).to_be (t2.k3) + expect (m.k3).not_to_be (original.k3) + - it only makes a shallow copy of field values: + expect (f ({}, t1).k1).to_be (t1.k1) + + - context with metatables: + - it copies the metatable by default: + expect (getmetatable (f ({}, t1mt))).to_be (getmetatable (t1mt)) + expect (getmetatable (f ({}, t1mt, {"k1"}))).to_be (getmetatable (t1mt)) + - it treats non-table arg3 as nometa parameter: + expect (getmetatable (f ({}, t1mt, ":nometa"))).to_be (nil) + - it treats table arg3 as a map parameter: + expect (getmetatable (f ({}, t1mt, {}))).to_be (getmetatable (t1mt)) + expect (getmetatable (f ({}, t1mt, {"k1"}))).to_be (getmetatable (t1mt)) + - it supports 4 arguments with nometa as arg4: + expect (getmetatable (f ({}, t1mt, {}, ":nometa"))).to_be (nil) + expect (getmetatable (f ({}, t1mt, {"k1"}, ":nometa"))).to_be (nil) + + - context when renaming some keys: + - it renames during merging: + target = { newkey = t1.k1, k2 = t1.k2, k3 = t1.k3 } + expect (f ({}, t1, {k1 = "newkey"})).to_equal (target) + - it does not perturb the value in the renamed key field: + expect (f ({}, t1, {k1 = "newkey"}).newkey).to_be (t1.k1) + + +- describe merge_select: + - before: | + -- Additional merge keys which are moderately unusual + tablekey = {"?"} + t1 = { k1 = {"v1"}, k2 = "if", k3 = {"?"} } + t1mt = setmetatable (M.clone (t1), {"meta!"}) + t2 = { ["if"] = true, [tablekey] = false, _ = "underscore", k3 = t1.k1 } + t2keys = { "if", tablekey, "_", "k3" } + target = {} + for k, v in pairs (t1) do target[k] = v end + for k, v in pairs (t2) do target[k] = v end + + f, badarg = init (M, this_module, "merge_select") + + - context with bad arguments: + badargs.diagnose (f, "std.table.merge_select (table, table, [table], ?boolean|:nometa)") + + examples { + ["it diagnoses more than 2 arguments with no pos"] = function () + pending "#issue 76" + expect (f ({}, {}, ":nometa", false)).to_raise (badarg (4)) + end + } + + - it does not create a whole new table: + expect (f (t1, t2)).to_be (t1) + - it does not change t1 when t2 is empty: + expect (f (t1, {})).to_be (t1) + - it does not change t1 when key list is empty: + expect (f (t1, t2, {})).to_be (t1) + - it copies the named fields: + expect (f ({}, t2, t2keys)).to_equal (t2) + - it makes a shallow copy: + expect (f ({}, t1, {"k1"}).k1).to_be (t1.k1) + - it copies exactly named fields of t2 when t1 is empty: + expect (f ({}, t1, {"k1", "k2", "k3"})).to_copy (t1) + - it merges keys from t2 into t1: + expect (f (t1, t2, t2keys)).to_equal (target) + - it gives precedence to values from t2: + original = M.clone (t1) + m = f (t1, t2, t2keys) -- Merge is destructive, do it once only. + expect (m.k3).to_be (t2.k3) + expect (m.k3).not_to_be (original.k3) + + - context with metatables: + - it copies the metatable by default: + expect (getmetatable (f ({}, t1mt))).to_be (getmetatable (t1mt)) + expect (getmetatable (f ({}, t1mt, {"k1"}))).to_be (getmetatable (t1mt)) + - it treats non-table arg3 as nometa parameter: + expect (getmetatable (f ({}, t1mt, ":nometa"))).to_be (nil) + - it treats table arg3 as a map parameter: + expect (getmetatable (f ({}, t1mt, {}))).to_be (getmetatable (t1mt)) + expect (getmetatable (f ({}, t1mt, {"k1"}))).to_be (getmetatable (t1mt)) + - it supports 4 arguments with nometa as arg4: + expect (getmetatable (f ({}, t1mt, {}, ":nometa"))).to_be (nil) + expect (getmetatable (f ({}, t1mt, {"k1"}, ":nometa"))).to_be (nil) + + +- describe metamethod: + - before: + Object = require "std.object" + objmethod = function () end + obj = Object { + _type = "DerivedObject", + _method = objmethod, + } + + f = M.metamethod + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {{}, subject})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {{}, subject})).not_to_contain_error "was deprecated" + + - it returns nil for missing metamethods: + expect (f (obj, "not a method on obj")).to_be (nil) + - it returns nil for non-function metatable entries: + expect (f (obj, "_type")).to_be (nil) + - it returns a method from the metatable: + expect (f (obj, "_method")).to_be (objmethod) + + +- describe monkey_patch: + - before: + f = M.monkey_patch + + - context with bad arguments: + badargs.diagnose (f, "std.table.monkey_patch (?table)") + + # Ideally, `.to_be (M)`, except that M is cloned from a nested context + # by Specl to prevent us from affecting any other examples, thus the + # address is different by now. + - it returns std.table module table: + expect (f {}).to_equal (M) + - it injects std.table apis into given namespace: + namespace = {} + f (namespace) + for _, api in ipairs (extend_base) do + expect (namespace.table[api]).to_be (M[api]) + end + + +- describe new: + - before: + f = M.new + + - context with bad arguments: + badargs.diagnose (f, "std.table.new (?any, ?table)") + + - context when not setting a default: + - before: default = nil + - it returns a new table when nil is passed: + expect (f (default, nil)).to_equal {} + - it returns any table passed in: + t = { "unique table" } + expect (f (default, t)).to_be (t) + + - context when setting a default: + - before: + default = "default" + - it returns a new table when nil is passed: + expect (f (default, nil)).to_equal {} + - it returns any table passed in: + t = { "unique table" } + expect (f (default, t)).to_be (t) + + - it returns the stored value for existing keys: + t = f ("default") + v = { "unique value" } + t[1] = v + expect (t[1]).to_be (v) + - it returns the constructor default for unset keys: + t = f ("default") + expect (t[1]).to_be "default" + - it returns the actual default object: + default = { "unique object" } + t = f (default) + expect (t[1]).to_be (default) + + +- describe okeys: + - before: + subject = { "v1", k1 = 1, "v2", k2 = 2, "v3", k3 = 3, [10]="v10" } + + f = M.okeys + + - context with bad arguments: + badargs.diagnose (f, "std.table.okeys (table)") + + - it returns an empty list when subject is empty: + expect (f {}).to_equal {} + - it makes an ordered list of table keys: + expect (f (subject)). + to_equal {1, 2, 3, 10, "k1", "k2", "k3"} + + +- describe pack: + - before: + unpack = unpack or table.unpack + t = {"one", "two", "five"} + f = M.pack + - it creates an empty table with no arguments: + expect (f ()).to_equal {} + - it creates a table with arguments as elements: + expect (f ("one", "two", "five")).to_equal (t) + - it is the inverse operation to unpack: + expect (f (unpack (t))).to_equal (t) + + +- describe project: + - before: + l = { + {first = false, second = true, third = true}, + {first = 1, second = 2, third = 3}, + {first = "1st", second = "2nd", third = "3rd"}, + } + + f = M.project + + - context with bad arguments: + badargs.diagnose (f, "std.table.project (any, list of tables)") + + - it returns a table: + expect (prototype (f ("third", l))).to_be "table" + - it works with an empty table: + expect (f ("third", {})).to_equal {} + - it projects a table of fields from a table of tables: + expect (f ("third", l)).to_equal {true, 3, "3rd"} + - it projects fields with a falsey value correctly: + expect (f ("first", l)).to_equal {false, 1, "1st"} + + +- describe remove: + - before: + f = M.remove + + - context with bad arguments: + badargs.diagnose (f, "std.table.remove (table, ?int)") + + examples { + ["it diagnoses out of bounds pos arguments"] = function () + expect (f ({1}, 0)).to_raise "position 0 out of bounds" + expect (f ({1}, 3)).to_raise "position 3 out of bounds" + expect (f ({1}, 5)).to_raise "position 5 out of bounds" + end + } + + - it returns the removed element: + t = {"one", "two", "five"} + expect (f ({"one", 2, 5}, 1)).to_be "one" + - it removes an element from the end by default: + expect (f {1, 2, "five"}).to_be "five" + - it ignores holes: + t = {"second", "first", [5]="invisible"} + expect (f (t)).to_be "first" + expect (f (t)).to_be "second" + - it respects __len when defaulting pos: + t = setmetatable ({1, 2, [43]="invisible"}, {__len = function () return 42 end}) + expect (f (t)).to_be (nil) + expect (f (t)).to_be (nil) + expect (t).to_equal {1, 2, [43]="invisible"} + - it moves other elements down if necessary: + t = {1, 2, 5, "third", "first", "second", 42} + expect (f (t, 5)).to_be "first" + expect (t).to_equal {1, 2, 5, "third", "second", 42} + expect (f (t, 5)).to_be "second" + expect (t).to_equal {1, 2, 5, "third", 42} + expect (f (t, 4)).to_be "third" + expect (t).to_equal {1, 2, 5, 42} + + +- describe ripairs: + - before: + f = M.ripairs + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {{}, subject})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {{}, subject})).not_to_contain_error "was deprecated" + + - it returns a function, the table and a number: + fn, t, i = f {1, 2, 3} + expect ({type (fn), t, type (i)}).to_equal {"function", {1, 2, 3}, "number"} + - it iterates over a array part of a table: + t, u = {1, 2, 3; a=4, b=5, c=6}, {} + for i, v in f (t) do u[i] = v end + expect (u).to_equal {1, 2, 3} + - it returns elements in reverse order: + t, u = {"one", "two", "five"}, {} + for _, v in f (t) do u[#u + 1] = v end + expect (u).to_equal {"five", "two", "one"} + + +- describe shape: + - before: + l = {1, 2, 3, 4, 5, 6} + + f = M.shape + + - context with bad arguments: + badargs.diagnose (f, "std.table.shape (table, table)") + + - it returns a table: + expect (prototype (f ({2, 3}, l))).to_be "table" + - it works for an empty table: + expect (f ({0}, {})).to_equal ({}) + - it returns the result in a new table: + expect (f ({2, 3}, l)).not_to_be (l) + - it does not perturb the argument table: + f ({2, 3}, l) + expect (l).to_equal {1, 2, 3, 4, 5, 6} + - it reshapes a table according to given dimensions: + expect (f ({2, 3}, l)). + to_equal ({{1, 2, 3}, {4, 5, 6}}) + expect (f ({3, 2}, l)). + to_equal ({{1, 2}, {3, 4}, {5, 6}}) + - it treats 0-valued dimensions as an indefinite number: + expect (f ({2, 0}, l)). + to_equal ({{1, 2, 3}, {4, 5, 6}}) + expect (f ({0, 2}, l)). + to_equal ({{1, 2}, {3, 4}, {5, 6}}) + + +- describe size: + - before: | + -- - 1 - --------- 2 ---------- -- 3 -- + subject = { "one", { { "two" }, "three" }, four = 5 } + + f = M.size + + - context with bad arguments: + badargs.diagnose (f, "std.table.size (table)") + + - it counts the number of keys in a table: + expect (f (subject)).to_be (3) + - it counts no keys in an empty table: + expect (f {}).to_be (0) + + +- describe sort: + - before: + subject = { 5, 2, 4, 1, 0, 3 } + target = { 0, 1, 2, 3, 4, 5 } + cmp = function (a, b) return a < b end + + f = M.sort + + - context with bad arguments: + badargs.diagnose (f, "std.table.sort (table, ?function)") + + - it sorts elements in place: + f (subject, cmp) + expect (subject).to_equal (target) + - it returns the sorted table: + expect (f (subject, cmp)).to_equal (target) + + +- describe totable: + - before: + t = {"one", "two", "five"} + mt = { _type = "MockObject", + __totable = function (self) return self.content end } + + f = M.totable + + - it writes a deprecation warning: + setdebug { deprecate = "nil" } + expect (capture (f, {{}})).to_contain_error "was deprecated" + setdebug { deprecate = false } + expect (capture (f, {{}})).not_to_contain_error "was deprecated" + + - it calls object's __totable metamethod: + object = setmetatable ({content = t}, mt) + expect (f (object)).to_be (t) + - it returns a table with no __totable metamethod unchanged: + t = {content = t} + object = setmetatable (t, { _type = "Thing" }) + expect (f (object)).to_be (t) + + +- describe unpack: + - before: + t = {"one", "two", "five"} + f = M.unpack + - it returns nil for an empty table: + expect (f {}).to_be (nil) + - it returns numeric indexed table elements: + expect ({f (t)}).to_equal (t) + - it returns holes in numeric indices as nil: + expect ({f {nil, 2}}).to_equal {[2] = 2} + expect ({f {nil, nil, 3}}).to_equal {[3] = 3} + expect ({f {1, nil, nil, 4}}).to_equal {1, [4] = 4} + - it is the inverse operation to pack: + expect ({f (M.pack ("one", "two", "five"))}).to_equal (t) + + +- describe values: + - before: + subject = { k1 = {1}, k2 = {2}, k3 = {3} } + + f = M.values + + - context with bad arguments: + badargs.diagnose (f, "std.table.values (table)") + + - it returns an empty list when subject is empty: + expect (f {}).to_equal {} + - it makes a list of table values: + cmp = function (a, b) return a[1] < b[1] end + expect (M.sort (f (subject), cmp)).to_equal {{1}, {2}, {3}} + - it does guarantee stable ordering: + subject = {} + -- is this a good test? or just requiring an implementation quirk? + for i = 10000, 1, -1 do table.insert (subject, i) end + expect (f (subject)).to_equal (subject) diff --git a/spec/tree_spec.yaml b/spec/tree_spec.yaml new file mode 100644 index 0000000..025d7fb --- /dev/null +++ b/spec/tree_spec.yaml @@ -0,0 +1,409 @@ +before: | + global_table = "_G" + this_module = "std.tree" + + Tree = require "std.tree" + +specify std.tree: +- before: + prototype = (require "std.object").prototype + t = {foo="foo", fnord={branch={bar="bar", baz="baz"}}, quux="quux"} + tr = Tree (t) + +- context when required: + - context by name: + - it does not touch the global table: + expect (show_apis {added_to=global_table, by=this_module}). + to_equal {} + + - context via the std module: + - it does not touch the global table: + expect (show_apis {added_to=global_table, by="std"}). + to_equal {} + +- describe construction: + - it constructs a new tree: + tr = Tree {} + expect (tr).not_to_be (Tree) + expect (prototype (tr)).to_be "Tree" + - it turns a table argument into a tree: + expect (prototype (Tree (t))).to_be "Tree" + - it does not turn table argument values into sub-Trees: + expect (prototype (tr["fnord"])).to_be "table" + - it understands branched nodes: + expect (tr).to_equal (Tree (t)) + expect (tr[{"fnord"}]).to_equal (t.fnord) + expect (tr[{"fnord", "branch", "bar"}]).to_equal (t.fnord.branch.bar) + - it serves as a prototype for new instances: + obj = tr {} + expect (prototype (obj)).to_be "Tree" + expect (obj).to_equal (tr) + expect (getmetatable (obj)).to_be (getmetatable (tr)) + + +- describe clone: + - before: + subject = { k1 = {"v1"}, k2 = {"v2"}, k3 = {"v3"} } + f = Tree.clone + - it does not just return the subject: + expect (f (subject)).not_to_be (subject) + - it does copy the subject: + expect (f (subject)).to_equal (subject) + - it makes a deep copy: + expect (f (subject).k1).not_to_be (subject.k1) + - it does not perturb the original subject: + target = { k1 = subject.k1, k2 = subject.k2, k3 = subject.k3 } + copy = f (subject) + expect (subject).to_equal (target) + expect (subject).to_be (subject) + - it diagnoses non-table arguments: + expect (f ()).to_raise ("table expected") + expect (f "foo").to_raise ("table expected") + + +- describe ileaves: + - before: + f = Tree.ileaves + l = {} + - it iterates over array part of a table argument: + for v in f {"first", "second", "3rd"} do l[1+#l]=v end + expect (l).to_equal {"first", "second", "3rd"} + - it iterates over array parts of nested table argument: + for v in f {{"one", {"two"}, {{"three"}, "four"}}, "five"} do + l[1+#l]=v + end + expect (l).to_equal {"one", "two", "three", "four", "five"} + - it skips hash part of a table argument: + for v in f {"first", "second"; third = "2rd"} do l[1+#l]=v end + expect (l).to_equal {"first", "second"} + - it skips hash parts of nested table argument: + for v in f {{"one", {two=2}, {{"three"}, four=4}}, foo="bar", "five"} do + l[1+#l]=v + end + expect (l).to_equal {"one", "three", "five"} + - it works on trees too: + for v in f (Tree {Tree {"one", + Tree {two=2}, + Tree {Tree {"three"}, four=4} + }, + foo="bar", "five"}) + do + l[1+#l]=v + end + expect (l).to_equal {"one", "three", "five"} + - it diagnoses non-table arguments: + expect (f ()).to_raise ("table expected") + expect (f "string").to_raise ("table expected") + + +- describe inodes: + - before: | + f = Tree.inodes + local tostring = (require "std.string").tostring + + function traverse (subject) + l = {} + for ty, p, n in f (subject) do + l[1+#l]={ty, Tree.clone (p), n} + end + return l + end + - it iterates over array part of a table argument: | + subject = {"first", "second", "3rd"} + expect (traverse (subject)). + to_equal {{"branch", {}, subject}, -- { + {"leaf", {1}, subject[1]}, -- first, + {"leaf", {2}, subject[2]}, -- second, + {"leaf", {3}, subject[3]}, -- 3rd, + {"join", {}, subject}} -- } + - it iterates over array parts of nested table argument: | + subject = {{"one", {"two"}, {{"three"}, "four"}}, "five"} + expect (traverse (subject)). + to_equal {{"branch", {}, subject}, -- { + {"branch", {1}, subject[1]}, -- { + {"leaf", {1,1}, subject[1][1]}, -- one, + {"branch", {1,2}, subject[1][2]}, -- { + {"leaf", {1,2,1}, subject[1][2][1]}, -- two, + {"join", {1,2}, subject[1][2]}, -- }, + {"branch", {1,3}, subject[1][3]}, -- { + {"branch", {1,3,1}, subject[1][3][1]}, -- { + {"leaf", {1,3,1,1}, subject[1][3][1][1]}, -- three, + {"join", {1,3,1}, subject[1][3][1]}, -- }, + {"leaf", {1,3,2}, subject[1][3][2]}, -- four, + {"join", {1,3}, subject[1][3]}, -- }, + {"join", {1}, subject[1]}, -- }, + {"leaf", {2}, subject[2]}, -- five, + {"join", {}, subject}} -- } + - it skips hash part of a table argument: | + subject = {"first", "second"; third = "3rd"} + expect (traverse (subject)). + to_equal {{"branch", {}, subject}, -- { + {"leaf", {1}, subject[1]}, -- first, + {"leaf", {2}, subject[2]}, -- second, + {"join", {}, subject}} -- } + - it skips hash parts of nested table argument: | + subject = {{"one", {two=2}, {{"three"}, four=4}}, foo="bar", "five"} + expect (traverse (subject)). + to_equal {{"branch", {}, subject}, -- { + {"branch", {1}, subject[1]}, -- { + {"leaf", {1,1}, subject[1][1]}, -- one, + {"branch", {1,2}, subject[1][2]}, -- { + {"join", {1,2}, subject[1][2]}, -- }, + {"branch", {1,3}, subject[1][3]}, -- { + {"branch", {1,3,1}, subject[1][3][1]}, -- { + {"leaf", {1,3,1,1}, subject[1][3][1][1]}, -- three, + {"join", {1,3,1}, subject[1][3][1]}, -- }, + {"join", {1,3}, subject[1][3]}, -- }, + {"join", {1}, subject[1]}, -- }, + {"leaf", {2}, subject[2]}, -- five, + {"join", {}, subject}} -- } + - it works on trees too: | + subject = Tree {Tree {"one", + Tree {two=2}, + Tree {Tree {"three"}, four=4}}, + foo="bar", + "five"} + expect (traverse (subject)). + to_equal {{"branch", {}, subject}, -- { + {"branch", {1}, subject[1]}, -- { + {"leaf", {1,1}, subject[1][1]}, -- one, + {"branch", {1,2}, subject[1][2]}, -- { + {"join", {1,2}, subject[1][2]}, -- }, + {"branch", {1,3}, subject[1][3]}, -- { + {"branch", {1,3,1}, subject[1][3][1]}, -- { + {"leaf", {1,3,1,1}, subject[1][3][1][1]}, -- three, + {"join", {1,3,1}, subject[1][3][1]}, -- }, + {"join", {1,3}, subject[1][3]}, -- }, + {"join", {1}, subject[1]}, -- }, + {"leaf", {2}, subject[2]}, -- five, + {"join", {}, subject}} -- } + - it diagnoses non-table arguments: + expect (f ()).to_raise ("table expected") + expect (f "string").to_raise ("table expected") + + +- describe leaves: + - before: + f = Tree.leaves + l = {} + - it iterates over elements of a table argument: + for v in f {"first", "second", "3rd"} do l[1+#l]=v end + expect (l).to_equal {"first", "second", "3rd"} + - it iterates over elements of a nested table argument: + for v in f {{"one", {"two"}, {{"three"}, "four"}}, "five"} do + l[1+#l]=v + end + expect (l).to_equal {"one", "two", "three", "four", "five"} + - it includes the hash part of a table argument: + for v in f {"first", "second"; third = "3rd"} do l[1+#l]=v end + expect (l).to_equal {"first", "second", "3rd"} + - it includes hash parts of a nested table argument: + for v in f {{"one", {two=2}, {{"three"}, four=4}}, foo="bar", "five"} do + l[1+#l]=v + end + expect (l).to_contain. + a_permutation_of {"one", 2, "three", 4, "bar", "five"} + - it works on trees too: + for v in f (Tree {Tree {"one", + Tree {two=2}, + Tree {Tree {"three"}, four=4} + }, + foo="bar", "five"}) + do + l[1+#l]=v + end + expect (l).to_contain. + a_permutation_of {"one", 2, "three", 4, "bar", "five"} + - it diagnoses non-table arguments: + expect (f ()).to_raise ("table expected") + expect (f "string").to_raise ("table expected") + + +- describe merge: + - before: | + f = Tree.merge + + -- Additional merge keys which are moderately unusual + t1 = Tree { k1 = "v1", k2 = "if", k3 = Tree {"?"} } + t2 = Tree { ["if"] = true, [{"?"}] = false, _ = "underscore", k3 = "v2" } + + target = Tree.clone (t1) + for ty, p, n in Tree.nodes (t2) do + if ty == "leaf" then target[p] = n end + end + - it does not create a whole new table: + expect (f (t1, t2)).to_be (t1) + - it does not change t1 when t2 is empty: + expect (f (t1, Tree {})).to_be (t1) + - it copies t2 when t1 is empty: | + expect (f (Tree {}, t1)).to_copy (t1) + - it merges keys from t2 into t1: | + expect (f (t1, t2)).to_equal (target) + - it gives precedence to values from t2: + original = Tree.clone (t1) + m = f (t1, t2) -- Merge is destructive, do it once only. + expect (m.k3).to_be (t2.k3) + expect (m.k3).not_to_be (original.k3) + - it diagnoses non-table arguments: + expect (f (nil, {})).to_raise ("table expected") + expect (f ({}, nil)).to_raise ("table expected") + + +- describe nodes: + - before: + f = Tree.nodes + + function traverse (subject) + l = {} + for ty, p, n in f (subject) do l[1+#l]={ty, Tree.clone (p), n} end + return l + end + - it iterates over the elements of a table argument: | + subject = {"first", "second", "3rd"} + expect (traverse (subject)). + to_equal {{"branch", {}, subject}, -- { + {"leaf", {1}, subject[1]}, -- first, + {"leaf", {2}, subject[2]}, -- second, + {"leaf", {3}, subject[3]}, -- 3rd, + {"join", {}, subject}} -- } + - it iterates over the elements of nested a table argument: | + subject = {{"one", {"two"}, {{"three"}, "four"}}, "five"} + expect (traverse (subject)). + to_equal {{"branch", {}, subject}, -- { + {"branch", {1}, subject[1]}, -- { + {"leaf", {1,1}, subject[1][1]}, -- one, + {"branch", {1,2}, subject[1][2]}, -- { + {"leaf", {1,2,1}, subject[1][2][1]}, -- two, + {"join", {1,2}, subject[1][2]}, -- }, + {"branch", {1,3}, subject[1][3]}, -- { + {"branch", {1,3,1}, subject[1][3][1]}, -- { + {"leaf", {1,3,1,1}, subject[1][3][1][1]}, -- three, + {"join", {1,3,1}, subject[1][3][1]}, -- }, + {"leaf", {1,3,2}, subject[1][3][2]}, -- four, + {"join", {1,3}, subject[1][3]}, -- }, + {"join", {1}, subject[1]}, -- }, + {"leaf", {2}, subject[2]}, -- five, + {"join", {}, subject}} -- } + - it includes the hash part of a table argument: | + -- like `pairs`, `nodes` can visit elements in any order, so we cannot + -- guarantee the array part is always visited before the hash part, or + -- even that the array elements are visited in order! + subject = {"first", "second"; third = "3rd"} + expect (traverse (subject)).to_contain. + a_permutation_of {{"branch", {}, subject}, -- { + {"leaf", {1}, subject[1]}, -- first, + {"leaf", {2}, subject[2]}, -- second, + {"leaf", {"third"}, subject["third"]}, -- 3rd + {"join", {}, subject}} -- } + - it includes hash parts of a nested table argument: | + -- like `pairs`, `nodes` can visit elements in any order, so we cannot + -- guarantee the array part is always visited before the hash part, or + -- even that the array elements are visited in order! + subject = {{"one", {two=2}, {{"three"}, four=4}}, foo="bar", "five"} + expect (traverse (subject)).to_contain. + a_permutation_of {{"branch", {}, subject}, -- { + {"branch", {1}, subject[1]}, -- { + {"leaf", {1,1}, subject[1][1]}, -- one, + {"branch", {1,2}, subject[1][2]}, -- { + {"leaf", {1,2,"two"}, subject[1][2]["two"]}, -- 2, + {"join", {1,2}, subject[1][2]}, -- }, + {"branch", {1,3}, subject[1][3]}, -- { + {"branch", {1,3,1}, subject[1][3][1]}, -- { + {"leaf", {1,3,1,1}, subject[1][3][1][1]}, -- three, + {"join", {1,3,1}, subject[1][3][1]}, -- }, + {"leaf", {1,3,"four"}, subject[1][3]["four"]}, -- 4, + {"join", {1,3}, subject[1][3]}, -- }, + {"join", {1}, subject[1]}, -- }, + {"leaf", {2}, subject[2]}, -- five, + {"leaf", {"foo"}, subject["foo"]}, -- bar, + {"join", {}, subject}} -- } + - it works on trees too: | + -- like `pairs`, `nodes` can visit elements in any order, so we cannot + -- guarantee the array part is always visited before the hash part, or + -- even that the array elements are visited in order! + subject = Tree {Tree {"one", + Tree {two=2}, + Tree {Tree {"three"}, four=4}}, + foo="bar", + "five"} + expect (traverse (subject)).to_contain. + a_permutation_of {{"branch", {}, subject}, -- { + {"branch", {1}, subject[1]}, -- { + {"leaf", {1,1}, subject[1][1]}, -- one, + {"branch", {1,2}, subject[1][2]}, -- { + {"leaf", {1,2,"two"}, subject[1][2]["two"]}, -- 2, + {"join", {1,2}, subject[1][2]}, -- }, + {"branch", {1,3}, subject[1][3]}, -- { + {"branch", {1,3,1}, subject[1][3][1]}, -- { + {"leaf", {1,3,1,1}, subject[1][3][1][1]}, -- three, + {"join", {1,3,1}, subject[1][3][1]}, -- }, + {"leaf", {1,3,"four"}, subject[1][3]["four"]}, -- 4, + {"join", {1,3}, subject[1][3]}, -- }, + {"join", {1}, subject[1]}, -- }, + {"leaf", {2}, subject[2]}, -- five, + {"leaf", {"foo"}, subject["foo"]}, -- bar, + {"join", {}, subject}} -- } + - it generates path key-lists that are valid __index arguments: | + subject = Tree {"first", Tree {"second"}, "3rd"} + expect (traverse (subject)). + to_equal {{"branch", {}, subject[{}]}, -- { + {"leaf", {1}, subject[{1}]}, -- first, + {"branch", {2}, subject[{2}]}, -- { + {"leaf", {2,1}, subject[{2,1}]}, -- second + {"join", {2}, subject[{2}]}, -- } + {"leaf", {3}, subject[{3}]}, -- 3rd, + {"join", {}, subject[{}]}} -- } + - it diagnoses non-table arguments: + expect (f ()).to_raise ("table expected") + expect (f "string").to_raise ("table expected") + + +- describe __index: + - it returns nil for a missing key: + expect (tr["no such key"]).to_be (nil) + - it returns nil for missing single element key lists: + expect (tr[{"no such key"}]).to_be (nil) + - it returns nil for missing multi-element key lists: + expect (tr[{"fnord", "foo"}]).to_be (nil) + expect (tr[{"no", "such", "key"}]).to_be (nil) + - it returns a value for the given key: + expect (tr["foo"]).to_be "foo" + expect (tr["quux"]).to_be "quux" + - it returns tree root for empty key list: + expect (tr[{}]).to_be (tr) + - it returns values for single element key lists: + expect (tr[{"foo"}]).to_be "foo" + expect (tr[{"quux"}]).to_be "quux" + - it returns values for multi-element key lists: + expect (tr[{"fnord", "branch", "bar"}]).to_be "bar" + expect (tr[{"fnord", "branch", "baz"}]).to_be "baz" + + +- describe __newindex: + - before: + tr = Tree {} + - it stores values for simple keys: + tr["foo"] = "foo" + expect (tr).to_equal (Tree {foo="foo"}) + - it stores values for single element key lists: + tr[{"foo"}] = "foo" + expect (tr).to_equal (Tree {foo="foo"}) + - it stores values for multi-element key lists: + tr[{"foo", "bar"}] = "baz" + expect (tr).to_equal (Tree {foo=Tree {bar="baz"}}) + - it separates branches for diverging key lists: + tr[{"foo", "branch", "bar"}] = "leaf1" + tr[{"foo", "branch", "baz"}] = "leaf2" + expect (tr).to_equal (Tree {foo=Tree {branch=Tree {bar="leaf1", baz="leaf2"}}}) + +- describe __tostring: + - it returns a string: + expect (prototype (tostring (tr))).to_be "string" + - it shows the type name: + expect (tostring (tr)).to_contain "Tree" + - it shows the contents in order: | + tr = Tree {foo = "foo", + fnord = Tree {branch = Tree {bar="bar", baz="baz"}}, + quux = "quux"} + expect (tostring (tr)). + to_contain 'fnord=Tree {branch=Tree {bar=bar, baz=baz}}, foo=foo, quux=quux' diff --git a/src/.gitignore b/src/.gitignore deleted file mode 100755 index cf640d2..0000000 --- a/src/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -/std.lua -/luadoc.css -/index.html -/files -/modules diff --git a/src/base.lua b/src/base.lua deleted file mode 100644 index 3ec219d..0000000 --- a/src/base.lua +++ /dev/null @@ -1,537 +0,0 @@ ---- Adds to the existing global functions -module ("base", package.seeall) - ---- Functional forms of infix operators. --- Defined here so that other modules can write to it. --- @class table --- @name _G.op -_G.op = {} - -require "table_ext" -local list = require "list" -require "string_ext" ---require "io_ext" FIXME: allow loops -local strbuf = require "strbuf" - - ---- Require a module with a particular version --- @param module module to require --- @param min lowest acceptable version (default: any) --- @param too_big lowest version that is too big (default: none) --- @pattern pattern to match version in module.version or --- module.VERSION (default: ".*[%.%d]+" -function _G.require_version (module, min, too_big, pattern) - local function version_to_list (v) - return list.new (string.split (v, "%.")) - end - local function module_version (module, pattern) - return version_to_list (string.match (module.version or module._VERSION, - pattern or ".*[%.%d]+")) - end - local m = require (module) - if min then - assert (module_version (m, pattern) >= version_to_list (min)) - end - if too_big then - assert (module_version (m, pattern) < version_to_list (too_big)) - end - return m -end - ---- Return given metamethod, if any, or nil. --- @param x object to get metamethod of --- @param n name of metamethod to get --- @return metamethod function or nil if no metamethod or not a --- function -function _G.metamethod (x, n) - local _, m = pcall (function (x) - return getmetatable (x)[n] - end, - x) - if type (m) ~= "function" then - m = nil - end - return m -end - ---- Turn tables into strings with recursion detection. --- N.B. Functions calling render should not recurse, or recursion --- detection will not work. --- @see render_OpenRenderer, render_CloseRenderer --- @see render_ElementRenderer, render_PairRenderer --- @see render_SeparatorRenderer --- @param x object to convert to string --- @param open open table renderer --- @param close close table renderer --- @param elem element renderer --- @param pair pair renderer --- @param sep separator renderer --- @return string representation -function _G.render (x, open, close, elem, pair, sep, roots) - local function stop_roots (x) - return roots[x] or render (x, open, close, elem, pair, sep, table.clone (roots)) - end - roots = roots or {} - if type (x) ~= "table" or metamethod (x, "__tostring") then - return elem (x) - else - local s = strbuf.new () - s = s .. open (x) - roots[x] = elem (x) - local i, v = nil, nil - for j, w in pairs (x) do - s = s .. sep (x, i, v, j, w) .. pair (x, j, w, stop_roots (j), stop_roots (w)) - i, v = j, w - end - s = s .. sep(x, i, v, nil, nil) .. close (x) - return s:tostring () - end -end - ---- --- @class function --- @name render_OpenRenderer --- @param t table --- @return open table string - ---- --- @class function --- @name render_CloseRenderer --- @param t table --- @return close table string - ---- --- @class function --- @name render_ElementRenderer --- @param e element --- @return element string - ---- --- @class function --- @name render_PairRenderer --- N.B. the function should not try to render i and v, or treat --- them recursively. --- @param t table --- @param i index --- @param v value --- @param is index string --- @param vs value string --- @return element string - ---- --- @class function --- @name render_SeparatorRenderer --- @param t table --- @param i preceding index (nil on first call) --- @param v preceding value (nil on first call) --- @param j following index (nil on last call) --- @param w following value (nil on last call) --- @return separator string - ---- Extend tostring to work better on tables. --- The original tostring is available as _tostring. --- @class function --- @name _G.tostring --- @param x object to convert to string --- @return string representation -_G._tostring = tostring -- make original tostring available -local _tostring = tostring -function _G.tostring (x) - return render (x, - function () return "{" end, - function () return "}" end, - _tostring, - function (t, _, _, i, v) - return i .. "=" .. v - end, - function (_, i, _, j) - if i and j then - return "," - end - return "" - end) -end - ---- Pretty-print a table. --- @param t table to print --- @param indent indent between levels ["\t"] --- @param spacing space before every line --- @return pretty-printed string -function _G.prettytostring (t, indent, spacing) - indent = indent or "\t" - spacing = spacing or "" - return render (t, - function () - local s = spacing .. "{" - spacing = spacing .. indent - return s - end, - function () - spacing = string.gsub (spacing, indent .. "$", "") - return spacing .. "}" - end, - function (x) - if type (x) == "string" then - return string.format ("%q", x) - else - return tostring (x) - end - end, - function (x, i, v, is, vs) - local s = spacing .. "[" - if type (i) == "table" then - s = s .. "\n" - end - s = s .. is - if type (i) == "table" then - s = s .. "\n" - end - s = s .. "] =" - if type (v) == "table" then - s = s .. "\n" - else - s = s .. " " - end - s = s .. vs - return s - end, - function (_, i) - local s = "\n" - if i then - s = "," .. s - end - return s - end) -end - ---- Turn an object into a table according to __totable metamethod. --- @param x object to turn into a table --- @return table or nil -function _G.totable (x) - local m = metamethod (x, "__totable") - if m then - return m (x) - elseif type (x) == "table" then - return x - else - return nil - end -end - ---- Convert a value to a string. --- The string can be passed to dostring to retrieve the value. ---
TODO: Make it work for recursive tables. --- @param x object to pickle --- @return string such that eval (s) is the same value as x -function _G.pickle (x) - if type (x) == "string" then - return string.format ("%q", x) - elseif type (x) == "number" or type (x) == "boolean" or - type (x) == "nil" then - return tostring (x) - else - x = totable (x) or x - if type (x) == "table" then - local s, sep = "{", "" - for i, v in pairs (x) do - s = s .. sep .. "[" .. pickle (i) .. "]=" .. pickle (v) - sep = "," - end - s = s .. "}" - return s - else - die ("cannot pickle " .. tostring (x)) - end - end -end - ---- Identity function. --- @param ... --- @return the arguments passed to the function -function _G.id (...) - return ... -end - ---- Turn a tuple into a list. --- @param ... tuple --- @return list -function _G.pack (...) - return {...} -end - ---- Partially apply a function. --- @param f function to apply partially --- @param ... arguments to bind --- @return function with ai already bound -function _G.bind (f, ...) - local fix = {...} - return function (...) - return f (unpack (list.concat (fix, {...}))) - end -end - ---- Curry a function. --- @param f function to curry --- @param n number of arguments --- @return curried version of f -function _G.curry (f, n) - if n <= 1 then - return f - else - return function (x) - return curry (bind (f, x), n - 1) - end - end -end - ---- Compose functions. --- @param f1...fn functions to compose --- @return composition of f1 ... fn -function _G.compose (...) - local arg = {...} - local fns, n = arg, #arg - return function (...) - local arg = {...} - for i = n, 1, -1 do - arg = {fns[i] (unpack (arg))} - end - return unpack (arg) - end -end - ---- Memoize a function, by wrapping it in a functable. --- @param fn function that returns a single result --- @return memoized function -function _G.memoize (fn) - return setmetatable ({}, { - __call = function (self, ...) - local k = tostring ({...}) - local v = self[k] - if v == nil then - v = fn (...) - self[k] = v - end - return v - end - }) -end - ---- Evaluate a string. --- @param s string --- @return value of string -function _G.eval (s) - return loadstring ("return " .. s)() -end - ---- An iterator like ipairs, but in reverse. --- @param t table to iterate over --- @return iterator function --- @return the table, as above --- @return #t + 1 -function _G.ripairs (t) - return function (t, n) - n = n - 1 - if n > 0 then - return n, t[n] - end - end, - t, #t + 1 -end - ---- --- @class function --- @name tree_Iterator --- @param n current node --- @return type ("leaf", "branch" (pre-order) or "join" (post-order)) --- @return path to node ({i1...ik}) --- @return node -local function _nodes (it, tr) - local p = {} - local function visit (n) - if type (n) == "table" then - coroutine.yield ("branch", p, n) - for i, v in it (n) do - table.insert (p, i) - visit (v) - table.remove (p) - end - coroutine.yield ("join", p, n) - else - coroutine.yield ("leaf", p, n) - end - end - return coroutine.wrap (visit), tr -end - ---- Tree iterator. --- @see tree_Iterator --- @param tr tree to iterate over --- @return iterator function --- @return the tree, as above -function _G.nodes (tr) - return _nodes (pairs, tr) -end - ---- Tree iterator over numbered nodes, in order. --- @see tree_Iterator --- @param tr tree to iterate over --- @return iterator function --- @return the tree, as above -function _G.inodes (tr) - return _nodes (ipairs, tr) -end - -local function _leaves (it, tr) - local function visit (n) - if type (n) == "table" then - for _, v in it (n) do - visit (v) - end - else - coroutine.yield (n) - end - end - return coroutine.wrap (visit), tr -end - ---- Tree iterator which returns just numbered leaves, in order. --- @param tr tree to iterate over --- @return iterator function --- @return the tree, as above -function _G.ileaves (tr) - return _leaves (ipairs, tr) -end - ---- Tree iterator which returns just leaves. --- @param tr tree to iterate over --- @return iterator function --- @return the tree, as above -function _G.leaves (tr) - return _leaves (pairs, tr) -end - ---- Collect the results of an iterator. --- @param i iterator --- @return results of running the iterator on its arguments -function _G.collect (i, ...) - local t = {} - for e in i (...) do - table.insert (t, e) - end - return t -end - ---- Map a function over an iterator. --- @param f function --- @param i iterator --- @return result table -function _G.map (f, i, ...) - local t = {} - for e in i (...) do - local r = f (e) - if r then - table.insert (t, r) - end - end - return t -end - ---- Filter an iterator with a predicate. --- @param p predicate --- @param i iterator --- @return result table containing elements e for which p (e) -function _G.filter (p, i, ...) - local t = {} - for e in i (...) do - if p (e) then - table.insert (t, e) - end - end - return t -end - ---- Fold a binary function into an iterator. --- @param f function --- @param d initial first argument --- @param i iterator --- @return result -function _G.fold (f, d, i, ...) - local r = d - for e in i (...) do - r = f (r, e) - end - return r -end - ---- Extend to allow formatted arguments. --- @param v value to assert --- @param f format --- @param ... arguments to format --- @return value -function _G.assert (v, f, ...) - if not v then - if f == nil then - f = "" - end - error (string.format (f, ...)) - end - return v -end - ---- Give warning with the name of program and file (if any). --- @param ... arguments for format -function _G.warn (...) - if prog.name then - io.stderr:write (prog.name .. ":") - end - if prog.file then - io.stderr:write (prog.file .. ":") - end - if prog.line then - io.stderr:write (tostring (prog.line) .. ":") - end - if prog.name or prog.file or prog.line then - io.stderr:write (" ") - end - io.writelines (io.stderr, string.format (...)) -end - ---- Die with error. --- @param ... arguments for format -function _G.die (...) - warn (...) - error () -end - --- Function forms of operators. --- FIXME: Make these visible in LuaDoc (also list.concat in list) -_G.op["[]"] = function (t, s) - return t[s] -end -_G.op["+"] = function (a, b) - return a + b -end -_G.op["-"] = function (a, b) - return a - b -end -_G.op["*"] = function (a, b) - return a * b -end -_G.op["/"] = function (a, b) - return a / b -end -_G.op["and"] = function (a, b) - return a and b -end -_G.op["or"] = function (a, b) - return a or b -end -_G.op["not"] = function (a) - return not a -end -_G.op["=="] = function (a, b) - return a == b -end -_G.op["~="] = function (a, b) - return a ~= b -end diff --git a/src/bin.lua b/src/bin.lua deleted file mode 100644 index 46375dc..0000000 --- a/src/bin.lua +++ /dev/null @@ -1,26 +0,0 @@ ---- Binary data utilities - ---- Turn a little-endian word into a number -local function le_to_number (s) - local res = 0 - for i = #s, 1, -1 do - res = res * 256 + string.byte (s, i) - end - return res -end - ---- Turn a little-endian word into a hex string -local function le_to_hex (s) - local res = "" - for i = 1, #s do - res = res .. string.format ("%.2x", string.byte (s, i)) - end - return res -end - -local M = { - le_to_number = le_to_number, - le_to_hex = le_to_hex, -} - -return M diff --git a/src/debug_ext.lua b/src/debug_ext.lua deleted file mode 100644 index c2d5dfd..0000000 --- a/src/debug_ext.lua +++ /dev/null @@ -1,84 +0,0 @@ ---- Additions to the debug module -module ("debug", package.seeall) - -require "debug_init" -require "io_ext" -require "string_ext" - ---- To activate debugging set _DEBUG either to any true value --- (equivalent to {level = 1}), or as documented below. --- @class table --- @name _DEBUG --- @field level debugging level --- @field call do call trace debugging --- @field std do standard library debugging (run examples & test code) - - ---- Print a debugging message --- @param n debugging level, defaults to 1 --- @param ... objects to print (as for print) -function say (n, ...) - local level = 1 - local arg = {n, ...} - if type (arg[1]) == "number" then - level = arg[1] - table.remove (arg, 1) - end - if _DEBUG and - ((type (_DEBUG) == "table" and type (_DEBUG.level) == "number" and - _DEBUG.level >= level) - or level <= 1) then - io.writelines (io.stderr, table.concat (list.map (tostring, arg), "\t")) - end -end - ---- --- The global function debug is an abbreviation for --- debug.say (1, ...) --- @class function --- @name debug --- @see say -getmetatable (_M).__call = - function (self, ...) - say (1, ...) - end - ---- Trace function calls --- Use as debug.sethook (trace, "cr"), which is done automatically --- when _DEBUG.call is set. --- Based on test/trace-calls.lua from the Lua distribution. --- @class function --- @name trace --- @param event event causing the call -local level = 0 -function trace (event) - local t = getinfo (3) - local s = " >>> " .. string.rep (" ", level) - if t ~= nil and t.currentline >= 0 then - s = s .. t.short_src .. ":" .. t.currentline .. " " - end - t = getinfo (2) - if event == "call" then - level = level + 1 - else - level = math.max (level - 1, 0) - end - if t.what == "main" then - if event == "call" then - s = s .. "begin " .. t.short_src - else - s = s .. "end " .. t.short_src - end - elseif t.what == "Lua" then - s = s .. event .. " " .. (t.name or "(Lua)") .. " <" .. - t.linedefined .. ":" .. t.short_src .. ">" - else - s = s .. event .. " " .. (t.name or "(C)") .. " [" .. t.what .. "]" - end - io.writelines (io.stderr, s) -end - --- Set hooks according to _DEBUG -if type (_DEBUG) == "table" and _DEBUG.call then - sethook (trace, "cr") -end diff --git a/src/debug_init.lua b/src/debug_init.lua deleted file mode 100644 index 37ab532..0000000 --- a/src/debug_init.lua +++ /dev/null @@ -1,2 +0,0 @@ --- Debugging is on by default -_G._DEBUG = true diff --git a/src/fstable.lua b/src/fstable.lua deleted file mode 100644 index 7e6574c..0000000 --- a/src/fstable.lua +++ /dev/null @@ -1,123 +0,0 @@ ---- Tables mapped to the filing system --- Only string keys are permitted; package.dirsep characters are --- converted to underscores. --- Values are stored as strings (converted by tostring). --- As with disk operations, a table's elements must be set to nil --- (deleted) before the table itself can be set to nil. - -require "io_ext" -require "table_ext" - -require "io_ext" -require "lfs" -require "posix" - -local new - -local function fsnext (dir) - local f - repeat - f = dir:next () - until f ~= "." and f ~= ".." - return f -end - --- Metamethods for persistent tables -local metatable = {} - -metatable.__index = -function (t, k) - local path = io.catfile (getmetatable (t).directory, k) - local attrs = lfs.attributes (path) - if attrs then - if attrs.mode == "file" then - return io.slurp (path) - elseif attrs.mode == "directory" then - return new (path) - end - end - return attrs -end - -metatable.__newindex = -function (t, k, v) - local ty = type (v) - if ty == "thread" or ty == "function" or ty == "userdata" then - error ("cannot persist a " .. ty .. "") - elseif type (k) ~= "string" then - error ("keys of persistent tables must be of type string") - else - k = string.gsub (k, package.dirsep, "_") - local path = io.catfile (getmetatable (t).directory, k) - local vm = getmetatable (v) - if v == nil then - os.remove (path) - elseif type (v) ~= "table" then - local h = io.open (path, "w") - h:write (tostring (v)) - h:close () - elseif type (vm) == "table" and vm.metatable == metatable then - -- To match Lua semantics we'd hardlink, but that's not allowed for directories - local ok, errmsg = posix.link (vm.directory, path, true) - else - local ok, errmsg = lfs.mkdir (path) - if not ok then - error (errmsg) - end - new (path, v) - end - end -end - -metatable.__pairs = -function (t) - local _, dir = lfs.dir (getmetatable (t).directory) - return function (dir) - local f = fsnext (dir) - if f then - return f, t[f] - end - end, - dir -end - -metatable.__ipairs = -function (t) - local _, dir = lfs.dir (getmetatable (t).directory) - return function (dir, i) - local f = fsnext (dir) - if f then - return i + 1, f - end - end, - dir, 0 -end - ---- Bind a directory to a table --- @param path directory path --- @param t table to merge with directory --- @return table bound to directory -function new (path, t) - if not path:find ("^" .. package.dirsep) then - path = io.catfile (lfs.currentdir (), path) - end - if lfs.attributes (path, "mode") ~= "directory" then - error ("`" .. path .. "' does not exist or is not a directory") - end - local m = table.clone (metatable) - m.directory = path - m.metatable = metatable - local d = setmetatable ({}, m) - if t then - for i, v in pairs (t) do - d[i] = v - end - end - return d -end - -local M = { - new = new, -} - -return M diff --git a/src/getopt.lua b/src/getopt.lua deleted file mode 100644 index 78734df..0000000 --- a/src/getopt.lua +++ /dev/null @@ -1,282 +0,0 @@ ---- Simplified getopt, based on Svenne Panne's Haskell GetOpt.
--- Usage: ---
    ---
  • options = {Option {...}, ...}
    --- getopt.processArgs ()
  • ---
  • Assumes prog = {name[, banner] [, purpose] [, notes] [, usage]}
  • ---
  • Options take a single dash, but may have a double dash.
  • ---
  • Arguments may be given as -opt=arg or -opt arg.
  • ---
  • If an option taking an argument is given multiple times, only the --- last value is returned; missing arguments are returned as 1.
  • ---
--- getOpt, usageInfo and usage can be called directly (see --- below, and the example at the end). Set _DEBUG.std to a non-nil --- value to run the example. ---
    ---
  • TODO: Sort out the packaging. getopt.Option is tedious to type, but --- surely Option shouldn't be in the root namespace?
  • ---
  • TODO: Wrap all messages; do all wrapping in processArgs, not --- usageInfo; use sdoc-like library (see string.format todos).
  • ---
  • TODO: Don't require name to be repeated in banner.
  • ---
  • TODO: Store version separately (construct banner?).
  • ---
- -require "base" -local list = require "list" -require "string_ext" -local Object = require "object" - - ---- Perform argument processing --- @param argIn list of command-line args --- @param options options table --- @return table of remaining non-options --- @return table of option key-value list pairs --- @return table of error messages -local function getOpt (argIn, options) - local noProcess = nil - local argOut, optOut, errors = {[0] = argIn[0]}, {}, {} - -- get an argument for option opt - local function getArg (o, opt, arg, oldarg) - if o.type == nil then - if arg ~= nil then - table.insert (errors, "option `" .. opt .. "' doesn't take an argument") - end - else - if arg == nil and argIn[1] and - string.sub (argIn[1], 1, 1) ~= "-" then - arg = argIn[1] - table.remove (argIn, 1) - end - if arg == nil and o.type == "Req" then - table.insert (errors, "option `" .. opt .. - "' requires an argument `" .. o.var .. "'") - return nil - end - end - return arg or 1 -- make sure arg has a value - end - - local function parseOpt (opt, arg) - local o = options.name[opt] - if o ~= nil then - optOut[o.name[1]] = optOut[o.name[1]] or {} - table.insert (optOut[o.name[1]], getArg (o, opt, arg, optOut[o.name[1]])) - else - table.insert (errors, "unrecognized option `-" .. opt .. "'") - end - end - while argIn[1] do - local v = argIn[1] - table.remove (argIn, 1) - local _, _, dash, opt = string.find (v, "^(%-%-?)([^=-][^=]*)") - local _, _, arg = string.find (v, "=(.*)$") - if v == "--" then - noProcess = 1 - elseif dash == nil or noProcess then -- non-option - table.insert (argOut, v) - else -- option - parseOpt (opt, arg) - end - end - return argOut, optOut, errors -end - - ---- Options table type. --- @class table --- @name _G.Option --- @field name list of names --- @field desc description of this option --- @field type type of argument (if any): Req(uired), --- Opt(ional) --- @field var descriptive name for the argument -_G.Option = Object {_init = {"name", "desc", "type", "var"}} - ---- Options table constructor: adds lookup tables for the option names -local function makeOptions (t) - t = list.concat (t or {}, - {Option {{"version", "V"}, - "output version information and exit"}, - Option {{"help", "h"}, - "display this help and exit"}} - ) - local name = {} - for v in list.elems (t) do - for j, s in pairs (v.name) do - if name[s] then - warn ("duplicate option '%s'", s) - end - name[s] = v - end - end - t.name = name - return t -end - - ---- Produce usage info for the given options --- @param header header string --- @param optDesc option descriptors --- @param pageWidth width to format to [78] --- @return formatted string -local function usageInfo (header, optDesc, pageWidth) - pageWidth = pageWidth or 78 - -- Format the usage info for a single option - -- @param opt the Option table - -- @return options - -- @return description - local function fmtOpt (opt) - local function fmtName (o) - return "-" .. o - end - local function fmtArg () - if opt.type == nil then - return "" - elseif opt.type == "Req" then - return "=" .. opt.var - else - return "[=" .. opt.var .. "]" - end - end - local textName = list.reverse (list.map (fmtName, opt.name)) - textName[#textName] = textName[#textName] .. fmtArg () - return {table.concat ({table.concat (textName, ", ")}, ", "), - opt.desc} - end - local function sameLen (xs) - local n = math.max (unpack (list.map (string.len, xs))) - for i, v in pairs (xs) do - xs[i] = string.sub (v .. string.rep (" ", n), 1, n) - end - return xs, n - end - local function paste (x, y) - return " " .. x .. " " .. y - end - local function wrapper (w, i) - return function (s) - return string.wrap (s, w, i, 0) - end - end - local optText = "" - if #optDesc > 0 then - local cols = list.transpose (list.map (fmtOpt, optDesc)) - local width - cols[1], width = sameLen (cols[1]) - cols[2] = list.map (wrapper (pageWidth, width + 4), cols[2]) - optText = "\n\n" .. - table.concat (list.mapWith (paste, - list.transpose ({sameLen (cols[1]), - cols[2]})), - "\n") - end - return header .. optText -end - ---- Emit a usage message. -local function usage () - local usage, purpose, notes = "[OPTION]... [FILE]...", "", "" - if prog.usage then - usage = prog.usage - end - if prog.purpose then - purpose = "\n" .. prog.purpose - end - if prog.notes then - notes = "\n\n" - if not string.find (prog.notes, "\n") then - notes = notes .. string.wrap (prog.notes) - else - notes = notes .. prog.notes - end - end - io.writelines (usageInfo ("Usage: " .. prog.name .. " " .. usage .. purpose, - options) - .. notes) -end - - ---- Simple getOpt wrapper. --- Adds -version/-V and --- -help/-h automatically; --- stops program if there was an error, or if -help or --- -version was used. -local function processArgs () - local totArgs = #arg - options = makeOptions (options) - local errors - _G.arg, opt, errors = getopt.getOpt (arg, options) - if (opt.version or opt.help) and prog.banner then - io.writelines (prog.banner) - end - if #errors > 0 or opt.help then - local name = prog.name - prog.name = nil - if #errors > 0 then - warn (table.concat (errors, "\n") .. "\n") - end - prog.name = name - usage () - if #errors > 0 then - error () - end - end - if opt.version or opt.help then - os.exit () - end -end -_G.options = nil - - --- A small and hopefully enlightening example: -if type (_DEBUG) == "table" and _DEBUG.std then - - options = makeOptions ({ - Option {{"verbose", "v"}, "verbosely list files"}, - Option {{"output", "o"}, "dump to FILE", "Opt", "FILE"}, - Option {{"name", "n"}, "only dump USER's files", "Req", "USER"}, - }) - - function test (cmdLine) - local nonOpts, opts, errors = getopt.getOpt (cmdLine, options) - if #errors == 0 then - print ("options=" .. tostring (opts) .. - " args=" .. tostring (nonOpts) .. "\n") - else - print (table.concat (errors, "\n") .. "\n" .. - usageInfo ("Usage: foobar [OPTION...] FILE...", - options)) - end - end - - -- FIXME: Turn the following documentation into unit tests - prog = {name = "foobar"} -- for errors - -- Example runs: - test {"foo", "-v"} - -- options={verbose={1}} args={1=foo} - test {"foo", "--", "-v"} - -- options={} args={1=foo,2=-v} - test {"-o", "-V", "-name", "bar", "--name=baz"} - -- options={name={"baz"},version={1},output={1}} args={} - test {"-foo"} - -- unrecognized option `-foo' - -- Usage: foobar [OPTION]... [FILE]... - -- - -- -v, -verbose verbosely list files - -- -o, -output[=FILE] dump to FILE - -- -n, -name=USER only dump USER's files - -- -V, -version output version information and exit - -- -h, -help display this help and exit - -end - --- Public interface -local M = { - getOpt = getOpt, - processArgs = processArgs, - usage = usage, - usageInfo = usageInfo, -} - -return M diff --git a/src/io_ext.lua b/src/io_ext.lua deleted file mode 100644 index d867f51..0000000 --- a/src/io_ext.lua +++ /dev/null @@ -1,115 +0,0 @@ ---- Additions to the io module -module ("io", package.seeall) - -require "base" -require "package_ext" - - --- Get file handle metatable -local file_metatable = getmetatable (io.stdin) - - --- Get an input file handle. --- @param h file handle or name (default: io.input ()) --- @return file handle, or nil on error -local function input_handle (h) - if h == nil then - h = input () - elseif _G.type (h) == "string" then - h = io.open (h) - end - return h -end - ---- Slurp a file handle. --- @param h file handle or name (default: io.input ()) --- @return contents of file or handle, or nil if error -function slurp (h) - h = input_handle (h) - if h then - local s = h:read ("*a") - h:close () - return s - end -end - ---- Read a file or file handle into a list of lines. --- @param h file handle or name (default: io.input ()); --- if h is a handle, the file is closed after reading --- @return list of lines -function readlines (h) - h = input_handle (h) - local l = {} - for line in h:lines () do - table.insert (l, line) - end - h:close () - return l -end -file_metatable.readlines = readlines - ---- Write values adding a newline after each. --- @param h file handle (default: io.output () --- @param ... values to write (as for write) -function writelines (h, ...) - if io.type (h) ~= "file" then - io.write (h, "\n") - h = io.output () - end - for v in ileaves ({...}) do - h:write (v, "\n") - end -end -file_metatable.writelines = writelines - ---- Split a directory path into components. --- Empty components are retained: the root directory becomes {"", ""}. --- @param path path --- @return list of path components -function splitdir (path) - return string.split (path, package.dirsep) -end - ---- Concatenate one or more directories and a filename into a path. --- @param ... path components --- @return path -function catfile (...) - return table.concat ({...}, package.dirsep) -end - ---- Concatenate two or more directories into a path, removing the trailing slash. --- @param ... path components --- @return path -function catdir (...) - return (string.gsub (catfile (...), "^$", package.dirsep)) -end - ---- Perform a shell command and return its output. --- @param c command --- @return output, or nil if error -function shell (c) - return io.slurp (io.popen (c)) -end - ---- Process files specified on the command-line. --- If no files given, process io.stdin; in list of files, --- - means io.stdin. ---
FIXME: Make the file list an argument to the function. --- @param f function to process files with, which is passed --- (name, arg_no) -function processFiles (f) - -- N.B. "arg" below refers to the global array of command-line args - if #arg == 0 then - table.insert (arg, "-") - end - for i, v in ipairs (arg) do - if v == "-" then - io.input (io.stdin) - else - io.input (v) - end - prog.file = v - f (v, i) - end - prog.file = nil -end diff --git a/src/lcs.lua b/src/lcs.lua deleted file mode 100644 index aab9920..0000000 --- a/src/lcs.lua +++ /dev/null @@ -1,61 +0,0 @@ ---- Longest Common Subsequence algorithm. --- After pseudo-code in lecture --- notes by David Eppstein. - - --- Find common subsequences. --- @param a first sequence --- @param b second sequence --- @return list of common subsequences --- @return the length of a --- @return the length of b -local function commonSubseqs (a, b) - local l, m, n = {}, #a, #b - for i = m + 1, 1, -1 do - l[i] = {} - for j = n + 1, 1, -1 do - if i > m or j > n then - l[i][j] = 0 - elseif a[i] == b[j] then - l[i][j] = 1 + l[i + 1][j + 1] - else - l[i][j] = math.max (l[i + 1][j], l[i][j + 1]) - end - end - end - return l, m, n -end - ---- Find the longest common subsequence of two sequences. --- The sequence objects must have an __append metamethod. --- This is provided by string_ext for strings, and by --- list for lists. --- @param a first sequence --- @param b second sequence --- @param s an empty sequence of the same type, to hold the result --- @return the LCS of a and b -local function longestCommonSubseq (a, b, s) - local l, m, n = commonSubseqs (a, b) - local i, j = 1, 1 - local f = getmetatable (s).__append - while i <= m and j <= n do - if a[i] == b[j] then - s = f (s, a[i]) - i = i + 1 - j = j + 1 - elseif l[i + 1][j] >= l[i][j + 1] then - i = i + 1 - else - j = j + 1 - end - end - return s -end - --- Public interface -local M = { - longestCommonSubseq = longestCommonSubseq, -} - -return M diff --git a/src/list.lua b/src/list.lua deleted file mode 100644 index f86a4ff..0000000 --- a/src/list.lua +++ /dev/null @@ -1,404 +0,0 @@ ---- Tables as lists. -require "base" -require "table_ext" - - ---- An iterator over the elements of a list. --- @param l list to iterate over --- @return iterator function which returns successive elements of the list --- @return the list l as above --- @return true -local function elems (l) - local n = 0 - return function (l) - n = n + 1 - if n <= #l then - return l[n] - end - end, - l, true -end - ---- An iterator over the elements of a list, in reverse. --- @param l list to iterate over --- @return iterator function which returns precessive elements of the list --- @return the list l as above --- @return true -local function relems (l) - local n = #l + 1 - return function (l) - n = n - 1 - if n > 0 then - return l[n] - end - end, - l, true -end - ---- Map a function over a list. --- @param f function --- @param l list --- @return result list {f (l[1]), ..., f (l[#l])} -local function map (f, l) - return _G.map (f, elems, l) -end - ---- Map a function over a list of lists. --- @param f function --- @param ls list of lists --- @return result list {f (unpack (ls[1]))), ..., f (unpack (ls[#ls]))} -local function mapWith (f, l) - return _G.map (compose (f, unpack), elems, l) -end - ---- Filter a list according to a predicate. --- @param p predicate (function of one argument returning a boolean) --- @param l list of lists --- @return result list containing elements e of --- l for which p (e) is true -local function filter (p, l) - return _G.filter (p, elems, l) -end - ---- Return a slice of a list. --- (Negative list indices count from the end of the list.) --- @param l list --- @param from start of slice (default: 1) --- @param to end of slice (default: #l) --- @return {l[from], ..., l[to]} -local function slice (l, from, to) - local m = {} - local len = #l - from = from or 1 - to = to or len - if from < 0 then - from = from + len + 1 - end - if to < 0 then - to = to + len + 1 - end - for i = from, to do - table.insert (m, l[i]) - end - return m -end - ---- Return a list with its first element removed. --- @param l list --- @return {l[2], ..., l[#l]} -local function tail (l) - return slice (l, 2) -end - ---- Fold a binary function through a list left associatively. --- @param f function --- @param e element to place in left-most position --- @param l list --- @return result -local function foldl (f, e, l) - return fold (f, e, elems, l) -end - ---- Fold a binary function through a list right associatively. --- @param f function --- @param e element to place in right-most position --- @param l list --- @return result -local function foldr (f, e, l) - return fold (function (x, y) return f (y, x) end, - e, relems, l) -end - ---- Prepend an item to a list. --- @param l list --- @param x item --- @return {x, unpack (l)} -local function cons (l, x) - return {x, unpack (l)} -end - ---- Append an item to a list. --- @param l list --- @param x item --- @return {l[1], ..., l[#l], x} -local function append (l, x) - local r = {unpack (l)} - table.insert (r, x) - return r -end - ---- Concatenate lists. --- @param ... lists --- @return {l1[1], ..., --- l1[#l1], ..., ln[1], ..., --- ln[#ln]} -local function concat (...) - local r = {} - for l in elems ({...}) do - for v in elems (l) do - table.insert (r, v) - end - end - return r -end - ---- Repeat a list. --- @param l list --- @param n number of times to repeat --- @return n copies of l appended together -local function rep (l, n) - local r = {} - for i = 1, n do - r = concat (r, l) - end - return r -end - ---- Reverse a list. --- @param l list --- @return list {l[#l], ..., l[1]} -local function reverse (l) - local m = {} - for i = #l, 1, -1 do - table.insert (m, l[i]) - end - return m -end - ---- Transpose a list of lists. --- This function in Lua is equivalent to zip and unzip in more --- strongly typed languages. --- @param ls {{l1,1, ..., l1,c}, ..., --- {lr,1, ..., lr,c}} --- @return {{l1,1, ..., lr,1}, ..., --- {l1,c, ..., lr,c}} -local function transpose (ls) - local ms, len = {}, #ls - for i = 1, math.max (unpack (map (function (l) return #l end, ls))) do - ms[i] = {} - for j = 1, len do - ms[i][j] = ls[j][i] - end - end - return ms -end - ---- Zip lists together with a function. --- @param f function --- @param ls list of lists --- @return {f (ls[1][1], ..., ls[#ls][1]), ..., f (ls[1][N], ..., ls[#ls][N]) --- where N = max {map (function (l) return #l end, ls)} -local function zipWith (f, ls) - return mapWith (f, transpose (ls)) -end - ---- Project a list of fields from a list of tables. --- @param f field to project --- @param l list of tables --- @return list of f fields -local function project (f, l) - return map (function (t) return t[f] end, l) -end - ---- Turn a table into a list of pairs. ---
FIXME: Find a better name. --- @param t table {i1=v1, ..., --- in=vn} --- @return list {{i1, v1}, ..., --- {in, vn}} -local function enpair (t) - local ls = {} - for i, v in pairs (t) do - table.insert (ls, {i, v}) - end - return ls -end - ---- Turn a list of pairs into a table. ---
FIXME: Find a better name. --- @param ls list {{i1, v1}, ..., --- {in, vn}} --- @return table {i1=v1, ..., --- in=vn} -local function depair (ls) - local t = {} - for v in elems (ls) do - t[v[1]] = v[2] - end - return t -end - ---- Flatten a list. --- @param l list to flatten --- @return flattened list -local function flatten (l) - local m = {} - for v in ileaves (l) do - table.insert (m, v) - end - return m -end - ---- Shape a list according to a list of dimensions. --- --- Dimensions are given outermost first and items from the original --- list are distributed breadth first; there may be one 0 indicating --- an indefinite number. Hence, {0} is a flat list, --- {1} is a singleton, {2, 0} is a list of --- two lists, and {0, 2} is a list of pairs. ---
--- Algorithm: turn shape into all positive numbers, calculating --- the zero if necessary and making sure there is at most one; --- recursively walk the shape, adding empty tables until the bottom --- level is reached at which point add table items instead, using a --- counter to walk the flattened original list. ---
--- @param s {d1, ..., dn} --- @param l list to reshape --- @return reshaped list --- FIXME: Use ileaves instead of flatten (needs a while instead of a --- for in fill function) -local function shape (s, l) - l = flatten (l) - -- Check the shape and calculate the size of the zero, if any - local size = 1 - local zero - for i, v in ipairs (s) do - if v == 0 then - if zero then -- bad shape: two zeros - return nil - else - zero = i - end - else - size = size * v - end - end - if zero then - s[zero] = math.ceil (#l / size) - end - local function fill (i, d) - if d > #s then - return l[i], i + 1 - else - local t = {} - for j = 1, s[d] do - local e - e, i = fill (i, d + 1) - table.insert (t, e) - end - return t, i - end - end - return (fill (1, 1)) -end - ---- Make an index of a list of tables on a given field --- @param f field --- @param l list of tables {t1, ..., --- tn} --- @return index {t1[f]=1, ..., --- tn[f]=n} -local function indexKey (f, l) - local m = {} - for i, v in ipairs (l) do - local k = v[f] - if k then - m[k] = i - end - end - return m -end - ---- Copy a list of tables, indexed on a given field --- @param f field whose value should be used as index --- @param l list of tables {i1=t1, ..., --- in=tn} --- @return index {t1[f]=t1, ..., --- tn[f]=tn} -local function indexValue (f, l) - local m = {} - for i, v in ipairs (l) do - local k = v[f] - if k then - m[k] = v - end - end - return m -end -permuteOn = indexValue - ---- Compare two lists element by element left-to-right --- @param l first list --- @param m second list --- @return -1 if l is less than m, 0 if they --- are the same, and 1 if l is greater than m -local function compare (l, m) - for i = 1, math.min (#l, #m) do - if l[i] < m[i] then - return -1 - elseif l[i] > m[i] then - return 1 - end - end - if #l < #m then - return -1 - elseif #l > #m then - return 1 - end - return 0 -end - --- Metamethods for lists -local metatable = { - -- list .. table = list.concat - __concat = concat, - -- list == list retains its referential meaning - -- list < list = list.compare returns < 0 - __lt = function (l, m) return compare (l, m) < 0 end, - -- list <= list = list.compare returns <= 0 - __le = function (l, m) return compare (l, m) <= 0 end, - __append = append, -} - ---- List constructor. --- Needed in order to use metamethods. --- @param t list (as a table) --- @return list (with list metamethods) -local function new (l) - return setmetatable (l, metatable) -end - --- Function forms of operators -_G.op[".."] = concat - --- Public interface -local M = { - append = append, - compare = compare, - concat = concat, - cons = cons, - depair = depair, - elems = elems, - enpair = enpair, - filter = filter, - flatten = flatten, - foldl = foldl, - foldr = foldr, - indexKey = indexKey, - indexValue = indexValue, - new = new, - map = map, - mapWith = mapWith, - project = project, - relems = relems, - rep = rep, - reverse = reverse, - shape = shape, - slice = slice, - tail = tail, - transpose = transpose, - zipWith = zipWith, -} - -return M diff --git a/src/math_ext.lua b/src/math_ext.lua deleted file mode 100644 index f328105..0000000 --- a/src/math_ext.lua +++ /dev/null @@ -1,27 +0,0 @@ ---- Additions to the math module. -module ("math", package.seeall) - - -local _floor = floor - ---- Extend math.floor to take the number of decimal places. --- @param n number --- @param p number of decimal places to truncate to (default: 0) --- @return n truncated to p decimal places -function floor (n, p) - if p and p ~= 0 then - local e = 10 ^ p - return _floor (n * e) / e - else - return _floor (n) - end -end - ---- Round a number to a given number of decimal places --- @param n number --- @param p number of decimal places to round to (default: 0) --- @return n rounded to p decimal places -function round (n, p) - local e = 10 ^ (p or 0) - return _floor (n * e + 0.5) / e -end diff --git a/src/mbox.lua b/src/mbox.lua deleted file mode 100644 index 996adb0..0000000 --- a/src/mbox.lua +++ /dev/null @@ -1,59 +0,0 @@ ---- mbox parser. --- Based on code by Diego Nahab. - -local function headers (s) - local header = {} - s = "\n" .. s .. "$$$:\n" - local i, j = 1, 1 - while true do - j = string.find (s, "\n%S-:", i + 1) - if not j then - break - end - local _, _, name, val = string.find (string.sub (s, i + 1, j - 1), - "(%S-):(.*)") - val = string.gsub (val or "", "\r\n", "\n") - val = string.gsub (val, "\n%s*", " ") - name = string.lower (name) - if header[name] then - header[name] = header[name] .. ", " .. val - else - header[name] = val - end - i, j = j, i - end - header["$$$"] = nil - return header -end - -local function message (s) - s = string.gsub (s, "^.-\n", "") - local _, s, body - _, _, s, body = string.find(s, "^(.-\n)\n(.*)") - return {header = headers (s or ""), body = body or ""} -end - ---- Parse a mailbox into messages. --- @param s mailbox as a string --- @return list of messages, each of form {header = {...}, body = "..."} -local function parse (s) - local mbox = {} - s = "\n" .. s .. "\nFrom " - local i, j = 1, 1 - while true do - j = string.find (s, "\nFrom ", i + 1) - if not j then - break - end - table.insert (mbox, message (string.sub (s, i + 1, j - 1))) - i, j = j, i - end - return mbox -end - --- Public interface -local M = { - parse = parse, -} - -return M diff --git a/src/modules.lua b/src/modules.lua deleted file mode 100644 index 1ca0556..0000000 --- a/src/modules.lua +++ /dev/null @@ -1,16 +0,0 @@ -return { - "debug_init", - --"strict", - "base", - "package_ext", - "debug_ext", - "table_ext", - "list", - "tree", - "string_ext", - "math_ext", - "io_ext", - "getopt", - "set", - "strbuf", -} diff --git a/src/object.lua b/src/object.lua deleted file mode 100644 index a52f321..0000000 --- a/src/object.lua +++ /dev/null @@ -1,57 +0,0 @@ ---- Prototype-based objects ---
    ---
  • Create an object/class:
  • ---
      ---
    • Either, if the _init field is a list: ---
        ---
      • object/Class = prototype {value, ...; field = value, ...}
      • ---
      • Named values are assigned to the corresponding fields, and unnamed values --- to the fields given by _init.
      • ---
      ---
    • Or, if the _init field is a function: ---
        ---
      • object/Class = prototype (value, ...)
      • ---
      • The given values are passed as arguments to the _init function.
      • ---
      ---
    • An object's metatable is itself.
    • ---
    • Private fields and methods start with "_".
    • ---
    ---
  • Access an object field: object.field
  • ---
  • Call an object method: object:method (...)
  • ---
  • Call a class method: Class.method (object, ...)
  • ---
  • Add a field: object.field = x
  • ---
  • Add a method: function object:method (...) ... end
  • --- - -require "table_ext" - - ---- Root object --- @class table --- @name Object --- @field _init constructor method or list of fields to be initialised by the --- constructor --- @field _clone object constructor which provides the behaviour for _init --- documented above -local Object = { - _init = {}, - - _clone = function (self, ...) - local object = table.clone (self) - if type (self._init) == "table" then - table.merge (object, table.clone_rename (self._init, ...)) - else - object = self._init (object, ...) - end - return setmetatable (object, object) - end, - - -- Sugar instance creation - __call = function (...) - -- First (...) gets first element of list - return (...)._clone (...) - end, -} -setmetatable (Object, Object) - -return Object diff --git a/src/package_ext.lua b/src/package_ext.lua deleted file mode 100644 index 974139a..0000000 --- a/src/package_ext.lua +++ /dev/null @@ -1,15 +0,0 @@ --- Additions to the package module. -module ("package", package.seeall) - - ---- Make named constants for package.config (undocumented --- in 5.1; see luaconf.h for C equivalents). --- @class table --- @name package --- @field dirsep directory separator --- @field pathsep path separator --- @field path_mark string that marks substitution points in a path template --- @field execdir (Windows only) replaced by the executable's directory in a path --- @field igmark Mark to ignore all before it when building luaopen_ function name. -dirsep, pathsep, path_mark, execdir, igmark = - string.match (package.config, "^([^\n]+)\n([^\n]+)\n([^\n]+)\n([^\n]+)\n([^\n]+)") diff --git a/src/parser.lua b/src/parser.lua deleted file mode 100644 index d93afb4..0000000 --- a/src/parser.lua +++ /dev/null @@ -1,267 +0,0 @@ ---- Parser generator. ---

    A parser is created by

    ---
    ---

    p = Parser {grammar}

    ---
    ---

    and called with

    ---
    ---

    result = p:parse (start_token, token_list[, --- from])

    ---
    ---

    where start_token is the non-terminal at which to start parsing --- in the grammar, token_list is a list of tokens of the form

    ---
    ---

    {ty = "token_type", tok = "token_text"}

    ---
    ---

    and from is the token in the list from which to start (the --- default value is 1).

    ---

    The output of the parser is a tree, each of whose --- nodes is of the form:

    ---
    ---

    {ty = symbol, node1 = tree1, --- node2 = tree2, ... [, list]}

    ---
    ---

    where each nodei is a symbolic name, and --- list is the list of trees returned if the corresponding token was a --- list token.

    ---

    A grammar is a table of rules of the form

    ---
    ---

    non-terminal = {production1, --- production2, ...}

    ---
    ---

    plus a special item

    ---
    ---

    lexemes = set.new {"class1", "class2", --- ...}

    ---
    ---

    Each production gives a form that a non-terminal may take. A --- production has the form

    ---
    ---

    production = {"token1", "token2", --- ..., [action][,abstract]}

    ---
    ---

    A production

    ---
      ---
    • must not start with the non-terminal being defined (it must not --- be left-recursive)
    • ---
    • must not be a prefix of a later production in the same --- non-terminal
    • ---
    ---

    Each token may be

    ---
      ---
    • a non-terminal, i.e. a token defined by the grammar
    • ---
        ---
      • an optional symbol is indicated by the suffix _opt
      • ---
      • a list is indicated by the suffix _list, and may be --- followed by _≤separator-symbol> (default is no separator)
      • ---
      ---
    • a lexeme class
    • ---
    • a string to match literally
    • ---
    ---

    The parse tree for a literal string or lexeme class is the string --- that was matched. The parse tree for a non-terminal is a table of --- the form

    ---
    ---

    {ty = "non_terminal_name", tree1, --- tree2, ...}

    ---
    ---

    where the treei are the parse trees for the --- corresponding terminals and non-terminals.

    ---

    An action is of the form

    ---
    ---

    action = function (tree, token, pos) ... return tree_ --- end

    ---
    ---

    It is passed the parse tree for the current node, the token list, --- and the current position in the token list, and returns a new parse --- tree.

    ---

    An abstract syntax rule is of the form

    ---
    ---

    name = {i1, i2, ...}

    ---
    ---

    where i1, i2, --- ... are numbers. This results in a parse tree of the form

    ---
    ---

    {ty = "name"; treei1, --- treei2, ...}

    ---
    ---

    If a production has no abstract syntax rule, the result is the --- parse node for the current node.

    ---

    FIXME: Give lexemes as an extra argument to Parser? ---
    FIXME: Rename second argument to parse method to "tokens"? ---
    FIXME: Make start_token an optional argument to parse? (swap with --- token list) and have it default to the first non-terminal?

    - -local Object = require "object" - - -local Parser = Object {_init = {"grammar"}} - - ---- Parser constructor --- @param grammar parser grammar --- @return parser -function Parser:_init (grammar) - local init = table.clone_rename ({"grammar"}, grammar) - -- Reformat the abstract syntax rules - for rname, rule in pairs (init.grammar) do - if name ~= "lexemes" then - for pnum, prod in ipairs (rule) do - local abstract - for i, v in pairs (prod) do - if type (i) == "string" and i ~= "action" then - if abstract then - die ("more than one abstract rule for " .. rname .. "." - .. tostring (pnum)) - else - if type (v) ~= "table" then - die ("bad abstract syntax rule of type " .. type (v)) - end - abstract = {ty = i, template = v} - prod[i] = nil - end - end - end - if abstract then - prod.abstract = abstract - end - end - end - end - return table.merge (self, init) -end - ---- Parse a token list. --- @param start the token at which to start --- @param token the list of tokens --- @param from the index of the token to start from (default: 1) --- @return parse tree -function Parser:parse (start, token, from) - - local grammar = self.grammar -- for consistency and brevity - local rule, symbol -- functions called before they are defined - - -- Try to parse an optional symbol. - -- @param sym the symbol being tried - -- @param from the index of the token to start from - -- @return the resulting parse tree, or false if empty - -- @return the index of the first unused token, or false to - -- indicate failure - local function optional (sym, from) - local tree, to = symbol (sym, from) - if to then - return tree, to - else - return false, from - end - end - - -- Try to parse a list of symbols. - -- @param sym the symbol being tried - -- @param sep the list separator - -- @param from the index of the token to start from - -- @return the resulting parse tree, or false if empty - -- @return the index of the first unused token, or false to - -- indicate failure - local function list (sym, sep, from) - local tree, to - tree, from = symbol (sym, from) - local list = {tree} - if from == false then - return list, false - end - to = from - repeat - if sep ~= "" then - tree, from = symbol (sep, from) - end - if from then - tree, from = symbol (sym, from) - if from then - table.insert (list, tree) - to = from - end - end - until from == false - return list, to - end - - -- Try to parse a given symbol. - -- @param sym the symbol being tried - -- @param from the index of the token to start from - -- @return tree the resulting parse tree, or false if empty - -- @return the index of the first unused token, or false to - -- indicate failure - symbol = function (sym, from) -- declared at the top - if string.sub (sym, -4, -1) == "_opt" then -- optional symbol - return optional (string.sub (sym, 1, -5), from) - elseif string.find (sym, "_list.-$") then -- list - local _, _, subsym, sep = string.find (sym, "^(.*)_list_?(.-)$") - return list (subsym, sep, from) - elseif grammar[sym] then -- non-terminal - return rule (sym, from) - elseif token[from] and -- not end of token list - ((grammar.lexemes:member (sym) and sym == token[from].ty) or -- lexeme - sym == token[from].tok) -- literal terminal - then - return token[from].tok, from + 1 -- advance to next token - else - return false, false - end - end - - -- Try a production. - -- @param name the name of the current rule - -- @param prod the production (list of symbols) being tried - -- @param from the index of the token to start from - -- @return the parse tree (incomplete if to is false) - -- @return the index of the first unused token, or false to - -- indicate failure - local function production (name, prod, from) - local tree = {ty = name} - local to = from - for prod in _G.list.elems (prod) do - local sym - sym, to = symbol (prod, to) - if to then - table.insert (tree, sym) - else - return tree, false - end - end - if prod.action then - tree = prod.action (tree, token, to) - end - if prod.abstract then - local ntree = {} - ntree.ty = prod.abstract.ty - for i, n in pairs (prod.abstract.template) do - ntree[i] = tree[n] - end - tree = ntree - end - return tree, to - end - - -- Parse according to a particular rule. - -- @param name the name of the rule to try - -- @param from the index of the token to start from - -- @return parse tree - -- @return the index of the first unused token, or false to - -- indicate failure - rule = function (name, from) -- declared at the top - local alt = grammar[name] - local tree, to - for alt in _G.list.elems (alt) do - tree, to = production (name, alt, from) - if to then - return tree, to - end - end - return tree, false - end - - return rule (start, 1, from or 1) -end - -return Parser diff --git a/src/set.lua b/src/set.lua deleted file mode 100644 index 763759e..0000000 --- a/src/set.lua +++ /dev/null @@ -1,167 +0,0 @@ -local list = require "list" - - --- Primitive methods (know about representation) --- The representation is a table whose tags are the elements, and --- whose values are true. - ---- Say whether an element is in a set --- @param s set --- @param e element --- @return true if e is in set, false --- otherwise -local function member (s, e) - return rawget (s.contents, e) == true -end - ---- Insert an element into a set --- @param s set --- @param e element -local function insert (s, e) - rawset (s.contents, e, true) -end - ---- Delete an element from a set --- @param s set --- @param e element -local function delete (s, e) - rawset (s.contents, e, nil) -end - ---- Make a list into a set --- @param l list --- @return set -local metatable = {} -local function new (l) - local s = setmetatable ({contents={}}, metatable) - for e in list.elems (l) do - insert (s, e) - end - return s -end - ---- Iterator for sets --- TODO: Make the iterator return only the key -local function elems (s) - return pairs (s.contents) -end - - --- High level methods (representation-independent) - -local difference, symmetric_difference, intersection, union, subset, equal - ---- Find the difference of two sets --- @param s set --- @param t set --- @return s with elements of t removed -function difference (s, t) - local r = new {} - for e in elems (s) do - if not member (t, e) then - insert (r, e) - end - end - return r -end - ---- Find the symmetric difference of two sets --- @param s set --- @param t set --- @return elements of s and t that are in s or t but not both -function symmetric_difference (s, t) - return difference (union (s, t), intersection (t, s)) -end - ---- Find the intersection of two sets --- @param s set --- @param t set --- @return set intersection of s and t -function intersection (s, t) - local r = new {} - for e in elems (s) do - if member (t, e) then - insert (r, e) - end - end - return r -end - ---- Find the union of two sets --- @param s set --- @param t set --- @return set union of s and t -function union (s, t) - local r = new {} - for e in elems (s) do - insert (r, e) - end - for e in elems (t) do - insert (r, e) - end - return r -end - ---- Find whether one set is a subset of another --- @param s set --- @param t set --- @return true if s is a subset of t, false --- otherwise -function subset (s, t) - for e in elems (s) do - if not member (t, e) then - return false - end - end - return true -end - ---- Find whether one set is a proper subset of another --- @param s set --- @param t set --- @return true if s is a proper subset of t, false otherwise -function propersubset (s, t) - return subset (s, t) and not subset (t, s) -end - ---- Find whether two sets are equal --- @param s set --- @param t set --- @return true if sets are equal, false --- otherwise -function equal (s, t) - return subset (s, t) and subset (t, s) -end - --- Public interface -local M = { - delete = delete, - difference = difference, - elems = elems, - equal = equal, - insert = insert, - intersection = intersection, - member = member, - new = new, - subset = subset, - symmetric_difference = symmetric_difference, - union = union, -} - ---- Metamethods for sets --- set:method () -metatable.__index = M --- set + table = union -metatable.__add = union --- set - table = set difference -metatable.__sub = difference --- set * table = intersection -metatable.__mul = intersection --- set / table = symmetric difference -metatable.__div = symmetric_difference --- set <= table = subset -metatable.__le = subset --- set < table = proper subset -metatable.__lt = propersubset - -return M diff --git a/src/std.lua.in b/src/std.lua.in deleted file mode 100644 index 312bce9..0000000 --- a/src/std.lua.in +++ /dev/null @@ -1,20 +0,0 @@ ---- Lua standard library ---
      ---
    • TODO: Write a style guide (indenting/wrapping, capitalisation, --- function and variable names); library functions should call --- error, not die; OO vs non-OO (a thorny problem).
    • ---
    • TODO: Add tests for each function immediately after the function; --- this also helps to check module dependencies.
    • ---
    • TODO: pre-compile.
    • ---
    -local version = "General Lua libraries / @VERSION@" - -for _, m in ipairs (require "modules") do - _G[m] = require (m) -end - -local M = { - version = version, -} - -return M diff --git a/src/strbuf.lua b/src/strbuf.lua deleted file mode 100644 index 66d77cf..0000000 --- a/src/strbuf.lua +++ /dev/null @@ -1,41 +0,0 @@ ---- String buffers - ---- Create a new string buffer -local metatable = {} -local function new () - return setmetatable ({}, metatable) -end - ---- Add a string to a buffer --- @param b buffer --- @param s string to add --- @return buffer -local function concat (b, s) - table.insert (b, s) - return b -end - ---- Convert a buffer to a string --- @param b buffer --- @return string -local function tostring (b) - return table.concat (b) -end - - --- Public interface -local M = { - concat = concat, - new = new, - tostring = tostring, -} - ---- Metamethods for string buffers --- buffer:method () -metatable.__index = M --- buffer .. string -metatable.__concat = concat --- tostring -metatable.__tostring = tostring - -return M diff --git a/src/strict.lua b/src/strict.lua deleted file mode 100644 index 1eb22c7..0000000 --- a/src/strict.lua +++ /dev/null @@ -1,40 +0,0 @@ ---- Checks uses of undeclared global variables. --- All global variables must be 'declared' through a regular --- assignment (even assigning nil will do) in a top-level --- chunk before being used anywhere or assigned to inside a function. --- From Lua distribution (etc/strict.lua). --- @class module --- @name strict - -local getinfo, error, rawset, rawget = debug.getinfo, error, rawset, rawget - -local mt = getmetatable (_G) -if mt == nil then - mt = {} - setmetatable (_G, mt) -end - -mt.__declared = {} - -local function what () - local d = getinfo (3, "S") - return d and d.what or "C" -end - -mt.__newindex = function (t, n, v) - if not mt.__declared[n] then - local w = what () - if w ~= "main" and w ~= "C" then - error ("assignment to undeclared variable '" .. n .. "'", 2) - end - mt.__declared[n] = true - end - rawset (t, n, v) -end - -mt.__index = function (t, n) - if not mt.__declared[n] and what () ~= "C" then - error ("variable '" .. n .. "' is not declared", 2) - end - return rawget (t, n) -end diff --git a/src/string_ext.lua b/src/string_ext.lua deleted file mode 100644 index a24c9f4..0000000 --- a/src/string_ext.lua +++ /dev/null @@ -1,274 +0,0 @@ ---- Additions to the string module --- TODO: Pretty printing (use in getopt); see source for details. -module ("string", package.seeall) - - --- Write pretty-printing based on: --- --- John Hughes's and Simon Peyton Jones's Pretty Printer Combinators --- --- Based on "The Design of a Pretty-printing Library in Advanced --- Functional Programming", Johan Jeuring and Erik Meijer (eds), LNCS 925 --- http://www.cs.chalmers.se/~rjmh/Papers/pretty.ps --- Heavily modified by Simon Peyton Jones, Dec 96 --- --- Haskell types: --- data Doc list of lines --- quote :: Char -> Char -> Doc -> Doc Wrap document in ... --- (<>) :: Doc -> Doc -> Doc Beside --- (<+>) :: Doc -> Doc -> Doc Beside, separated by space --- ($$) :: Doc -> Doc -> Doc Above; if there is no overlap it "dovetails" the two --- nest :: Int -> Doc -> Doc Nested --- punctuate :: Doc -> [Doc] -> [Doc] punctuate p [d1, ... dn] = [d1 <> p, d2 <> p, ... dn-1 <> p, dn] --- render :: Int Line length --- -> Float Ribbons per line --- -> (TextDetails -> a -> a) What to do with text --- -> a What to do at the end --- -> Doc The document --- -> a Result - - ---- Give strings a subscription operator. --- @param s string --- @param i index --- @return string.sub (s, i, i) if i is a number, or --- falls back to any previous metamethod (by default, string methods) -local old__index = getmetatable ("").__index -getmetatable ("").__index = function (s, i) - if type (i) == "number" then - return sub (s, i, i) - -- Fall back to old metamethods - elseif type (old__index) == "function" then - return old__index (s, i) - else - return old__index[i] - end -end - ---- Give strings an append metamethod. --- @param s string --- @param c character (1-character string) --- @return s .. c -getmetatable ("").__append = function (s, c) - return s .. c -end - ---- Give strings a concat metamethod. --- @param s string --- @param o object --- @return s .. tostring (o) -getmetatable ("").__concat = function (s, o) - return tostring (s) .. tostring (o) -end - ---- Capitalise each word in a string. --- @param s string --- @return capitalised string -function caps (s) - return (gsub (s, "(%w)([%w]*)", - function (l, ls) - return upper (l) .. ls - end)) -end - ---- Remove any final newline from a string. --- @param s string to process --- @return processed string -function chomp (s) - return (gsub (s, "\n$", "")) -end - ---- Escape a string to be used as a pattern --- @param s string to process --- @return --- @param s_: processed string -function escapePattern (s) - return (gsub (s, "(%W)", "%%%1")) -end - --- Escape a string to be used as a shell token. --- Quotes spaces, parentheses, brackets, quotes, apostrophes and --- whitespace. --- @param s string to process --- @return processed string -function escapeShell (s) - return (gsub (s, "([ %(%)%\\%[%]\"'])", "\\%1")) -end - ---- Return the English suffix for an ordinal. --- @param n number of the day --- @return suffix -function ordinalSuffix (n) - n = math.mod (n, 100) - local d = math.mod (n, 10) - if d == 1 and n ~= 11 then - return "st" - elseif d == 2 and n ~= 12 then - return "nd" - elseif d == 3 and n ~= 13 then - return "rd" - else - return "th" - end -end - ---- Extend to work better with one argument. --- If only one argument is passed, no formatting is attempted. --- @param f format --- @param ... arguments to format --- @return formatted string -local _format = format -function format (f, arg1, ...) - if arg1 == nil then - return f - else - return _format (f, arg1, ...) - end -end - ---- Justify a string. --- When the string is longer than w, it is truncated (left or right --- according to the sign of w). --- @param s string to justify --- @param w width to justify to (-ve means right-justify; +ve means --- left-justify) --- @param p string to pad with (default: " ") --- @return justified string -function pad (s, w, p) - p = rep (p or " ", math.abs (w)) - if w < 0 then - return sub (p .. s, w) - end - return sub (s .. p, 1, w) -end - ---- Wrap a string into a paragraph. --- @param s string to wrap --- @param w width to wrap to (default: 78) --- @param ind indent (default: 0) --- @param ind1 indent of first line (default: ind) --- @return wrapped paragraph -function wrap (s, w, ind, ind1) - w = w or 78 - ind = ind or 0 - ind1 = ind1 or ind - assert (ind1 < w and ind < w, - "the indents must be less than the line width") - s = rep (" ", ind1) .. s - local lstart, len = 1, len (s) - while len - lstart > w - ind do - local i = lstart + w - ind - while i > lstart and sub (s, i, i) ~= " " do - i = i - 1 - end - local j = i - while j > lstart and sub (s, j, j) == " " do - j = j - 1 - end - s = sub (s, 1, j) .. "\n" .. rep (" ", ind) .. - sub (s, i + 1, -1) - local change = ind + 1 - (i - j) - lstart = j + change - len = len + change - end - return s -end - ---- Write a number using SI suffixes. --- The number is always written to 3 s.f. --- @param n number --- @return string -function numbertosi (n) - local SIprefix = { - [-8] = "y", [-7] = "z", [-6] = "a", [-5] = "f", - [-4] = "p", [-3] = "n", [-2] = "mu", [-1] = "m", - [0] = "", [1] = "k", [2] = "M", [3] = "G", - [4] = "T", [5] = "P", [6] = "E", [7] = "Z", - [8] = "Y" - } - local t = format("% #.2e", n) - local _, _, m, e = t:find(".(.%...)e(.+)") - local man, exp = tonumber (m), tonumber (e) - local siexp = math.floor (exp / 3) - local shift = exp - siexp * 3 - local s = SIprefix[siexp] or "e" .. tostring (siexp) - man = man * (10 ^ shift) - return tostring (man) .. s -end - ---- Do find, returning captures as a list. --- @param s target string --- @param p pattern --- @param init start position (default: 1) --- @param plain inhibit magic characters (default: nil) --- @return start of match, end of match, table of captures -function tfind (s, p, init, plain) - local function pack (from, to, ...) - return from, to, {...} - end - return pack (p.find (s, p, init, plain)) -end - ---- Do multiple finds on a string. --- @param s target string --- @param p pattern --- @param init start position (default: 1) --- @param plain inhibit magic characters (default: nil) --- @return list of {from, to; capt = {captures}} -function finds (s, p, init, plain) - init = init or 1 - local l = {} - local from, to, r - repeat - from, to, r = tfind (s, p, init, plain) - if from ~= nil then - table.insert (l, {from, to, capt = r}) - init = to + 1 - end - until not from - return l -end - ---- Split a string at a given separator. --- FIXME: Consider Perl and Python versions. --- @param s string to split --- @param sep separator regex --- @return list of strings -function split (s, sep) - -- finds gets a list of {from, to, capt = {}} lists; we then - -- flatten the result, discarding the captures, and prepend 0 (1 - -- before the first character) and append 0 (1 after the last - -- character), and then read off the result in pairs. - local pairs = list.concat ({0}, list.flatten (finds (s, sep)), {0}) - local l = {} - for i = 1, #pairs, 2 do - table.insert (l, sub (s, pairs[i] + 1, pairs[i + 1] - 1)) - end - return l -end - ---- Remove leading matter from a string. --- @param s string --- @param r leading regex (default: "%s+") --- @return string without leading r -function ltrim (s, r) - r = r or "%s+" - return (gsub (s, "^" .. r, "")) -end - ---- Remove trailing matter from a string. --- @param s string --- @param r trailing regex (default: "%s+") --- @return string without trailing r -function rtrim (s, r) - r = r or "%s+" - return (gsub (s, r .. "$", "")) -end - ---- Remove leading and trailing matter from a string. --- @param s string --- @param r leading/trailing regex (default: "%s+") --- @return string without leading/trailing r -function trim (s, r) - return rtrim (ltrim (s, r), r) -end diff --git a/src/table_ext.lua b/src/table_ext.lua deleted file mode 100644 index c2eb4ad..0000000 --- a/src/table_ext.lua +++ /dev/null @@ -1,117 +0,0 @@ --- Extensions to the table module -module ("table", package.seeall) - ---local list = require "list" FIXME: allow require loops - - -local _sort = sort ---- Make table.sort return its result. --- @param t table --- @param c comparator function --- @return sorted table -function sort (t, c) - _sort (t, c) - return t -end - ---- Return whether table is empty. --- @param t table --- @return true if empty or false otherwise -function empty (t) - return not next (t) -end - ---- Find the number of elements in a table. --- @param t table --- @return number of elements in t -function size (t) - local n = 0 - for _ in pairs (t) do - n = n + 1 - end - return n -end - ---- Make the list of keys of a table. --- @param t table --- @return list of keys -function keys (t) - local u = {} - for i, v in pairs (t) do - insert (u, i) - end - return u -end - ---- Make the list of values of a table. --- @param t table --- @return list of values -function values (t) - local u = {} - for i, v in pairs (t) do - insert (u, v) - end - return u -end - ---- Invert a table. --- @param t table {i=v, ...} --- @return inverted table {v=i, ...} -function invert (t) - local u = {} - for i, v in pairs (t) do - u[v] = i - end - return u -end - ---- Make a shallow copy of a table, including any metatable (for a --- deep copy, use tree.clone). --- @param t table --- @param nometa if non-nil don't copy metatable --- @return copy of table -function clone (t, nometa) - local u = {} - if not nometa then - setmetatable (u, getmetatable (t)) - end - for i, v in pairs (t) do - u[i] = v - end - return u -end - ---- Clone a table, renaming some keys. --- @param map table {old_key=new_key, ...} --- @param t table to copy --- @return copy of table -function clone_rename (map, t) - local r = clone (t) - for i, v in pairs (map) do - r[v] = t[i] - r[i] = nil - end - return r -end - ---- Merge one table into another. u is merged into t. --- @param t first table --- @param u second table --- @return first table -function merge (t, u) - for i, v in pairs (u) do - t[i] = v - end - return t -end - ---- Make a table with a default value for unset keys. --- @param x default entry value (default: nil) --- @param t initial table (default: {}) --- @return table whose unset elements are x -function new (x, t) - return setmetatable (t or {}, - {__index = function (t, i) - return x - end}) -end diff --git a/src/tree.lua b/src/tree.lua deleted file mode 100644 index 1f1159b..0000000 --- a/src/tree.lua +++ /dev/null @@ -1,102 +0,0 @@ ---- Tables as trees. -local list = require "list" - - -local metatable = {} ---- Make a table into a tree --- @param t table --- @return tree -local function new (t) - return setmetatable (t or {}, metatable) -end - ---- Tree __index metamethod. --- @param tr tree --- @param i non-table, or list of keys {i1 ... --- in} --- @return tr[i]...[in] if i is a table, or --- tr[i] otherwise -function metatable.__index (tr, i) - -- FIXME: the following doesn't treat list keys correctly - -- e.g. tr[{{1, 2}, {3, 4}}], maybe flatten first? - if type (i) == "table" and #i > 0 then - return list.foldl (op["[]"], tr, i) - else - return rawget (tr, i) - end -end - ---- Tree __newindex metamethod. --- Sets tr[i1]...[in] = v if i is a --- table, or tr[i] = v otherwise --- @param tr tree --- @param i non-table, or list of keys {i1 ... --- in} --- @param v value -function metatable.__newindex (tr, i, v) - if type (i) == "table" then - for n = 1, #i - 1 do - if getmetatable (tr[i[n]]) ~= metatable then - rawset (tr, i[n], new ()) - end - tr = tr[i[n]] - end - rawset (tr, i[#i], v) - else - rawset (tr, i, v) - end -end - ---- Make a deep copy of a tree, including any metatables --- @param t table --- @param nometa if non-nil don't copy metatables --- @return copy of table -local function clone (t, nometa) - local r = {} - if not nometa then - setmetatable (r, getmetatable (t)) - end - local d = {[t] = r} - local function copy (o, x) - for i, v in pairs (x) do - if type (v) == "table" then - if not d[v] then - d[v] = {} - if not nometa then - setmetatable (d[v], getmetatable (v)) - end - o[i] = copy (d[v], v) - else - o[i] = d[v] - end - else - o[i] = v - end - end - return o - end - return copy (r, t) -end - ---- Deep-merge one tree into another. u is merged into ---- t. --- @param t first tree --- @param u second tree --- @return first tree -local function merge (t, u) - for ty, p, n in nodes (u) do - if ty == "leaf" then - t[p] = n - end - end - return t -end - --- Public interface -local M = { - clone = clone, - merge = merge, - new = new, -} - -return M diff --git a/src/xml.lua b/src/xml.lua deleted file mode 100644 index 14a2203..0000000 --- a/src/xml.lua +++ /dev/null @@ -1,75 +0,0 @@ --- XML extensions to string module. --- @class module --- @name xml - -require "base" -require "string_ext" - - ---- Write a table as XML. --- The input format is assumed to be that output by luaexpat. --- @param t table to print. --- In each element, tag is its name, attr is the table of attributes, --- and the sub-elements are held in the integer keys --- @param indent indent between levels (default: "\t") --- @param spacing space before every line --- @returns XML string -function string.writeXML (t, indent, spacing) - indent = indent or "\t" - spacing = spacing or "" - return render (t, - function (x) - spacing = spacing .. indent - if x.tag then - local s = "<" .. x.tag - if type (x.attr) == "table" then - for i, v in pairs (x.attr) do - if type (i) ~= "number" then - -- luaexpat gives names of attributes in list elements - s = s .. " " .. tostring (i) .. "=" .. string.format ("%q", tostring (v)) - end - end - end - if #x == 0 then - s = s .. " /" - end - s = s .. ">" - return s - end - return "" - end, - function (x) - spacing = string.gsub (spacing, indent .. "$", "") - if x.tag and #x > 0 then - return spacing .. "" - end - return "" - end, - function (s) - s = tostring (s) - s = string.gsub (s, "&([%S]+)", - function (s) - if not string.match (s, "^#?%w+;") then - return "&" .. s - else - return "&" .. s - end - end) - s = string.gsub (s, "<", "<") - s = string.gsub (s, ">", ">") - return s - end, - function (x, i, v, is, vs) - local s = "" - if type (i) == "number" then - s = spacing .. vs - end - return s - end, - function (_, i, _, j) - if type (i) == "number" or type (j) == "number" then - return "\n" - end - return "" - end) -end diff --git a/stdlib-41.2.2-1.rockspec b/stdlib-41.2.2-1.rockspec new file mode 100644 index 0000000..2de8067 --- /dev/null +++ b/stdlib-41.2.2-1.rockspec @@ -0,0 +1,57 @@ +local _MODREV, _SPECREV = '41.2.2', '-1' + +package = 'stdlib' +version = _MODREV .. _SPECREV + +description = { + summary = 'General Lua Libraries', + detailed = [[ + stdlib is a library of modules for common programming tasks, + including list, table and functional operations, objects, pickling, + pretty-printing and command-line option parsing. + ]], + homepage = 'http://lua-stdlib.github.io/lua-stdlib', + license = 'MIT/X11', +} + +source = { + url = 'http://github.com/lua-stdlib/lua-stdlib/archive/v' .. _MODREV .. '.zip', + dir = 'lua-stdlib-' .. _MODREV, +} + +dependencies = { + 'lua >= 5.1, < 5.5', +} + +build = { + type = 'builtin', + modules = { + std = 'lib/std.lua', + ['std.base'] = 'lib/std/base.lua', + ['std.container'] = 'lib/std/container.lua', + ['std.debug'] = 'lib/std/debug.lua', + ['std.debug_init'] = 'lib/std/debug_init/init.lua', + ['std.functional'] = 'lib/std/functional.lua', + ['std.io'] = 'lib/std/io.lua', + ['std.list'] = 'lib/std/list.lua', + ['std.math'] = 'lib/std/math.lua', + ['std.object'] = 'lib/std/object.lua', + ['std.operator'] = 'lib/std/operator.lua', + ['std.optparse'] = 'lib/std/optparse.lua', + ['std.package'] = 'lib/std/package.lua', + ['std.set'] = 'lib/std/set.lua', + ['std.strbuf'] = 'lib/std/strbuf.lua', + ['std.strict'] = 'lib/std/strict.lua', + ['std.string'] = 'lib/std/string.lua', + ['std.table'] = 'lib/std/table.lua', + ['std.tree'] = 'lib/std/tree.lua', + }, +} + +if _MODREV == 'git' then + build.copy_directories = nil + + source = { + url = 'git://github.com/lua-stdlib/lua-stdlib.git', + } +end diff --git a/stdlib.rockspec.in b/stdlib.rockspec.in deleted file mode 100644 index bc69d01..0000000 --- a/stdlib.rockspec.in +++ /dev/null @@ -1,25 +0,0 @@ -package="stdlib" -version="@VERSION@-1" -source = { - url = "git://github.com/rrthomas/lua-stdlib.git", - branch = "release-v@VERSION@", -} -description = { - summary = "General Lua libraries", - detailed = [[ - stdlib is a library of modules for common programming tasks, - including list, table and functional operations, regexps, objects, - pickling, pretty-printing and getopt. - ]], - homepage = "http://github.com/rrthomas/lua-stdlib/", - license = "MIT/X11" -} -dependencies = { - "lua >= @LUA_MIN_VERSION@" -} -build = { - type = "command", - build_command = "LUA=$(LUA) CPPFLAGS=-I$(LUA_INCDIR) ./configure --prefix=$(PREFIX) --libdir=$(LIBDIR) --datadir=$(LUADIR) && make clean && make", - install_command = "make install", - copy_directories = {} -} diff --git a/template.lua b/template.lua deleted file mode 100644 index 1d2abe7..0000000 --- a/template.lua +++ /dev/null @@ -1,33 +0,0 @@ -#! /usr/bin/env lua -prog = { - name = "", - banner = " VERSION (DATE) by AUTHOR )", - purpose = "", -} - - -require "std" - - --- Process a file -function main (file, number) -end - - --- Command-line options -options = { - Option {{"test", "t"}, - "test option"}, -} - --- Main routine -getopt.processArgs () -if table.getn (arg) == 0 then - getopt.dieWithUsage () -end -io.processFiles (main) - - --- Changelog - --- 0.1 Program started