├── .github └── workflows │ └── spec.yml ├── .gitignore ├── .luacov ├── LICENSE.md ├── Makefile ├── NEWS.md ├── README.md ├── build-aux └── config.ld.in ├── lib └── typecheck │ └── init.lua ├── spec ├── init_spec.yaml ├── spec_helper.lua └── version_spec.yaml └── typecheck-git-1.rockspec /.github/workflows/spec.yml: -------------------------------------------------------------------------------- 1 | name: spec 2 | 3 | on: 4 | push: 5 | branches: [ '*' ] 6 | pull_request: 7 | branches: [ 'master' ] 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | lua-version: ["5.4", "5.3", "5.2", "5.1", "luajit"] 15 | strict: ["std.strict", ""] 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | 22 | - uses: leafo/gh-actions-lua@v10 23 | with: 24 | luaVersion: ${{ matrix.lua-version }} 25 | 26 | - uses: leafo/gh-actions-luarocks@v4 27 | with: 28 | luaRocksVersion: 3.9.2 29 | 30 | - name: install 31 | run: | 32 | sudo apt-get install -y libyaml-dev 33 | test -n "${{ matrix.strict }}" && luarocks install std.strict || true 34 | 35 | - name: build 36 | run: | 37 | luarocks install ldoc 38 | make all doc 39 | luarocks make 40 | 41 | - name: test 42 | run: | 43 | luarocks test --prepare 44 | make check SPECL_OPTS='-vfreport --coverage' 45 | bash <(curl -s https://codecov.io/bash) -f luacov.report.out 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .DS_Store 3 | /build-aux/config.ld 4 | /doc 5 | /lib/typecheck/version.lua 6 | /luacov.*.out 7 | /typecheck-*.src.rock 8 | /typecheck-*.tar.gz 9 | -------------------------------------------------------------------------------- /.luacov: -------------------------------------------------------------------------------- 1 | return { 2 | -- filename to store stats collected 3 | ['statsfile'] = 'luacov.stats.out', 4 | 5 | -- filename to store report 6 | ['reportfile'] = 'luacov.report.out', 7 | 8 | -- luacov.stats file updating frequency. 9 | -- The lower this value - the more frequenty results will be written out to luacov.stats 10 | -- You may want to reduce this value for short lived scripts (to for example 2) to avoid losing coverage data. 11 | ['savestepsize'] = 100, 12 | 13 | -- Run reporter on completion? (won't work for ticks) 14 | runreport = true, 15 | 16 | -- Delete stats file after reporting? 17 | deletestats = false, 18 | 19 | -- Process Lua code loaded from raw strings 20 | -- (that is, when the 'source' field in the debug info 21 | -- does not start with '@') 22 | codefromstrings = false, 23 | 24 | -- Patterns for files to include when reporting 25 | -- all will be included if nothing is listed 26 | -- (exclude overrules include, do not include 27 | -- the .lua extension, path separator is always '/') 28 | ['include'] = { 29 | 'lib/typecheck/init$', 30 | 'lib/typecheck/version$', 31 | }, 32 | 33 | -- Patterns for files to exclude when reporting 34 | -- all will be included if nothing is listed 35 | -- (exclude overrules include, do not include 36 | -- the .lua extension, path separator is always '/') 37 | ['exclude'] = { 38 | 'luacov$', 39 | 'luacov/reporter$', 40 | 'luacov/defaults$', 41 | 'luacov/runner$', 42 | 'luacov/stats$', 43 | 'luacov/tick$', 44 | }, 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014-2023 Gary V. Vaughan 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without restriction, 6 | including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, 8 | and to permit persons to whom the Software is furnished to do so, 9 | subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGE- 17 | MENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 18 | FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 19 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Gradual Function Type Checking for Lua 5.1, 5.2, 5.3 & 5.4 2 | # Copyright (C) 2014-2023 Gary V. Vaughan 3 | 4 | LDOC = ldoc 5 | LUA = lua 6 | MKDIR = mkdir -p 7 | SED = sed 8 | SPECL = specl 9 | 10 | VERSION = git 11 | 12 | luadir = lib/typecheck 13 | SOURCES = \ 14 | $(luadir)/init.lua \ 15 | $(luadir)/version.lua \ 16 | $(NOTHING_ELSE) 17 | 18 | 19 | all: $(luadir)/version.lua 20 | 21 | 22 | $(luadir)/version.lua: Makefile 23 | echo "return 'Gradual Function Typechecks / $(VERSION)'" > '$@' 24 | 25 | doc: build-aux/config.ld $(SOURCES) 26 | $(LDOC) -c build-aux/config.ld . 27 | 28 | build-aux/config.ld: build-aux/config.ld.in Makefile 29 | $(SED) -e 's,@PACKAGE_VERSION@,$(VERSION),' '$<' > '$@' 30 | 31 | 32 | CHECK_ENV = LUA=$(LUA) 33 | 34 | check: $(SOURCES) 35 | LUA=$(LUA) $(SPECL) $(SPECL_OPTS) spec/*_spec.yaml 36 | 37 | 38 | .FORCE: 39 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # typecheck NEWS - User visible changes 2 | 3 | ## Noteworthy changes in release ?.? (????-??-??) [?] 4 | 5 | 6 | ## Noteworthy changes in release 3.0 (2023-01-31) [stable] 7 | 8 | ### New Features 9 | 10 | - `ARGCHECK_FRAME` is now exported for use when writing your own 11 | functions that need to adjust any stack `level` argument they 12 | support, rather than having to divine if from the internals of 13 | `_debug.argcheck`. 14 | 15 | - Accept either of 'integer' or 'int' in an argcheck typespec. 16 | 17 | - When diagnosing a type mismatch, be specific about unexpected 18 | 'integer' or 'float' rather than just 'number' 19 | 20 | ``` 21 | bad argument #1 to 'getfenv' (integer expected, got float) 22 | ``` 23 | 24 | - Instead of diagnosing an argument mismatch against `?any` as 25 | 'expected any value or nil, got no value', we now get 26 | 'expected argument, got no value'. Similarly, for missing 27 | results for `?any' we now get 'expected result, go no value'. 28 | 29 | - Support importing into another project directly with: 30 | 31 | ```sh 32 | $ cp ../typecheck/lib/typecheck/init.lua lib/typecheck.lua 33 | ``` 34 | 35 | - Multi-typed specifications are sorted asciibetically in error 36 | messages by `argerror` and `resulterror` to make writing tests 37 | for typechecked functions easier. 38 | 39 | ### Bug fixes 40 | 41 | - No matter whether 'int' or 'integer' is specified, always use 42 | 'integer' in error messages, for consistency with 'bool' as an 43 | alias of 'boolean'. 44 | 45 | - When 'table of int', 'list of funcs', 'table of bool' or 46 | similar are specified, consistently use 'table of integers', 47 | 'list of functions', 'table of booleans', etc. 48 | 49 | - `argscheck` now correctly diagnoses unexpected `nil` arguments 50 | with 'got nil', and missing arguments with 'got no value'. 51 | Likewise for result errors about unexpected `nil` results and 52 | missing return values. 53 | 54 | - Diagnose passing of incompatible objects with a `__tostring` 55 | metamethod to parameters that require a string instead of silently 56 | coercing to a string. 57 | 58 | - Functable's are most definitely NOT functors as that term is used 59 | by functional programmers. The library will accept 'functor' 60 | as a synonym for backwards compatibility, but otherwise we now 61 | use the term functable everywhere to avoid confusion. 62 | 63 | ### Incompatible changes 64 | 65 | - `types.stringy` is no longer available; silently converting any 66 | object passed to a string parameter by calling the `__tostring` 67 | metamethod has unintended consequences for the function behaviour 68 | and behaves differently when typechecking is disabled and the 69 | conversion to string is consequently disabled. 70 | 71 | 1. If you know you want an argument converted to a string before 72 | passing to typechecked function, you should call `tostring` on 73 | it at the call-site. 74 | 2. If you know you want tables with `__tostring` metamethods to 75 | be valid arguments, make the typespec 'string|table' and call 76 | `tostring` on it in your function. 77 | 78 | - Number-like string arguments are considered a mismatch for 79 | previously compatible expected number types for the same reason. 80 | As above, either call `tonumber` at the call-site, or make the 81 | typespec 'number|string' and call `tonumber` inside your function. 82 | 83 | - `argcheck`, `argerror`, `argscheck` and `resulterror` no longer 84 | accept a table with a `__tostring` metamethod as a substitute for 85 | an actual string for those parameters that require a string. 86 | 87 | - `extramsg_mismatch` provides more specific details when the 88 | expected argument is 'int' or 'integer': 89 | 90 | ``` 91 | bad argument #1 to 'getfenv' (float has no integer representation) 92 | ``` 93 | 94 | 95 | ## Noteworthy changes in release 2.1 (2020-04-24) [stable] 96 | 97 | ### New features 98 | 99 | - Initial support for Lua 5.4. 100 | 101 | - No longer depends on `std.normalize`. 102 | 103 | - No need to preinstall `std.strict` for deployment, of course that 104 | means without runtime global variable checking. In development 105 | environments, `std.strict` will be loaded and used for runtime 106 | checks as before. 107 | 108 | ### Bug fixes 109 | 110 | - works with std.strict again. 111 | 112 | 113 | ## Noteworthy changes in release 2.0 (2018-01-15) [stable] 114 | 115 | ### Incompatible changes 116 | 117 | - Use `std._debug` hints to enable or disable runtime type 118 | checking instead of shared global `_DEBUG` symbol. 119 | 120 | 121 | ## Noteworthy changes in release 1.1 (2017-07-07) [stable] 122 | 123 | ### New features 124 | 125 | - Support type annotations with concat decorators. 126 | 127 | ```lua 128 | local my_function = argscheck "my_function (int, int) => int" .. 129 | function (a, b) 130 | return a + b 131 | end 132 | ``` 133 | 134 | - New `check` method for ensuring that a single value matches a 135 | given type specifier. 136 | 137 | - New "functor" type specifier for matching objects with a `__call` 138 | metamethod - note, most `std.object` derived types will match 139 | successfully against the "functor" specifier. 140 | 141 | - New "callable" type specifier for matching both objects with a 142 | `__call` metamethod, and objects for which Lua `type` returns 143 | "function" - note, this is exactly what the "function" specifier 144 | used to do. 145 | 146 | ### Bug fixes 147 | 148 | - `argerror` and `resulterror` pass level 0 argument through to 149 | `error` to suppress file and line number prefix to error message. 150 | 151 | ### Incompatible changes 152 | 153 | - The "function" (and "func") type specifiers no longer match objects 154 | with a `__call` metamethod. Use the new "callable" type specifier 155 | to match both types in the way that "function" used to, including 156 | most `std.object` derived types. 157 | 158 | - Rather than a hardcoded `typecheck._VERSION` string, install a 159 | generated `typecheck.version` module, and autoload it on reference. 160 | 161 | 162 | ## Noteworthy changes in release 1.0 (2016-01-25) [stable] 163 | 164 | ### New features 165 | 166 | - Initial release, now separated out from lua-stdlib. 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Gradual type checking for Lua functions 2 | ======================================= 3 | 4 | Copyright (C) 2014-2023 [Gary V. Vaughan][github] 5 | 6 | [![License](http://img.shields.io/:license-mit-blue.svg)](http://mit-license.org) 7 | [![workflow status](https://github.com/gvvaughan/typecheck/actions/workflows/spec.yml/badge.svg?branch=master)](https://github.com/gvvaughan/typecheck/actions) 8 | [![codecov.io](https://codecov.io/gh/gvvaughan/typecheck/branch/master/graph/badge.svg)](https://codecov.io/gh/gvvaughan/typecheck) 9 | 10 | A Luaish run-time gradual type checking system, for argument and return 11 | types at function boundaries with simple annotations that can be disabled 12 | in production code. Its API and type mismatch errors are modelled on the 13 | core Lua C-language `argcheck ()` API. 14 | 15 | - *Luaish*: Type check failures show error messages in the same format 16 | as Lua itself; 17 | - *run time*: Without changing any library code, the application can 18 | decide at run time whether to enable type checking as it loads the 19 | library; 20 | - *gradual*: Type checks can be introduced to the functions in your code 21 | gradually, to as few or as many as seem useful; 22 | - *type checking*: function argument types and return types are checked 23 | against the specification, and raise an error if some don't match 24 | 25 | This is a light-weight library for [Lua][] 5.1 (including [LuaJIT][]), 26 | 5.2, 5.3 and 5.4 written in pure Lua. 27 | 28 | [github]: http://github.com/gvvaughan/typecheck/ "Github repository" 29 | [lua]: http://www.lua.org "The Lua Project" 30 | [luajit]: http://luajit.org "The LuaJIT Project" 31 | 32 | 33 | Installation 34 | ------------ 35 | 36 | The simplest and best way to install typecheck is with [LuaRocks][]. To 37 | install the latest release (recommended): 38 | 39 | ```bash 40 | luarocks install typecheck 41 | ``` 42 | 43 | To install current git master (for testing, before submitting a bug 44 | report for example): 45 | 46 | ```bash 47 | luarocks install http://raw.githubusercontent.com/gvvaughan/typecheck/master/typecheck-git-1.rockspec 48 | ``` 49 | 50 | The best way to install without [LuaRocks][] is to copy the entire 51 | `lib/typecheck` directory into a subdirectory of your package search path, 52 | along with the modules listed as dependencies in the included rockspec. 53 | 54 | [luarocks]: http://www.luarocks.org "Lua package manager" 55 | 56 | 57 | Use 58 | --- 59 | 60 | Add expressive type assertions on specific arguments right in the body 61 | of a function, for cases where that function can only handle specific 62 | types in that argument: 63 | 64 | ```lua 65 | local argcheck = require "typecheck".argcheck 66 | 67 | local function case (with, branches) 68 | argcheck ("std.functional.case", 2, "#table", branches) 69 | ... 70 | ``` 71 | 72 | Or more comprehensively, wrap exported functions to raise an error if 73 | the return or argument types do not meet your specification: 74 | 75 | ```lua 76 | return { 77 | len = argscheck ("string.len (string) => int", string.len), 78 | ... 79 | ``` 80 | 81 | Alternatively, argscheck can be used as an annotation, which makes it 82 | look nicer when used at declaration time: 83 | 84 | ```lua 85 | local my_function = argscheck "my_function (int, int) => int" .. 86 | function (a, b) 87 | return a + b 88 | end 89 | ``` 90 | 91 | By default, type checks are performed on every call. But, they can be 92 | turned off and all of the run-time overhead eliminated in production 93 | code, either by calling `require 'std._debug' (false)` prior to loading 94 | `typecheck` or, more precisely, by setting 95 | `require 'std._debug'.argcheck = false` 96 | 97 | 98 | 99 | Documentation 100 | ------------- 101 | 102 | The latest release is [documented with LDoc][github.io]. 103 | Pre-built HTML files are included in the [release tarball][releases]. 104 | 105 | [github.io]: http://gvvaughan.github.io/typecheck 106 | [releases]: https://github.com/gvvaughan/typecheck/releases 107 | 108 | 109 | Bug reports and code contributions 110 | ---------------------------------- 111 | 112 | Please make bug reports and suggestions as [GitHub Issues][issues]. 113 | Pull requests are especially appreciated. 114 | 115 | But first, please check that your issue has not already been reported by 116 | someone else, and that it is not already fixed by [master][github] in 117 | preparation for the next release (see Installation section above for how 118 | to temporarily install master with [LuaRocks][]). 119 | 120 | There is no strict coding style, but please bear in mind the following 121 | points when proposing changes: 122 | 123 | 0. Follow existing code. There are a lot of useful patterns and avoided 124 | traps there. 125 | 126 | 1. 3-character indentation using SPACES in Lua sources: It makes rogue 127 | TABs easier to see, and lines up nicely with 'if' and 'end' keywords. 128 | 129 | 2. Simple strings are easiest to type using single-quote delimiters, 130 | saving double-quotes for where a string contains apostrophes. 131 | 132 | 3. Save horizontal space by only using SPACEs where the parser requires 133 | them. 134 | 135 | 4. Use vertical space to separate out compound statements to help the 136 | coverage reports discover untested lines. 137 | 138 | 5. Prefer explicit string function calls over object methods, to mitigate 139 | issues with monkey-patching in caller environment. 140 | 141 | [issues]: http://github.com/gvvaughan/typecheck/issues 142 | -------------------------------------------------------------------------------- /build-aux/config.ld.in: -------------------------------------------------------------------------------- 1 | --[[ 2 | Gradual Function Type Checking for Lua 5.1, 5.2, 5.3 & 5.4 3 | Copyright (C) 2014-2023 Gary V. Vaughan 4 | ]] 5 | 6 | title = 'typecheck @PACKAGE_VERSION@ Reference' 7 | project = 'typecheck @PACKAGE_VERSION@' 8 | description = [[ 9 | # Gradual Function Type Checking 10 | 11 | A Luaish run-time gradual type checking system, for argument and return 12 | types at function boundaries with simple annotations that can be disabled 13 | in production code. Its API and type mismatch errors are modelled on the 14 | core Lua C-language `argcheck ()` API. 15 | 16 | This is a light-weight library for Lua 5.1 (including LuaJIT), 5.2 17 | 5.3 and 5.4 written in pure Lua. 18 | 19 | ## LICENSE 20 | 21 | The code is copyright by its author, and released under the MIT 22 | license (the same license as Lua itself). There is no warranty. 23 | ]] 24 | 25 | dir = '../doc' 26 | 27 | file = '../lib/typecheck/init.lua' 28 | 29 | format = 'markdown' 30 | backtick_references = false 31 | sort = false 32 | -------------------------------------------------------------------------------- /lib/typecheck/init.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Gradual Function Type Checking for Lua 5.1, 5.2, 5.3 & 5.4 3 | Copyright (C) 2014-2023 Gary V. Vaughan 4 | ]] 5 | --[[-- 6 | Gradual type checking for Lua functions. 7 | 8 | The behaviour of the functions in this module are controlled by the value 9 | of the `argcheck` field maintained by the `std._debug` module. Not setting 10 | a value prior to loading this module is equivalent to having `argcheck = true`. 11 | 12 | The first line of Lua code in production quality applications that value 13 | execution speed over rigorous function type checking should be: 14 | 15 | require 'std._debug' (false) 16 | 17 | Alternatively, if your project also depends on other `std._debug` hints 18 | remaining enabled: 19 | 20 | require 'std._debug'.argcheck = false 21 | 22 | This mitigates almost all of the overhead of type checking with the 23 | functions from this module. 24 | 25 | @module typecheck 26 | ]] 27 | 28 | 29 | 30 | --[[ ====================== ]]-- 31 | --[[ Load optional modules. ]]-- 32 | --[[ ====================== ]]-- 33 | 34 | 35 | local _debug = (function() 36 | local ok, r = pcall(require, 'std._debug') 37 | if not ok then 38 | r = setmetatable({ 39 | -- If this module was required, but there's no std._debug, safe to 40 | -- assume we do want runtime argchecks! 41 | argcheck = true, 42 | -- Similarly, if std.strict is available, but there's no _std.debug, 43 | -- then apply strict global symbol checks to this module! 44 | strict = true, 45 | }, { 46 | __call = function(self, x) 47 | self.argscheck = (x ~= false) 48 | end, 49 | }) 50 | end 51 | 52 | return r 53 | end)() 54 | 55 | 56 | local strict = (function() 57 | local setfenv = rawget(_G, 'setfenv') or function() end 58 | 59 | -- No strict global symbol checks with no std.strict module, even 60 | -- if we found std._debug and requested that! 61 | local r = function(env, level) 62 | setfenv(1+(level or 1), env) 63 | return env 64 | end 65 | 66 | if _debug.strict then 67 | -- Specify `.init` submodule to make sure we only accept 68 | -- lua-stdlib/strict, and not the old strict module from 69 | -- lua-stdlib/lua-stdlib. 70 | local ok, m = pcall(require, 'std.strict.init') 71 | if ok then 72 | r = m 73 | end 74 | end 75 | return r 76 | end)() 77 | 78 | 79 | local _ENV = strict(_G) 80 | 81 | 82 | 83 | --[[ ================== ]]-- 84 | --[[ Lua normalization. ]]-- 85 | --[[ ================== ]]-- 86 | 87 | 88 | local concat = table.concat 89 | local find = string.find 90 | local floor = math.floor 91 | local format = string.format 92 | local gsub = string.gsub 93 | local insert = table.insert 94 | local io_type = io.type 95 | local match = string.match 96 | local remove = table.remove 97 | local sort = table.sort 98 | local sub = string.sub 99 | 100 | 101 | -- Return callable objects. 102 | -- @function callable 103 | -- @param x an object or primitive 104 | -- @return *x* if *x* can be called, otherwise `nil` 105 | -- @usage 106 | -- (callable(functable) or function()end)(args, ...) 107 | local function callable(x) 108 | -- Careful here! 109 | -- Most versions of Lua don't recurse functables, so make sure you 110 | -- always put a real function in __call metamethods. Consequently, 111 | -- no reason to recurse here. 112 | -- func=function() print 'called' end 113 | -- func() --> 'called' 114 | -- functable=setmetatable({}, {__call=func}) 115 | -- functable() --> 'called' 116 | -- nested=setmetatable({}, {__call=function(self, ...) return functable(...)end}) 117 | -- nested() -> 'called' 118 | -- notnested=setmetatable({}, {__call=functable}) 119 | -- notnested() 120 | -- --> stdin:1: attempt to call global 'nested' (a table value) 121 | -- --> stack traceback: 122 | -- --> stdin:1: in main chunk 123 | -- --> [C]: in ? 124 | if type(x) == 'function' or (getmetatable(x) or {}).__call then 125 | return x 126 | end 127 | end 128 | 129 | 130 | -- Return named metamethod, if callable, otherwise `nil`. 131 | -- @param x item to act on 132 | -- @string n name of metamethod to look up 133 | -- @treturn function|nil metamethod function, if callable, otherwise `nil` 134 | local function getmetamethod(x, n) 135 | return callable((getmetatable(x) or {})[n]) 136 | end 137 | 138 | 139 | -- Length of a string or table object without using any metamethod. 140 | -- @function rawlen 141 | -- @tparam string|table x object to act on 142 | -- @treturn int raw length of *x* 143 | -- @usage 144 | -- --> 0 145 | -- rawlen(setmetatable({}, {__len=function() return 42})) 146 | local function rawlen(x) 147 | -- Lua 5.1 does not implement rawlen, and while # operator ignores 148 | -- __len metamethod, `nil` in sequence is handled inconsistently. 149 | if type(x) ~= 'table' then 150 | return #x 151 | end 152 | 153 | local n = #x 154 | for i = 1, n do 155 | if x[i] == nil then 156 | return i -1 157 | end 158 | end 159 | return n 160 | end 161 | 162 | 163 | -- Deterministic, functional version of core Lua `#` operator. 164 | -- 165 | -- Respects `__len` metamethod (like Lua 5.2+). Otherwise, always return 166 | -- one less than the lowest integer index with a `nil` value in *x*, where 167 | -- the `#` operator implementation might return the size of the array part 168 | -- of a table. 169 | -- @function len 170 | -- @param x item to act on 171 | -- @treturn int the length of *x* 172 | -- @usage 173 | -- x = {1, 2, 3, nil, 5} 174 | -- --> 5 3 175 | -- print(#x, len(x)) 176 | local function len(x) 177 | return (getmetamethod(x, '__len') or rawlen)(x) 178 | end 179 | 180 | 181 | -- Return a list of given arguments, with field `n` set to the length. 182 | -- 183 | -- The returned table also has a `__len` metamethod that returns `n`, so 184 | -- `ipairs` and `unpack` behave sanely when there are `nil` valued elements. 185 | -- @function pack 186 | -- @param ... tuple to act on 187 | -- @treturn table packed list of *...* values, with field `n` set to 188 | -- number of tuple elements (including any explicit `nil` elements) 189 | -- @see unpack 190 | -- @usage 191 | -- --> 5 192 | -- len(pack(nil, 2, 5, nil, nil)) 193 | local pack = (function(f) 194 | local pack_mt = { 195 | __len = function(self) 196 | return self.n 197 | end, 198 | } 199 | 200 | local pack_fn = f or function(...) 201 | return {n=select('#', ...), ...} 202 | end 203 | 204 | return function(...) 205 | return setmetatable(pack_fn(...), pack_mt) 206 | end 207 | end)(rawget(_G, "pack")) 208 | 209 | 210 | -- Like Lua `pairs` iterator, but respect `__pairs` even in Lua 5.1. 211 | -- @function pairs 212 | -- @tparam table t table to act on 213 | -- @treturn function iterator function 214 | -- @treturn table *t*, the table being iterated over 215 | -- @return the previous iteration key 216 | -- @usage 217 | -- for k, v in pairs {'a', b='c', foo=42} do process(k, v) end 218 | local pairs = (function(f) 219 | if not f(setmetatable({},{__pairs=function() return false end})) then 220 | return f 221 | end 222 | 223 | return function(t) 224 | return(getmetamethod(t, '__pairs') or f)(t) 225 | end 226 | end)(pairs) 227 | 228 | 229 | -- Convert a number to an integer and return if possible, otherwise `nil`. 230 | -- @function math.tointeger 231 | -- @param x object to act on 232 | -- @treturn[1] integer *x* converted to an integer if possible 233 | -- @return[2] `nil` otherwise 234 | local tointeger = (function(f) 235 | if f == nil then 236 | -- No host tointeger implementationm use our own. 237 | return function(x) 238 | if type(x) == 'number' and x - floor(x) == 0.0 then 239 | return x 240 | end 241 | end 242 | 243 | elseif f '1' ~= nil then 244 | -- Don't perform implicit string-to-number conversion! 245 | return function(x) 246 | if type(x) == 'number' then 247 | return f(x) 248 | end 249 | end 250 | end 251 | 252 | -- Host tointeger is good! 253 | return f 254 | end)(math.tointeger) 255 | 256 | 257 | -- Return 'integer', 'float' or `nil` according to argument type. 258 | -- 259 | -- To ensure the same behaviour on all host Lua implementations, 260 | -- this function returns 'float' for integer-equivalent floating 261 | -- values, even on Lua 5.3. 262 | -- @function math.type 263 | -- @param x object to act on 264 | -- @treturn[1] string 'integer', if *x* is a whole number 265 | -- @treturn[2] string 'float', for other numbers 266 | -- @return[3] `nil` otherwise 267 | local math_type = math.type or function(x) 268 | if type(x) == 'number' then 269 | return tointeger(x) and 'integer' or 'float' 270 | end 271 | end 272 | 273 | 274 | -- Get a function or functable environment. 275 | -- 276 | -- This version of getfenv works on all supported Lua versions, and 277 | -- knows how to unwrap functables. 278 | -- @function getfenv 279 | -- @tparam function|int fn stack level, C or Lua function or functable 280 | -- to act on 281 | -- @treturn table the execution environment of *fn* 282 | -- @usage 283 | -- callers_environment = getfenv(1) 284 | local getfenv = (function(f) 285 | local debug_getfenv = debug.getfenv 286 | local debug_getinfo = debug.getinfo 287 | local debug_getupvalue = debug.getupvalue 288 | 289 | if debug_getfenv then 290 | 291 | return function(fn) 292 | local n = tointeger(fn or 1) 293 | if n then 294 | if n > 0 then 295 | -- Adjust for this function's stack frame, if fn is non-zero. 296 | n = n + 1 297 | end 298 | 299 | -- Return an additional nil result to defeat tail call elimination 300 | -- which would remove a stack frame and break numeric *fn* count. 301 | return f(n), nil 302 | end 303 | 304 | if type(fn) ~= 'function' then 305 | -- Unwrap functables: 306 | -- No need to recurse because Lua doesn't support nested functables. 307 | -- __call can only (sensibly) be a function, so no need to adjust 308 | -- stack frame offset either. 309 | fn =(getmetatable(fn) or {}).__call or fn 310 | end 311 | 312 | -- In Lua 5.1, only debug.getfenv works on C functions; but it 313 | -- does not work on stack counts. 314 | return debug_getfenv(fn) 315 | end 316 | 317 | else 318 | 319 | -- Thanks to http://lua-users.org/lists/lua-l/2010-06/msg00313.html 320 | return function(fn) 321 | if fn == 0 then 322 | return _G 323 | end 324 | local n = tointeger(fn or 1) 325 | if n then 326 | fn = debug_getinfo(n + 1, 'f').func 327 | elseif type(fn) ~= 'function' then 328 | fn = (getmetatable(fn) or {}).__call or fn 329 | end 330 | 331 | local name, env 332 | local up = 0 333 | repeat 334 | up = up + 1 335 | name, env = debug_getupvalue(fn, up) 336 | until name == '_ENV' or name == nil 337 | return env 338 | end 339 | 340 | end 341 | end)(rawget(_G, 'getfenv')) 342 | 343 | 344 | -- Set a function or functable environment. 345 | -- 346 | -- This version of setfenv works on all supported Lua versions, and 347 | -- knows how to unwrap functables. 348 | -- @function setfenv 349 | -- @tparam function|int fn stack level, C or Lua function or functable 350 | -- to act on 351 | -- @tparam table env new execution environment for *fn* 352 | -- @treturn function function acted upon 353 | -- @usage 354 | -- function clearenv(fn) return setfenv(fn, {}) end 355 | local setfenv = (function(f) 356 | local debug_getinfo = debug.getinfo 357 | local debug_getupvalue = debug.getupvalue 358 | local debug_setfenv = debug.setfenv 359 | local debug_setupvalue = debug.setupvalue 360 | local debug_upvaluejoin = debug.upvaluejoin 361 | 362 | if debug_setfenv then 363 | 364 | return function(fn, env) 365 | local n = tointeger(fn or 1) 366 | if n then 367 | if n > 0 then 368 | n = n + 1 369 | end 370 | return f(n, env), nil 371 | end 372 | if type(fn) ~= 'function' then 373 | fn =(getmetatable(fn) or {}).__call or fn 374 | end 375 | return debug_setfenv(fn, env) 376 | end 377 | 378 | else 379 | 380 | -- Thanks to http://lua-users.org/lists/lua-l/2010-06/msg00313.html 381 | return function(fn, env) 382 | local n = tointeger(fn or 1) 383 | if n then 384 | if n > 0 then 385 | n = n + 1 386 | end 387 | fn = debug_getinfo(n, 'f').func 388 | elseif type(fn) ~= 'function' then 389 | fn =(getmetatable(fn) or {}).__call or fn 390 | end 391 | 392 | local up, name = 0, nil 393 | repeat 394 | up = up + 1 395 | name = debug_getupvalue(fn, up) 396 | until name == '_ENV' or name == nil 397 | if name then 398 | debug_upvaluejoin(fn, up, function() return name end, 1) 399 | debug_setupvalue(fn, up, env) 400 | end 401 | return n ~= 0 and fn or nil 402 | end 403 | 404 | end 405 | end)(rawget(_G, 'setfenv')) 406 | 407 | 408 | -- Either `table.unpack` in newer-, or `unpack` in older Lua implementations. 409 | -- Always defaulting to full packed table unpacking when no index arguments 410 | -- are passed. 411 | -- @function unpack 412 | -- @tparam table t table to act on 413 | -- @int[opt=1] i first index to unpack 414 | -- @int[opt=len(t)] j last index to unpack 415 | -- @return ... values of numeric indices of *t* 416 | -- @see pack 417 | -- @usage 418 | -- local a, b, c = unpack(pack(nil, 2, nil)) 419 | -- assert(a == nil and b == 2 and c == nil) 420 | local unpack = (function(f) 421 | return function(t, i, j) 422 | return f(t, tointeger(i) or 1, tointeger(j) or len(t)) 423 | end 424 | end)(rawget(_G, "unpack") or table.unpack) 425 | 426 | 427 | 428 | --[[ ================= ]]-- 429 | --[[ Helper Functions. ]]-- 430 | --[[ ================= ]]-- 431 | 432 | 433 | local function copy(dest, src) 434 | if src == nil then 435 | dest, src = {}, dest 436 | end 437 | for k, v in pairs(src) do 438 | dest[k] = v 439 | end 440 | return dest 441 | end 442 | 443 | 444 | local function split(s, sep) 445 | local r, pattern = {}, nil 446 | if sep == '' then 447 | pattern = '(.)' 448 | r[#r + 1] = '' 449 | else 450 | pattern = '(.-)' ..(sep or '%s+') 451 | end 452 | local b, slen = 0, len(s) 453 | while b <= slen do 454 | local _, n, m = find(s, pattern, b + 1) 455 | r[#r + 1] = m or sub(s, b + 1, slen) 456 | b = n or slen + 1 457 | end 458 | return r 459 | end 460 | 461 | 462 | 463 | --[[ ================== ]]-- 464 | --[[ Argument Checking. ]]-- 465 | --[[ ================== ]]-- 466 | 467 | 468 | -- There's an additional stack frame to count over from inside functions 469 | -- with argchecks enabled. 470 | local ARGCHECK_FRAME = 0 471 | 472 | 473 | local function argerror(name, i, extramsg, level) 474 | level = tointeger(level) or 1 475 | local s = format("bad argument #%d to '%s'", tointeger(i), name) 476 | if extramsg ~= nil then 477 | s = s .. ' (' .. extramsg .. ')' 478 | end 479 | error(s, level > 0 and level + 2 + ARGCHECK_FRAME or 0) 480 | end 481 | 482 | 483 | -- A rudimentary argument type validation decorator. 484 | -- 485 | -- Return the checked function directly if `_debug.argcheck` is reset, 486 | -- otherwise use check function arguments using predicate functions in 487 | -- the corresponding position in the decorator call. 488 | -- @function checktypes 489 | -- @string name function name to use in error messages 490 | -- @tparam funct predicate return true if checked function argument is 491 | -- valid, otherwise return nil and an error message suitable for 492 | -- *extramsg* argument of @{argerror} 493 | -- @tparam func ... additional predicates for subsequent checked 494 | -- function arguments 495 | -- @raises argerror when an argument validator returns failure 496 | -- @see argerror 497 | -- @usage 498 | -- local unpack = checktypes('unpack', types.table) .. 499 | -- function(t, i, j) 500 | -- return table.unpack(t, i or 1, j or #t) 501 | -- end 502 | local checktypes = (function() 503 | -- Set checktypes according to whether argcheck was required by _debug. 504 | if _debug.argcheck then 505 | 506 | ARGCHECK_FRAME = 1 507 | 508 | local function icalls(checks, argu) 509 | return function(state, i) 510 | if i < state.checks.n then 511 | i = i + 1 512 | local r = pack(state.checks[i](state.argu, i)) 513 | if r.n > 0 then 514 | return i, r[1], r[2] 515 | end 516 | return i 517 | end 518 | end, {argu=argu, checks=checks}, 0 519 | end 520 | 521 | return function(name, ...) 522 | return setmetatable(pack(...), { 523 | __concat = function(checks, inner) 524 | if not callable(inner) then 525 | error("attempt to annotate non-callable value with 'checktypes'", 2) 526 | end 527 | return function(...) 528 | local argu = pack(...) 529 | for i, expected, got in icalls(checks, argu) do 530 | if got or expected then 531 | local buf, extramsg = {}, nil 532 | if expected then 533 | got = got or ('got ' .. type(argu[i])) 534 | buf[#buf +1] = expected .. ' expected, ' .. got 535 | elseif got then 536 | buf[#buf +1] = got 537 | end 538 | if #buf > 0 then 539 | extramsg = concat(buf) 540 | end 541 | return argerror(name, i, extramsg, 3), nil 542 | end 543 | end 544 | -- Tail call pessimisation: inner might be counting frames, 545 | -- and have several return values that need preserving. 546 | -- Different Lua implementations tail call under differing 547 | -- conditions, so we need this hair to make sure we always 548 | -- get the same number of stack frames interposed. 549 | local results = pack(inner(...)) 550 | return unpack(results, 1, results.n) 551 | end 552 | end, 553 | }) 554 | end 555 | 556 | else 557 | 558 | -- Return `inner` untouched, for no runtime overhead! 559 | return function(...) 560 | return setmetatable({}, { 561 | __concat = function(_, inner) 562 | return inner 563 | end, 564 | }) 565 | end 566 | 567 | end 568 | end)() 569 | 570 | 571 | local function resulterror(name, i, extramsg, level) 572 | level = level or 1 573 | local s = format("bad result #%d from '%s'", i, name) 574 | if extramsg ~= nil then 575 | s = s .. ' (' .. extramsg .. ')' 576 | end 577 | error(s, level > 0 and level + 1 + ARGCHECK_FRAME or 0) 578 | end 579 | 580 | 581 | 582 | --[[ ================= ]]-- 583 | --[[ Type annotations. ]]-- 584 | --[[ ================= ]]-- 585 | 586 | 587 | local function fail(expected, argu, i, got) 588 | if i > argu.n then 589 | return expected, 'got no value' 590 | elseif got ~= nil then 591 | return expected, 'got ' .. got 592 | end 593 | return expected 594 | end 595 | 596 | 597 | --- Low-level type conformance check helper. 598 | -- 599 | -- Use this, with a simple @{Predicate} function, to write concise argument 600 | -- type check functions. 601 | -- @function check 602 | -- @string expected name of the expected type 603 | -- @tparam table argu a packed table (including `n` field) of all arguments 604 | -- @int i index into *argu* for argument to action 605 | -- @tparam Predicate predicate check whether `argu[i]` matches `expected` 606 | -- @usage 607 | -- function callable(argu, i) 608 | -- return check('string', argu, i, function(x) 609 | -- return type(x) == 'string' 610 | -- end) 611 | -- end 612 | local function check(expected, argu, i, predicate) 613 | local arg = argu[i] 614 | local ok, got = predicate(arg) 615 | if not ok then 616 | return fail(expected, argu, i, got) 617 | end 618 | end 619 | 620 | 621 | local function _type(x) 622 | return (getmetatable(x) or {})._type or io_type(x) or math_type(x) or type(x) 623 | end 624 | 625 | 626 | local types = setmetatable({ 627 | -- Accept argu[i]. 628 | accept = function() end, 629 | 630 | -- Reject missing argument *i*. 631 | arg = function(argu, i) 632 | if i > argu.n then 633 | return 'no value' 634 | end 635 | end, 636 | 637 | -- Accept function valued or `__call` metamethod carrying argu[i]. 638 | callable = function(argu, i) 639 | return check('callable', argu, i, callable) 640 | end, 641 | 642 | -- Accept argu[i] if it is an integer valued number 643 | integer = function(argu, i) 644 | local value = argu[i] 645 | if type(tonumber(value)) ~= 'number' then 646 | return fail('integer', argu, i) 647 | end 648 | if tointeger(value) == nil then 649 | return nil, _type(value) .. ' has no integer representation' 650 | end 651 | end, 652 | 653 | -- Accept missing argument *i* (but not explicit `nil`). 654 | missing = function(argu, i) 655 | if i <= argu.n then 656 | return nil 657 | end 658 | end, 659 | 660 | -- Accept non-nil valued argu[i]. 661 | value = function(argu, i) 662 | if i > argu.n then 663 | return 'value', 'got no value' 664 | elseif argu[i] == nil then 665 | return 'value' 666 | end 667 | end, 668 | }, { 669 | __index = function(_, k) 670 | -- Accept named primitive valued argu[i]. 671 | return function(argu, i) 672 | return check(k, argu, i, function(x) 673 | return type(x) == k 674 | end) 675 | end 676 | end, 677 | }) 678 | 679 | 680 | local function any(...) 681 | local fns = {...} 682 | return function(argu, i) 683 | local buf = {} 684 | local expected, got, r 685 | for _, predicate in ipairs(fns) do 686 | r = pack(predicate(argu, i)) 687 | expected, got = r[1], r[2] 688 | if r.n == 0 then 689 | -- A match! 690 | return 691 | elseif r.n == 2 and expected == nil and #got > 0 then 692 | -- Return non-type based mismatch immediately. 693 | return expected, got 694 | elseif expected ~= 'nil' then 695 | -- Record one of the types we would have matched. 696 | buf[#buf + 1] = expected 697 | end 698 | end 699 | if #buf == 0 then 700 | return got 701 | elseif #buf > 1 then 702 | sort(buf) 703 | buf[#buf -1], buf[#buf] = buf[#buf -1] .. ' or ' .. buf[#buf], nil 704 | end 705 | expected = concat(buf, ', ') 706 | if got ~= nil then 707 | return expected, got 708 | end 709 | return expected 710 | end 711 | end 712 | 713 | 714 | local function opt(...) 715 | return any(types['nil'], ...) 716 | end 717 | 718 | 719 | 720 | --[[ =============================== ]]-- 721 | --[[ Implementation of value checks. ]]-- 722 | --[[ =============================== ]]-- 723 | 724 | 725 | local function xform_gsub(pattern, replace) 726 | return function(s) 727 | return (gsub(s, pattern, replace)) 728 | end 729 | end 730 | 731 | 732 | local ORCONCAT_XFORMS = { 733 | xform_gsub('#table', 'non-empty table'), 734 | xform_gsub('#list', 'non-empty list'), 735 | xform_gsub('functor', 'functable'), 736 | xform_gsub('list of', '\t%0'), -- tab sorts before any other printable 737 | xform_gsub('table of', '\t%0'), 738 | } 739 | 740 | 741 | --- Concatenate a table of strings using ', ' and ' or ' delimiters. 742 | -- @tparam table alternatives a table of strings 743 | -- @treturn string string of elements from alternatives delimited by ', ' 744 | -- and ' or ' 745 | local function orconcat(alternatives) 746 | if len(alternatives) > 1 then 747 | local t = copy(alternatives) 748 | sort(t, function(a, b) 749 | for _, fn in ipairs(ORCONCAT_XFORMS) do 750 | a, b = fn(a), fn(b) 751 | end 752 | return a < b 753 | end) 754 | local top = remove(t) 755 | t[#t] = t[#t] .. ' or ' .. top 756 | alternatives = t 757 | end 758 | return concat(alternatives, ', ') 759 | end 760 | 761 | 762 | local EXTRAMSG_XFORMS = { 763 | xform_gsub('any value or nil', 'argument'), 764 | xform_gsub('#table', 'non-empty table'), 765 | xform_gsub('#list', 'non-empty list'), 766 | xform_gsub('functor', 'functable'), 767 | xform_gsub('(%S+ of) bool([,%s])', '%1 boolean%2'), 768 | xform_gsub('(%S+ of) func([,%s])', '%1 function%2'), 769 | xform_gsub('(%S+ of) int([,%s])', '%1 integer%2'), 770 | xform_gsub('(%S+ of [^,%s]-)s?([,%s])', '%1s%2'), 771 | xform_gsub('(s, [^,%s]-)s?([,%s])', '%1s%2'), 772 | xform_gsub('(of .-)s? or ([^,%s]-)s? ', '%1s or %2s '), 773 | } 774 | 775 | 776 | local function extramsg_mismatch(i, expectedtypes, argu, key) 777 | local actual, actualtype 778 | 779 | if type(i) ~= 'number' then 780 | -- Support the old (expectedtypes, actual, key) calling convention. 781 | expectedtypes, actual, key, argu = i, expectedtypes, argu, nil 782 | actualtype = _type(actual) 783 | else 784 | -- Support the new (i, expectedtypes, argu) convention, which can 785 | -- diagnose missing arguments properly. 786 | actual = argu[i] 787 | if i > argu.n then 788 | actualtype = 'no value' 789 | else 790 | actualtype = _type(actual) or type(actual) 791 | end 792 | end 793 | 794 | -- Tidy up actual type for display. 795 | if actualtype == 'string' and sub(actual, 1, 1) == ':' then 796 | actualtype = actual 797 | elseif type(actual) == 'table' then 798 | if actualtype == 'table' and (getmetatable(actual) or {}).__call ~= nil then 799 | actualtype = 'functable' 800 | elseif next(actual) == nil then 801 | local matchstr = ',' .. concat(expectedtypes, ',') .. ',' 802 | if actualtype == 'table' and matchstr == ',#list,' then 803 | actualtype = 'empty list' 804 | elseif actualtype == 'table' or match(matchstr, ',#') then 805 | actualtype = 'empty ' .. actualtype 806 | end 807 | end 808 | end 809 | 810 | if key then 811 | actualtype = actualtype .. ' at index ' .. tostring(key) 812 | end 813 | 814 | -- Tidy up expected types for display. 815 | local expectedstr = expectedtypes 816 | if type(expectedtypes) == 'table' then 817 | local t = {} 818 | for i, v in ipairs(expectedtypes) do 819 | if v == 'func' then 820 | t[i] = 'function' 821 | elseif v == 'bool' then 822 | t[i] = 'boolean' 823 | elseif v == 'int' then 824 | t[i] = 'integer' 825 | elseif v == 'any' then 826 | t[i] = 'any value' 827 | elseif v == 'file' then 828 | t[i] = 'FILE*' 829 | elseif not key then 830 | t[i] = match(v, '(%S+) of %S+') or v 831 | else 832 | t[i] = v 833 | end 834 | end 835 | expectedstr = orconcat(t) .. ' expected' 836 | for _, fn in ipairs(EXTRAMSG_XFORMS) do 837 | expectedstr = fn(expectedstr) 838 | end 839 | end 840 | 841 | if expectedstr == 'integer expected' and tonumber(actual) then 842 | if tointeger(actual) == nil then 843 | return actualtype .. ' has no integer representation' 844 | end 845 | end 846 | 847 | return expectedstr .. ', got ' .. actualtype 848 | end 849 | 850 | 851 | --- Compare *check* against type of *actual*. *check* must be a single type 852 | -- @string expected extended type name expected 853 | -- @param actual object being typechecked 854 | -- @treturn boolean `true` if *actual* is of type *check*, otherwise 855 | -- `false` 856 | local function checktype(expected, actual) 857 | if expected == 'any' and actual ~= nil then 858 | return true 859 | elseif expected == 'file' and io_type(actual) == 'file' then 860 | return true 861 | elseif expected == 'functable' or expected == 'callable' or expected == 'functor' then 862 | if (getmetatable(actual) or {}).__call ~= nil then 863 | return true 864 | end 865 | end 866 | 867 | local actualtype = type(actual) 868 | if expected == actualtype then 869 | return true 870 | elseif expected == 'bool' and actualtype == 'boolean' then 871 | return true 872 | elseif expected == '#table' then 873 | if actualtype == 'table' and next(actual) then 874 | return true 875 | end 876 | elseif expected == 'func' or expected == 'callable' then 877 | if actualtype == 'function' then 878 | return true 879 | end 880 | elseif expected == 'int' or expected == 'integer' then 881 | if actualtype == 'number' and actual == floor(actual) then 882 | return true 883 | end 884 | elseif type(expected) == 'string' and sub(expected, 1, 1) == ':' then 885 | if expected == actual then 886 | return true 887 | end 888 | end 889 | 890 | actualtype = _type(actual) 891 | if expected == actualtype then 892 | return true 893 | elseif expected == 'list' or expected == '#list' then 894 | if actualtype == 'table' or actualtype == 'List' then 895 | local n, count = len(actual), 0 896 | local i = next(actual) 897 | repeat 898 | if i ~= nil then 899 | count = count + 1 900 | end 901 | i = next(actual, i) 902 | until i == nil or count > n 903 | if count == n and (expected == 'list' or count > 0) then 904 | return true 905 | end 906 | end 907 | elseif expected == 'object' then 908 | if actualtype ~= 'table' and type(actual) == 'table' then 909 | return true 910 | end 911 | end 912 | 913 | return false 914 | end 915 | 916 | 917 | local function typesplit(typespec) 918 | if type(typespec) == 'string' then 919 | typespec = split(gsub(typespec, '%s+or%s+', '|'), '%s*|%s*') 920 | end 921 | local r, seen, add_nil = {}, {}, false 922 | for _, v in ipairs(typespec) do 923 | local m = match(v, '^%?(.+)$') 924 | if m then 925 | add_nil, v = true, m 926 | end 927 | if not seen[v] then 928 | r[#r + 1] = v 929 | seen[v] = true 930 | end 931 | end 932 | if add_nil then 933 | r[#r + 1] = 'nil' 934 | end 935 | return r 936 | end 937 | 938 | 939 | local function checktypespec(expected, actual) 940 | expected = typesplit(expected) 941 | 942 | -- Check actual has one of the types from expected 943 | for _, expect in ipairs(expected) do 944 | local container, contents = match(expect, '^(%S+) of (%S-)s?$') 945 | container = container or expect 946 | 947 | -- Does the type of actual check out? 948 | local ok = checktype(container, actual) 949 | 950 | -- For 'table of things', check all elements are a thing too. 951 | if ok and contents and type(actual) == 'table' then 952 | for k, v in pairs(actual) do 953 | if not checktype(contents, v) then 954 | return nil, extramsg_mismatch(expected, v, k) 955 | end 956 | end 957 | end 958 | if ok then 959 | return true 960 | end 961 | end 962 | 963 | return nil, extramsg_mismatch(expected, actual) 964 | end 965 | 966 | 967 | 968 | --[[ ================================== ]]-- 969 | --[[ Implementation of function checks. ]]-- 970 | --[[ ================================== ]]-- 971 | 972 | 973 | local function extramsg_toomany(bad, expected, actual) 974 | local s = 'no more than %d %s%s expected, got %d' 975 | return format(s, expected, bad, expected == 1 and '' or 's', actual) 976 | end 977 | 978 | 979 | --- Strip trailing ellipsis from final argument if any, storing maximum 980 | -- number of values that can be matched directly in `t.maxvalues`. 981 | -- @tparam table t table to act on 982 | -- @string v element added to *t*, to match against ... suffix 983 | -- @treturn table *t* with ellipsis stripped and maxvalues field set 984 | local function markdots(t, v) 985 | return (gsub(v, '%.%.%.$', function() 986 | t.dots = true return '' 987 | end)) 988 | end 989 | 990 | 991 | --- Calculate permutations of type lists with and without [optionals]. 992 | -- @tparam table t a list of expected types by argument position 993 | -- @treturn table set of possible type lists 994 | local function permute(t) 995 | if t[#t] then 996 | t[#t] = gsub(t[#t], '%]%.%.%.$', '...]') 997 | end 998 | 999 | local p = {{}} 1000 | for _, v in ipairs(t) do 1001 | local optional = match(v, '%[(.+)%]') 1002 | 1003 | if optional == nil then 1004 | -- Append non-optional type-spec to each permutation. 1005 | for b = 1, #p do 1006 | insert(p[b], markdots(p[b], v)) 1007 | end 1008 | else 1009 | -- Duplicate all existing permutations, and add optional type-spec 1010 | -- to the unduplicated permutations. 1011 | local o = #p 1012 | for b = 1, o do 1013 | p[b + o] = copy(p[b]) 1014 | insert(p[b], markdots(p[b], optional)) 1015 | end 1016 | end 1017 | end 1018 | return p 1019 | end 1020 | 1021 | 1022 | local function projectuniq(fkey, tt) 1023 | -- project 1024 | local t = {} 1025 | for _, u in ipairs(tt) do 1026 | t[#t + 1] = u[fkey] 1027 | end 1028 | 1029 | -- split and remove duplicates 1030 | local r, s = {}, {} 1031 | for _, e in ipairs(t) do 1032 | for _, v in ipairs(typesplit(e)) do 1033 | if s[v] == nil then 1034 | r[#r + 1], s[v] = v, true 1035 | end 1036 | end 1037 | end 1038 | return r 1039 | end 1040 | 1041 | 1042 | local function parsetypes(typespec) 1043 | local r, permutations = {}, permute(typespec) 1044 | for i = 1, #permutations[1] do 1045 | r[i] = projectuniq(i, permutations) 1046 | end 1047 | r.dots = permutations[1].dots 1048 | return r 1049 | end 1050 | 1051 | 1052 | 1053 | local argcheck = (function() 1054 | if _debug.argcheck then 1055 | 1056 | return function(name, i, expected, actual, level) 1057 | level = level or 1 1058 | local _, err = checktypespec(expected, actual) 1059 | if err then 1060 | argerror(name, i, err, level + 1) 1061 | end 1062 | end 1063 | 1064 | else 1065 | 1066 | return function(...) 1067 | return ... 1068 | end 1069 | 1070 | end 1071 | end)() 1072 | 1073 | 1074 | local argscheck = (function() 1075 | if _debug.argcheck then 1076 | 1077 | --- Return index of the first mismatch between types and values, or `nil`. 1078 | -- @tparam table typelist a list of expected types 1079 | -- @tparam table valuelist a table of arguments to compare 1080 | -- @treturn int|nil position of first mismatch in *typelist* 1081 | local function typematch(typelist, valuelist) 1082 | local n = #typelist 1083 | for i = 1, n do -- normal parameters 1084 | local ok = pcall(argcheck, 'pcall', i, typelist[i], valuelist[i]) 1085 | if not ok or i > valuelist.n then 1086 | return i 1087 | end 1088 | end 1089 | for i = n + 1, valuelist.n do -- additional values against final type 1090 | local ok = pcall(argcheck, 'pcall', i, typelist[n], valuelist[i]) 1091 | if not ok then 1092 | return i 1093 | end 1094 | end 1095 | end 1096 | 1097 | 1098 | --- Diagnose mismatches between *valuelist* and type *permutations*. 1099 | -- @tparam table valuelist list of actual values to be checked 1100 | -- @tparam table argt table of precalculated values and handler functiens 1101 | local function diagnose(valuelist, argt) 1102 | local permutations = argt.permutations 1103 | local bestmismatch, t 1104 | 1105 | bestmismatch = 0 1106 | for i, typelist in ipairs(permutations) do 1107 | local mismatch = typematch(typelist, valuelist) 1108 | if mismatch == nil then 1109 | bestmismatch, t = nil, nil 1110 | break -- every *valuelist* matched types from this *typelist* 1111 | elseif mismatch > bestmismatch then 1112 | bestmismatch, t = mismatch, permutations[i] 1113 | end 1114 | end 1115 | 1116 | if bestmismatch ~= nil then 1117 | -- Report an error for all possible types at bestmismatch index. 1118 | local i, expected = bestmismatch, nil 1119 | if t.dots and i > #t then 1120 | expected = typesplit(t[#t]) 1121 | else 1122 | expected = projectuniq(i, permutations) 1123 | end 1124 | 1125 | -- This relies on the `permute()` algorithm leaving the longest 1126 | -- possible permutation(with dots if necessary) at permutations[1]. 1127 | local typelist = permutations[1] 1128 | 1129 | -- For 'container of things', check all elements are a thing too. 1130 | if typelist[i] then 1131 | local contents = match(typelist[i], '^%S+ of (%S-)s?$') 1132 | if contents and type(valuelist[i]) == 'table' then 1133 | for k, v in pairs(valuelist[i]) do 1134 | if not checktype(contents, v) then 1135 | argt.badtype(i, extramsg_mismatch(expected, v, k), 3) 1136 | end 1137 | end 1138 | end 1139 | end 1140 | 1141 | -- Otherwise the argument type itself was mismatched. 1142 | if t.dots or #t >= valuelist.n then 1143 | argt.badtype(i, extramsg_mismatch(i, expected, valuelist), 3) 1144 | end 1145 | end 1146 | 1147 | local n = valuelist.n 1148 | t = t or permutations[1] 1149 | if t and t.dots == nil and n > #t then 1150 | argt.badtype(#t + 1, extramsg_toomany(argt.bad, #t, n), 3) 1151 | end 1152 | end 1153 | 1154 | 1155 | -- Pattern to extract: fname([types]?[, types]*) 1156 | local args_pattern = '^%s*([%w_][%.%:%d%w_]*)%s*%(%s*(.*)%s*%)' 1157 | 1158 | return function(decl, inner) 1159 | -- Parse 'fname(argtype, argtype, argtype...)'. 1160 | local fname, argtypes = match(decl, args_pattern) 1161 | if argtypes == '' then 1162 | argtypes = {} 1163 | elseif argtypes then 1164 | argtypes = split(argtypes, '%s*,%s*') 1165 | else 1166 | fname = match(decl, '^%s*([%w_][%.%:%d%w_]*)') 1167 | end 1168 | 1169 | -- Precalculate vtables once to make multiple calls faster. 1170 | local input = { 1171 | bad = 'argument', 1172 | badtype = function(i, extramsg, level) 1173 | level = level or 1 1174 | argerror(fname, i, extramsg, level + 1) 1175 | end, 1176 | permutations = permute(argtypes), 1177 | } 1178 | 1179 | -- Parse '... => returntype, returntype, returntype...'. 1180 | local output, returntypes = nil, match(decl, '=>%s*(.+)%s*$') 1181 | if returntypes then 1182 | local i, permutations = 0, {} 1183 | for _, group in ipairs(split(returntypes, '%s+or%s+')) do 1184 | returntypes = split(group, ',%s*') 1185 | for _, t in ipairs(permute(returntypes)) do 1186 | i = i + 1 1187 | permutations[i] = t 1188 | end 1189 | end 1190 | 1191 | -- Ensure the longest permutation is first in the list. 1192 | sort(permutations, function(a, b) 1193 | return #a > #b 1194 | end) 1195 | 1196 | output = { 1197 | bad = 'result', 1198 | badtype = function(i, extramsg, level) 1199 | level = level or 1 1200 | resulterror(fname, i, gsub(extramsg, 'argument( expected,)', 'result%1'), level + 1) 1201 | end, 1202 | permutations = permutations, 1203 | } 1204 | end 1205 | 1206 | local wrap_function = function(my_inner) 1207 | return function(...) 1208 | local argt = pack(...) 1209 | 1210 | -- Don't check type of self if fname has a ':' in it. 1211 | if find(fname, ':') then 1212 | remove(argt, 1) 1213 | argt.n = argt.n - 1 1214 | end 1215 | 1216 | -- Diagnose bad inputs. 1217 | diagnose(argt, input) 1218 | 1219 | -- Propagate outer environment to inner function. 1220 | if type(my_inner) == 'table' then 1221 | setfenv((getmetatable(my_inner) or {}).__call, getfenv(1)) 1222 | else 1223 | setfenv(my_inner, getfenv(1)) 1224 | end 1225 | 1226 | -- Execute. 1227 | local results = pack(my_inner(...)) 1228 | 1229 | -- Diagnose bad outputs. 1230 | if returntypes then 1231 | diagnose(results, output) 1232 | end 1233 | 1234 | return unpack(results, 1, results.n) 1235 | end 1236 | end 1237 | 1238 | if inner then 1239 | return wrap_function(inner) 1240 | else 1241 | return setmetatable({}, { 1242 | __concat = function(_, concat_inner) 1243 | return wrap_function(concat_inner) 1244 | end 1245 | }) 1246 | end 1247 | end 1248 | 1249 | else 1250 | 1251 | -- Turn off argument checking if _debug is false, or a table containing 1252 | -- a false valued `argcheck` field. 1253 | return function(_, inner) 1254 | if inner then 1255 | return inner 1256 | else 1257 | return setmetatable({}, { 1258 | __concat = function(_, concat_inner) 1259 | return concat_inner 1260 | end 1261 | }) 1262 | end 1263 | end 1264 | 1265 | end 1266 | end)() 1267 | 1268 | 1269 | local T = types 1270 | 1271 | return setmetatable({ 1272 | --- Add this to any stack frame offsets when argchecks are in force. 1273 | -- @int ARGCHECK_FRAME 1274 | ARGCHECK_FRAME = ARGCHECK_FRAME, 1275 | 1276 | --- Check the type of an argument against expected types. 1277 | -- Equivalent to luaL_argcheck in the Lua C API. 1278 | -- 1279 | -- Call `argerror` if there is a type mismatch. 1280 | -- 1281 | -- Argument `actual` must match one of the types from in `expected`, each 1282 | -- of which can be the name of a primitive Lua type, a stdlib object type, 1283 | -- or one of the special options below: 1284 | -- 1285 | -- #table accept any non-empty table 1286 | -- any accept any non-nil argument type 1287 | -- callable accept a function or a functable 1288 | -- file accept an open file object 1289 | -- func accept a function 1290 | -- function accept a function 1291 | -- functable accept an object with a __call metamethod 1292 | -- int accept an integer valued number 1293 | -- list accept a table where all keys are a contiguous 1-based integer range 1294 | -- #list accept any non-empty list 1295 | -- object accept any std.Object derived type 1296 | -- :foo accept only the exact string ':foo', works for any :-prefixed string 1297 | -- 1298 | -- The `:foo` format allows for type-checking of self-documenting 1299 | -- boolean-like constant string parameters predicated on `nil` versus 1300 | -- `:option` instead of `false` versus `true`. Or you could support 1301 | -- both: 1302 | -- 1303 | -- argcheck('table.copy', 2, 'boolean|:nometa|nil', nometa) 1304 | -- 1305 | -- A very common pattern is to have a list of possible types including 1306 | -- 'nil' when the argument is optional. Rather than writing long-hand 1307 | -- as above, prepend a question mark to the list of types and omit the 1308 | -- explicit 'nil' entry: 1309 | -- 1310 | -- argcheck('table.copy', 2, '?boolean|:nometa', predicate) 1311 | -- 1312 | -- Normally, you should not need to use the `level` parameter, as the 1313 | -- default is to blame the caller of the function using `argcheck` in 1314 | -- error messages; which is almost certainly what you want. 1315 | -- @function argcheck 1316 | -- @string name function to blame in error message 1317 | -- @int i argument number to blame in error message 1318 | -- @string expected specification for acceptable argument types 1319 | -- @param actual argument passed 1320 | -- @int[opt=2] level call stack level to blame for the error 1321 | -- @usage 1322 | -- local function case(with, branches) 1323 | -- argcheck('std.functional.case', 2, '#table', branches) 1324 | -- ... 1325 | argcheck = checktypes( 1326 | 'argcheck', T.string, T.integer, T.string, T.accept, opt(T.integer) 1327 | ) .. argcheck, 1328 | 1329 | --- Raise a bad argument error. 1330 | -- Equivalent to luaL_argerror in the Lua C API. This function does not 1331 | -- return. The `level` argument behaves just like the core `error` 1332 | -- function. 1333 | -- @function argerror 1334 | -- @string name function to callout in error message 1335 | -- @int i argument number 1336 | -- @string[opt] extramsg additional text to append to message inside parentheses 1337 | -- @int[opt=1] level call stack level to blame for the error 1338 | -- @see resulterror 1339 | -- @see extramsg_mismatch 1340 | -- @usage 1341 | -- local function slurp(file) 1342 | -- local h, err = input_handle(file) 1343 | -- if h == nil then 1344 | -- argerror('std.io.slurp', 1, err, 2) 1345 | -- end 1346 | -- ... 1347 | argerror = checktypes( 1348 | 'argerror', T.string, T.integer, T.accept, opt(T.integer) 1349 | ) .. argerror, 1350 | 1351 | --- Wrap a function definition with argument type and arity checking. 1352 | -- In addition to checking that each argument type matches the corresponding 1353 | -- element in the *types* table with `argcheck`, if the final element of 1354 | -- *types* ends with an ellipsis, remaining unchecked arguments are checked 1355 | -- against that type: 1356 | -- 1357 | -- format = argscheck('string.format(string, ?any...)', string.format) 1358 | -- 1359 | -- A colon in the function name indicates that the argument type list does 1360 | -- not have a type for `self`: 1361 | -- 1362 | -- format = argscheck('string:format(?any...)', string.format) 1363 | -- 1364 | -- If an argument can be omitted entirely, then put its type specification 1365 | -- in square brackets: 1366 | -- 1367 | -- insert = argscheck('table.insert(table, [int], ?any)', table.insert) 1368 | -- 1369 | -- Similarly return types can be checked with the same list syntax as 1370 | -- arguments: 1371 | -- 1372 | -- len = argscheck('string.len(string) => int', string.len) 1373 | -- 1374 | -- Additionally, variant return type lists can be listed like this: 1375 | -- 1376 | -- open = argscheck('io.open(string, ?string) => file or nil, string', 1377 | -- io.open) 1378 | -- 1379 | -- @function argscheck 1380 | -- @string decl function type declaration string 1381 | -- @func inner function to wrap with argument checking 1382 | -- @usage 1383 | -- local case = argscheck('std.functional.case(?any, #table) => [any...]', 1384 | -- function(with, branches) 1385 | -- ... 1386 | -- end) 1387 | -- 1388 | -- -- Alternatively, as an annotation: 1389 | -- local case = argscheck 'std.functional.case(?any, #table) => [any...]' .. 1390 | -- function(with, branches) 1391 | -- ... 1392 | -- end 1393 | argscheck = checktypes( 1394 | 'argscheck', T.string, opt(T.callable) 1395 | ) .. argscheck, 1396 | 1397 | --- Checks the type of *actual* against the *expected* typespec 1398 | -- @function check 1399 | -- @tparam string expected expected typespec 1400 | -- @param actual object being typechecked 1401 | -- @treturn[1] bool `true`, if *actual* matches *expected* 1402 | -- @return[2] `nil` 1403 | -- @treturn[2] string an @{extramsg_mismatch} format error message, otherwise 1404 | -- @usage 1405 | -- --> stdin:2: string or number expected, got empty table 1406 | -- assert(check('string|number', {})) 1407 | check = checktypespec, 1408 | 1409 | --- Format a type mismatch error. 1410 | -- @function extramsg_mismatch 1411 | -- @int[opt] i index of *argu* to be matched with 1412 | -- @string expected a pipe delimited list of matchable types 1413 | -- @tparam table argu packed table of all arguments 1414 | -- @param[opt] key erroring container element key 1415 | -- @treturn string formatted *extramsg* for this mismatch for @{argerror} 1416 | -- @see argerror 1417 | -- @see resulterror 1418 | -- @usage 1419 | -- if fmt ~= nil and type(fmt) ~= 'string' then 1420 | -- argerror('format', 1, extramsg_mismatch(1, '?string', argu)) 1421 | -- end 1422 | extramsg_mismatch = function(i, expected, argu, key) 1423 | if tointeger(i) and type(expected) == 'string' then 1424 | expected = typesplit(expected) 1425 | else 1426 | -- support old (expected, actual, key) calling convention 1427 | i = typesplit(i) 1428 | end 1429 | return extramsg_mismatch(i, expected, argu, key) 1430 | end, 1431 | 1432 | --- Format a too many things error. 1433 | -- @function extramsg_toomany 1434 | -- @string bad the thing there are too many of 1435 | -- @int expected maximum number of *bad* things expected 1436 | -- @int actual actual number of *bad* things that triggered the error 1437 | -- @see argerror 1438 | -- @see resulterror 1439 | -- @see extramsg_mismatch 1440 | -- @usage 1441 | -- if select('#', ...) > 7 then 1442 | -- argerror('sevenses', 8, extramsg_toomany('argument', 7, select('#', ...))) 1443 | -- end 1444 | extramsg_toomany = extramsg_toomany, 1445 | 1446 | --- Create an @{ArgCheck} predicate for an optional argument. 1447 | -- 1448 | -- This function satisfies the @{ArgCheck} interface in order to be 1449 | -- useful as an argument to @{argscheck} when a particular argument 1450 | -- is optional. 1451 | -- @function opt 1452 | -- @tparam ArgCheck ... type predicate callables 1453 | -- @treturn ArgCheck a new function that calls all passed 1454 | -- predicates, and combines error messages if all fail 1455 | -- @usage 1456 | -- getfenv = argscheck( 1457 | -- 'getfenv', opt(types.integer, types.callable) 1458 | -- ) .. getfenv 1459 | opt = opt, 1460 | 1461 | --- Compact permutation list into a list of valid types at each argument. 1462 | -- Eliminate bracketed types by combining all valid types at each position 1463 | -- for all permutations of *typelist*. 1464 | -- @function parsetypes 1465 | -- @tparam list types a normalized list of type names 1466 | -- @treturn list valid types for each positional parameter 1467 | parsetypes = parsetypes, 1468 | 1469 | --- Raise a bad result error. 1470 | -- Like @{argerror} for bad results. This function does not 1471 | -- return. The `level` argument behaves just like the core `error` 1472 | -- function. 1473 | -- @function resulterror 1474 | -- @string name function to callout in error message 1475 | -- @int i result number 1476 | -- @string[opt] extramsg additional text to append to message inside parentheses 1477 | -- @int[opt=1] level call stack level to blame for the error 1478 | -- @usage 1479 | -- local function slurp(file) 1480 | -- ... 1481 | -- if type(result) ~= 'string' then 1482 | -- resulterror('std.io.slurp', 1, err, 2) 1483 | -- end 1484 | resulterror = checktypes( 1485 | 'resulterror', T.string, T.integer, T.accept, opt(T.integer) 1486 | ) .. resulterror, 1487 | 1488 | --- A collection of @{ArgCheck} functions used by `normalize` APIs. 1489 | -- @table types 1490 | -- @tfield ArgCheck accept always succeeds 1491 | -- @tfield ArgCheck callable accept a function or functable 1492 | -- @tfield ArgCheck integer accept integer valued number 1493 | -- @tfield ArgCheck nil accept only `nil` 1494 | -- @tfield ArgCheck table accept any table 1495 | -- @tfield ArgCheck value accept any non-`nil` value 1496 | types = types, 1497 | 1498 | --- Split a typespec string into a table of normalized type names. 1499 | -- @function typesplit 1500 | -- @tparam string|table either `"?bool|:nometa"` or `{"boolean", ":nometa"}` 1501 | -- @treturn table a new list with duplicates removed and leading '?'s 1502 | -- replaced by a 'nil' element 1503 | typesplit = typesplit, 1504 | 1505 | }, { 1506 | 1507 | --- Metamethods 1508 | -- @section metamethods 1509 | 1510 | --- Lazy loading of typecheck modules. 1511 | -- Don't load everything on initial startup, wait until first attempt 1512 | -- to access a submodule, and then load it on demand. 1513 | -- @function __index 1514 | -- @string name submodule name 1515 | -- @treturn table|nil the submodule that was loaded to satisfy the missing 1516 | -- `name`, otherwise `nil` if nothing was found 1517 | -- @usage 1518 | -- local version = require 'typecheck'.version 1519 | __index = function(self, name) 1520 | local ok, t = pcall(require, 'typecheck.' .. name) 1521 | if ok then 1522 | rawset(self, name, t) 1523 | return t 1524 | end 1525 | end, 1526 | }) 1527 | 1528 | 1529 | --- Types 1530 | -- @section types 1531 | 1532 | --- Signature of an @{argscheck} callable. 1533 | -- @function ArgCheck 1534 | -- @tparam table argu a packed table (including `n` field) of all arguments 1535 | -- @int index into @argu* for argument to action 1536 | -- @return[1] nothing, to accept `argu[i]` 1537 | -- @treturn[2] string error message, to reject `argu[i]` immediately 1538 | -- @treturn[3] string the expected type of `argu[i]` 1539 | -- @treturn[3] string a description of rejected `argu[i]` 1540 | -- @usage 1541 | -- len = argscheck('len', any(types.table, types.string)) .. len 1542 | 1543 | --- Signature of a @{check} type predicate callable. 1544 | -- @function Predicate 1545 | -- @param x object to action 1546 | -- @treturn boolean `true` if *x* is of the expected type, otherwise `false` 1547 | -- @treturn[opt] string description of the actual type for error message 1548 | -------------------------------------------------------------------------------- /spec/init_spec.yaml: -------------------------------------------------------------------------------- 1 | # Gradual Function Type Checking for Lua 5.1, 5.2, 5.3 & 5.4 2 | # Copyright (C) 2014-2023 Gary V. Vaughan 3 | 4 | before: 5 | this_module = 'typecheck' 6 | 7 | M = require(this_module) 8 | 9 | specify typecheck: 10 | - describe require: 11 | - it does not perturb the global namespace: 12 | expect(show_apis {added_to='_G', by=this_module}). 13 | to_equal {} 14 | 15 | 16 | - describe resulterror: 17 | - before: | 18 | preamble = [[ 19 | local typecheck = require 'typecheck' -- line 2 20 | function ohnoes(n) -- line 3 21 | typecheck.resulterror('ohnoes', 1, nil, n) -- line 4 22 | end -- line 5 23 | function caller(n) -- line 6 24 | local r = ohnoes(n) -- line 7 25 | return 'not a tail call' -- line 8 26 | end -- line 9 27 | ]] 28 | 29 | f, badarg = init(M, this_module, 'resulterror') 30 | 31 | - context with bad arguments: 32 | - 'it diagnoses missing argument #1': 33 | expect(f()).to_raise 'string expected, got no value' 34 | - 'it diagnoses argument #1 type not string': 35 | stringy = setmetatable({}, {__tostring = function() end}) 36 | expect(f(nil)).to_raise 'string expected, got nil' 37 | expect(f(stringy)).to_raise 'string expected' 38 | - 'it diagnoses missing argument #2': 39 | expect(f 'X').to_raise 'integer expected, got no value' 40 | - 'it diagnoses argument #2 type not number': 41 | expect(f('X', nil)).to_raise 'integer expected, got nil' 42 | expect(f('X', 'X')).to_raise 'integer expected, got string' 43 | expect(f('X', 1.0001)).to_raise 'float has no integer representation' 44 | expect(f('X', '1.0001')).to_raise 'string has no integer representation' 45 | - 'it diagnoses argument #4 type not number': | 46 | expect(f('X', 1, 'ohnoes', false)). 47 | to_raise "bad argument #4 to 'resulterror' (integer expected, got boolean)" 48 | - 'it diagnoses argument #4 type not integer': | 49 | expect(f('X', 99999.0, 'ohnoes', 0.99999)). 50 | to_raise "bad argument #4 to 'resulterror' (float has no integer representation)" 51 | 52 | - it raises a result error: | 53 | expect(f('myfunc', 1)).to_raise "bad result #1 from 'myfunc'" 54 | - it supports optional extramsg argument: | 55 | expect(f('another', 3, 'oh noes')). 56 | to_raise "bad result #3 from 'another' (oh noes)" 57 | 58 | - context std._debug is not set: 59 | - before: | 60 | shamble = [[ 61 | require 'std._debug'(nil) -- line 1 62 | ]] .. preamble 63 | - it blames the call site by default: | 64 | expect(luaproc(shamble .. [[ 65 | caller() -- line 10 66 | ]])).to_contain_error ':4: bad result' 67 | - it honors optional call stack level reporting: | 68 | expect(luaproc(shamble .. [[ 69 | caller(1) -- line 10 70 | ]])).to_contain_error ':4: bad result' 71 | expect(luaproc(shamble .. [[ 72 | caller(2) -- line 10 73 | ]])).to_contain_error ':7: bad result' 74 | - it suppresses position information with level 0: | 75 | expect(luaproc(shamble .. [[ 76 | caller(0) -- line 10 77 | ]])).to_fail_while_matching '^[^:]+: bad result' 78 | 79 | - context std._debug set to false: 80 | - before: | 81 | shamble = [[ 82 | require 'std._debug'(false) -- line 1 83 | ]] .. preamble 84 | - it blames the call site by default: | 85 | expect(luaproc(shamble .. [[ 86 | caller() -- line 10 87 | ]])).to_contain_error ':4: bad result' 88 | - it honors optional call stack level reporting: | 89 | expect(luaproc(shamble .. [[ 90 | caller(1) -- line 10 91 | ]])).to_contain_error ':4: bad result' 92 | expect(luaproc(shamble .. [[ 93 | caller(2) -- line 10 94 | ]])).to_contain_error ':7: bad result' 95 | - it suppresses position information with level 0: | 96 | expect(luaproc(shamble .. [[ 97 | caller(0) -- line 10 98 | ]])).to_fail_while_matching '^[^:]+: bad result' 99 | 100 | - context std._debug set to true: 101 | - before: | 102 | shamble = [[ 103 | require 'std._debug'(true) -- line 1 104 | ]] .. preamble 105 | - it blames the call site by default: | 106 | expect(luaproc(shamble .. [[ 107 | caller() -- line 10 108 | ]])).to_contain_error ':4: bad result' 109 | - it honors optional call stack level reporting: | 110 | expect(luaproc(shamble .. [[ 111 | caller(1) -- line 10 112 | ]])).to_contain_error ':4: bad result' 113 | expect(luaproc(shamble .. [[ 114 | caller(2) -- line 10 115 | ]])).to_contain_error ':7: bad result' 116 | - it suppresses position information with level 0: | 117 | expect(luaproc(shamble .. [[ 118 | caller(0) -- line 10 119 | ]])).to_fail_while_matching '^[^:]+: bad result' 120 | 121 | - context std._debug argcheck hint is nil: 122 | - before: | 123 | shamble = [[ 124 | require 'std._debug'.argcheck = nil -- line 1 125 | ]] .. preamble 126 | - it blames the call site by default: | 127 | expect(luaproc(shamble .. [[ 128 | caller() -- line 10 129 | ]])).to_contain_error ':4: bad result' 130 | - it honors optional call stack level reporting: | 131 | expect(luaproc(shamble .. [[ 132 | caller(1) -- line 10 133 | ]])).to_contain_error ':4: bad result' 134 | expect(luaproc(shamble .. [[ 135 | caller(2) -- line 10 136 | ]])).to_contain_error ':7: bad result' 137 | - it suppresses position information with level 0: | 138 | expect(luaproc(shamble .. [[ 139 | caller(0) -- line 10 140 | ]])).to_fail_while_matching '^[^:]+: bad result' 141 | 142 | - context std._debug argcheck hint is false: 143 | - before: | 144 | shamble = [[ 145 | require 'std._debug'.argcheck = false -- line 1 146 | ]] .. preamble 147 | - it blames the call site by default: | 148 | expect(luaproc(shamble .. [[ 149 | caller() -- line 10 150 | ]])).to_contain_error ':4: bad result' 151 | - it honors optional call stack level reporting: | 152 | expect(luaproc(shamble .. [[ 153 | caller(1) -- line 10 154 | ]])).to_contain_error ':4: bad result' 155 | expect(luaproc(shamble .. [[ 156 | caller(2) -- line 10 157 | ]])).to_contain_error ':7: bad result' 158 | - it suppresses position information with level 0: | 159 | expect(luaproc(shamble .. [[ 160 | caller(0) -- line 10 161 | ]])).to_fail_while_matching '^[^:]+: bad result' 162 | 163 | - context std._debug argcheck hint is true: 164 | - before: | 165 | shamble = [[ 166 | require 'std._debug'.argcheck = true -- line 1 167 | ]] .. preamble 168 | - it blames the call site by default: | 169 | expect(luaproc(shamble .. [[ 170 | caller() -- line 10 171 | ]])).to_contain_error ':4: bad result' 172 | - it honors optional call stack level reporting: | 173 | expect(luaproc(shamble .. [[ 174 | caller(1) -- line 10 175 | ]])).to_contain_error ':4: bad result' 176 | expect(luaproc(shamble .. [[ 177 | caller(2) -- line 10 178 | ]])).to_contain_error ':7: bad result' 179 | - it suppresses position information with level 0: | 180 | expect(luaproc(shamble .. [[ 181 | caller(0) -- line 10 182 | ]])).to_fail_while_matching '^[^:]+: bad result' 183 | 184 | 185 | - describe argerror: 186 | - before: | 187 | preamble = [[ 188 | local typecheck = require 'typecheck' -- line 2 189 | function ohnoes(n) -- line 3 190 | typecheck.argerror('ohnoes', 1, nil, n) -- line 4 191 | end -- line 5 192 | function inner(n) -- line 6 193 | local r = ohnoes(n) -- line 7 194 | return 'not a tail call' -- line 8 195 | end -- line 9 196 | function caller(n) -- line 10 197 | local r = inner(n) -- line 11 198 | return 'not a tail call' -- line 12 199 | end -- line 13 200 | ]] 201 | 202 | f, badarg = init(M, this_module, 'argerror') 203 | 204 | - context with bad arguments: 205 | - 'it diagnoses missing argument #1': 206 | expect(f()).to_raise 'string expected, got no value' 207 | - 'it diagnoses argument #1 type not string': 208 | stringy = setmetatable({}, {__tostring = function() end}) 209 | expect(f(nil)).to_raise 'string expected, got nil' 210 | expect(f(stringy)).to_raise 'string expected' 211 | - 'it diagnoses missing argument #2': 212 | expect(f 'X').to_raise 'integer expected, got no value' 213 | - 'it diagnoses argument #2 type not number': 214 | expect(f('X', nil)).to_raise 'integer expected, got nil' 215 | expect(f('X', '1')).not_to_raise 'integer expected, got string' 216 | expect(f('X', 1.0001)).to_raise 'float has no integer representation' 217 | expect(f('X', '1.0001')).to_raise 'string has no integer representation' 218 | - 'it diagnoses argument #4 type not number': | 219 | expect(f('X', 1, 'ohnoes', false)). 220 | to_raise "bad argument #4 to 'argerror' (integer expected, got boolean)" 221 | - 'it diagnoses argument #4 type not integer': | 222 | expect(f('X', 99999.0, 'ohnoes', 0.99999)). 223 | to_raise "bad argument #4 to 'argerror' (float has no integer representation)" 224 | 225 | - it raises an argument error: | 226 | expect(f('myfunc', 1)).to_raise "bad argument #1 to 'myfunc'" 227 | - it supports optional extramsg argument: | 228 | expect(f('another', 3, 'oh noes')). 229 | to_raise "bad argument #3 to 'another' (oh noes)" 230 | 231 | - context std._debug set to nil: 232 | - before: | 233 | shamble = [[ 234 | require 'std._debug'(nil) -- line 1 235 | ]] .. preamble 236 | - it blames the call site by default: | 237 | expect(luaproc(shamble .. [[ 238 | caller() -- line 14 239 | ]])).to_contain_error ':7: bad argument' 240 | - it honors optional call stack level reporting: | 241 | expect(luaproc(shamble .. [[ 242 | caller(1) -- line 14 243 | ]])).to_contain_error ':7: bad argument' 244 | expect(luaproc(shamble .. [[ 245 | caller(2) -- line 14 246 | ]])).to_contain_error ':11: bad argument' 247 | expect(luaproc(shamble .. [[ 248 | caller(3) -- line 14 249 | ]])).to_contain_error ':14: bad argument' 250 | - it suppresses position information with level 0: | 251 | expect(luaproc(shamble .. [[ 252 | caller(0) -- line 14 253 | ]])).to_fail_while_matching '^[^:]+: bad argument' 254 | 255 | - context std._debug set to false: 256 | - before: | 257 | shamble = [[ 258 | require 'std._debug'(false) -- line 1 259 | ]] .. preamble 260 | - it blames the call site by default: | 261 | expect(luaproc(shamble .. [[ 262 | caller() -- line 14 263 | ]])).to_contain_error ':7: bad argument' 264 | - it honors optional call stack level reporting: | 265 | expect(luaproc(shamble .. [[ 266 | caller(1) -- line 14 267 | ]])).to_contain_error ':7: bad argument' 268 | expect(luaproc(shamble .. [[ 269 | caller(2) -- line 14 270 | ]])).to_contain_error ':11: bad argument' 271 | expect(luaproc(shamble .. [[ 272 | caller(3) -- line 14 273 | ]])).to_contain_error ':14: bad argument' 274 | - it suppresses position information with level 0: | 275 | expect(luaproc(shamble .. [[ 276 | caller(0) -- line 14 277 | ]])).to_fail_while_matching '^[^:]+: bad argument' 278 | 279 | - context std._debug set to true: 280 | - before: | 281 | shamble = [[ 282 | require 'std._debug'(true) -- line 1 283 | ]] .. preamble 284 | - it blames the call site by default: | 285 | expect(luaproc(shamble .. [[ 286 | caller() -- line 14 287 | ]])).to_contain_error ':7: bad argument' 288 | - it honors optional call stack level reporting: | 289 | expect(luaproc(shamble .. [[ 290 | caller(1) -- line 14 291 | ]])).to_contain_error ':7: bad argument' 292 | expect(luaproc(shamble .. [[ 293 | caller(2) -- line 14 294 | ]])).to_contain_error ':11: bad argument' 295 | expect(luaproc(shamble .. [[ 296 | caller(3) -- line 14 297 | ]])).to_contain_error ':14: bad argument' 298 | - it suppresses position information with level 0: | 299 | expect(luaproc(shamble .. [[ 300 | caller(0) -- line 14 301 | ]])).to_fail_while_matching '^[^:]+: bad argument' 302 | 303 | - context std._debug argcheck hint is nil: 304 | - before: | 305 | shamble = [[ 306 | require 'std._debug'.argcheck = nil -- line 1 307 | ]] .. preamble 308 | - it blames the call site by default: | 309 | expect(luaproc(shamble .. [[ 310 | caller() -- line 14 311 | ]])).to_contain_error ':7: bad argument' 312 | - it honors optional call stack level reporting: | 313 | expect(luaproc(shamble .. [[ 314 | caller(1) -- line 14 315 | ]])).to_contain_error ':7: bad argument' 316 | expect(luaproc(shamble .. [[ 317 | caller(2) -- line 14 318 | ]])).to_contain_error ':11: bad argument' 319 | expect(luaproc(shamble .. [[ 320 | caller(3) -- line 14 321 | ]])).to_contain_error ':14: bad argument' 322 | - it suppresses position information with level 0: | 323 | expect(luaproc(shamble .. [[ 324 | caller(0) -- line 14 325 | ]])).to_fail_while_matching '^[^:]+: bad argument' 326 | 327 | - context std._debug argcheck hint is false: 328 | - before: | 329 | shamble = [[ 330 | require 'std._debug'.argcheck = false -- line 1 331 | ]] .. preamble 332 | - it blames the call site by default: | 333 | expect(luaproc(shamble .. [[ 334 | caller() -- line 14 335 | ]])).to_contain_error ':7: bad argument' 336 | - it honors optional call stack level reporting: | 337 | expect(luaproc(shamble .. [[ 338 | caller(1) -- line 14 339 | ]])).to_contain_error ':7: bad argument' 340 | expect(luaproc(shamble .. [[ 341 | caller(2) -- line 14 342 | ]])).to_contain_error ':11: bad argument' 343 | expect(luaproc(shamble .. [[ 344 | caller(3) -- line 14 345 | ]])).to_contain_error ':14: bad argument' 346 | - it suppresses position information with level 0: | 347 | expect(luaproc(shamble .. [[ 348 | caller(0) -- line 14 349 | ]])).to_fail_while_matching '^[^:]+: bad argument' 350 | 351 | - context std._debug argcheck hint is true: 352 | - before: | 353 | shamble = [[ 354 | require 'std._debug'.argcheck = true -- line 1 355 | ]] .. preamble 356 | - it blames the call site by default: | 357 | expect(luaproc(shamble .. [[ 358 | caller() -- line 14 359 | ]])).to_contain_error ':7: bad argument' 360 | - it honors optional call stack level reporting: | 361 | expect(luaproc(shamble .. [[ 362 | caller(1) -- line 14 363 | ]])).to_contain_error ':7: bad argument' 364 | expect(luaproc(shamble .. [[ 365 | caller(2) -- line 14 366 | ]])).to_contain_error ':11: bad argument' 367 | expect(luaproc(shamble .. [[ 368 | caller(3) -- line 14 369 | ]])).to_contain_error ':14: bad argument' 370 | - it suppresses position information with level 0: | 371 | expect(luaproc(shamble .. [[ 372 | caller(0) -- line 14 373 | ]])).to_fail_while_matching '^[^:]+: bad argument' 374 | 375 | 376 | - describe argcheck: 377 | - before: | 378 | function clone(self, t) 379 | return setmetatable(t, getmetatable(self)) 380 | end 381 | 382 | Object = setmetatable({}, {_type = 'Object', __call = clone}) 383 | List = setmetatable({}, { _type = 'List', __call = clone}) 384 | Foo = setmetatable({}, { _type = 'Foo', __call = clone }) 385 | 386 | function fn(...) 387 | return M.argcheck('expect', 1, ...) 388 | end 389 | 390 | preamble = [[ 391 | local typecheck = require 'typecheck' -- line 2 392 | function ohnoes(t, n) -- line 3 393 | typecheck.argcheck('ohnoes', 1, 'table', t, n) -- line 4 394 | end -- line 5 395 | function inner(n) -- line 6 396 | local r = ohnoes('not a table', n) -- line 7 397 | return 'not a tail call' -- line 8 398 | end -- line 9 399 | function caller(n) -- line 10 400 | local r = inner(n) -- line 11 401 | return 'not a tail call' -- line 12 402 | end -- line 13 403 | ]] 404 | 405 | f, badarg = init(M, this_module, 'argcheck') 406 | 407 | - context with bad arguments: 408 | - 'it diagnoses missing argument #1': 409 | expect(f()).to_raise 'string expected, got no value' 410 | - 'it diagnoses argument #1 type not string': 411 | stringy = setmetatable({}, {__tostring = function() end}) 412 | expect(f(nil)).to_raise 'string expected, got nil' 413 | expect(f(stringy)).to_raise 'string expected' 414 | - 'it diagnoses missing argument #2': 415 | expect(f 'X').to_raise 'integer expected, got no value' 416 | - 'it diagnoses argument #2 type not number': 417 | expect(f('X', nil)).to_raise 'integer expected, got nil' 418 | expect(f('X', '1')).not_to_raise 'integer expected' 419 | - 'it diagnoses argument #2 type not integer': 420 | expect(f('X', 1.0001)).to_raise 'float has no integer representation' 421 | expect(f('X', '1.0001')).to_raise 'string has no integer representation' 422 | - 'it diagnoses missing argument #3': 423 | expect(f('X', 1)).to_raise 'string expected, got no value' 424 | - 'it diagnoses argument #3 type not string': 425 | stringy = setmetatable({}, {__tostring = function() end}) 426 | expect(f('X', 1, nil)).to_raise 'string expected, got nil' 427 | expect(f('X', 1, stringy)).to_raise 'string expected' 428 | - 'it diagnoses argument #5 type not number': | 429 | expect(f('X', 1, 'ohnoes', nil, false)). 430 | to_raise "bad argument #5 to 'argcheck' (integer expected, got boolean)" 431 | - 'it diagnoses argument #5 type not integer': | 432 | expect(f('X', 99999.0, 'ohnoes', nil, 0.99999)). 433 | to_raise "bad argument #5 to 'argcheck' (float has no integer representation)" 434 | 435 | - context std._debug set to nil: 436 | - before: | 437 | shamble = [[ 438 | require 'std._debug'(nil) -- line 1 439 | ]] .. preamble 440 | - it blames the call site by default: | 441 | expect(luaproc(shamble .. [[ 442 | caller() -- line 14 443 | ]])).to_contain_error ':7: bad argument' 444 | - it honors optional call stack level reporting: | 445 | expect(luaproc(shamble .. [[ 446 | caller(1) -- line 14 447 | ]])).to_contain_error ':7: bad argument' 448 | expect(luaproc(shamble .. [[ 449 | caller(2) -- line 14 450 | ]])).to_contain_error ':11: bad argument' 451 | expect(luaproc(shamble .. [[ 452 | caller(3) -- line 14 453 | ]])).to_contain_error ':14: bad argument' 454 | 455 | - context std._debug set to false: 456 | - before: | 457 | shamble = [[ 458 | require 'std._debug'(false) -- line 1 459 | ]] .. preamble 460 | - it is disabled: 461 | expect(luaproc(shamble .. [[ 462 | caller() -- line 14 463 | ]])).not_to_contain_error 'bad argument' 464 | expect(luaproc(shamble .. [[ 465 | caller(1) -- line 14 466 | ]])).not_to_contain_error 'bad argument' 467 | expect(luaproc(shamble .. [[ 468 | caller(2) -- line 14 469 | ]])).not_to_contain_error 'bad argument' 470 | expect(luaproc(shamble .. [[ 471 | caller(3) -- line 14 472 | ]])).not_to_contain_error 'bad argument' 473 | 474 | - context std._debug set to true: 475 | - before: | 476 | shamble = [[ 477 | require 'std._debug'(true) -- line 1 478 | ]] .. preamble 479 | - it blames the call site by default: | 480 | expect(luaproc(shamble .. [[ 481 | caller() -- line 14 482 | ]])).to_contain_error ':7: bad argument' 483 | - it honors optional call stack level reporting: | 484 | expect(luaproc(shamble .. [[ 485 | caller(1) -- line 14 486 | ]])).to_contain_error ':7: bad argument' 487 | expect(luaproc(shamble .. [[ 488 | caller(2) -- line 14 489 | ]])).to_contain_error ':11: bad argument' 490 | expect(luaproc(shamble .. [[ 491 | caller(3) -- line 14 492 | ]])).to_contain_error ':14: bad argument' 493 | 494 | - context std._debug argcheck hint is nil: 495 | - before: | 496 | shamble = [[ 497 | require 'std._debug'.argcheck = nil -- line 1 498 | ]] .. preamble 499 | - it blames the call site by default: | 500 | expect(luaproc(shamble .. [[ 501 | caller() -- line 14 502 | ]])).to_contain_error ':7: bad argument' 503 | - it honors optional call stack level reporting: | 504 | expect(luaproc(shamble .. [[ 505 | caller(1) -- line 14 506 | ]])).to_contain_error ':7: bad argument' 507 | expect(luaproc(shamble .. [[ 508 | caller(2) -- line 14 509 | ]])).to_contain_error ':11: bad argument' 510 | expect(luaproc(shamble .. [[ 511 | caller(3) -- line 14 512 | ]])).to_contain_error ':14: bad argument' 513 | 514 | - context std._debug argcheck hint is false: 515 | - before: | 516 | shamble = [[ 517 | require 'std._debug'.argcheck = false -- line 1 518 | ]] .. preamble 519 | - it is disabled: | 520 | expect(luaproc(shamble .. [[ 521 | caller() -- line 14 522 | ]])).not_to_contain_error 'bad argument' 523 | expect(luaproc(shamble .. [[ 524 | caller(1) -- line 14 525 | ]])).not_to_contain_error 'bad argument' 526 | expect(luaproc(shamble .. [[ 527 | caller(2) -- line 14 528 | ]])).not_to_contain_error 'bad argument' 529 | expect(luaproc(shamble .. [[ 530 | caller(3) -- line 14 531 | ]])).not_to_contain_error 'bad argument' 532 | 533 | - context std._debug argcheck hint is true: 534 | - before: | 535 | shamble = [[ 536 | require 'std._debug'.argcheck = true -- line 1 537 | ]] .. preamble 538 | - it blames the call site by default: | 539 | expect(luaproc(shamble .. [[ 540 | caller() -- line 14 541 | ]])).to_contain_error ':7: bad argument' 542 | - it honors optional call stack level reporting: | 543 | expect(luaproc(shamble .. [[ 544 | caller(1) -- line 14 545 | ]])).to_contain_error ':7: bad argument' 546 | expect(luaproc(shamble .. [[ 547 | caller(2) -- line 14 548 | ]])).to_contain_error ':11: bad argument' 549 | expect(luaproc(shamble .. [[ 550 | caller(3) -- line 14 551 | ]])).to_contain_error ':14: bad argument' 552 | 553 | - context with primitives: 554 | - it diagnoses nil argument: 555 | expect(fn('bool', nil)).to_raise 'boolean expected, got nil' 556 | expect(fn('boolean', nil)).to_raise 'boolean expected, got nil' 557 | expect(fn('file', nil)).to_raise 'FILE* expected, got nil' 558 | expect(fn('number', nil)).to_raise 'number expected, got nil' 559 | expect(fn('string', nil)).to_raise 'string expected, got nil' 560 | expect(fn('table', nil)).to_raise 'table expected, got nil' 561 | - it diagnoses mismatched types: 562 | expect(fn('bool', {0})).to_raise 'boolean expected, got table' 563 | expect(fn('boolean', {0})).to_raise 'boolean expected, got table' 564 | expect(fn('file', {0})).to_raise 'FILE* expected, got table' 565 | expect(fn('number', '1')).to_raise 'number expected, got string' 566 | expect(fn('number', '1.234')).to_raise 'number expected, got string' 567 | expect(fn('number', {0})).to_raise 'number expected, got table' 568 | expect(fn('string', {0})).to_raise 'string expected, got table' 569 | expect(fn('table', false)).to_raise 'table expected, got boolean' 570 | - it matches types: 571 | expect(fn('bool', true)).not_to_raise 'any error' 572 | expect(fn('boolean', true)).not_to_raise 'any error' 573 | expect(fn('file', io.stderr)).not_to_raise 'any error' 574 | expect(fn('number', 1)).not_to_raise 'any error' 575 | expect(fn('string', 's')).not_to_raise 'any error' 576 | expect(fn('table', {})).not_to_raise 'any error' 577 | expect(fn('table', Object)).not_to_raise 'any error' 578 | - context with int: 579 | - it diagnoses nil argument: 580 | expect(fn('int', nil)).to_raise 'integer expected, got nil' 581 | - it diagnoses mismatched types: 582 | expect(fn('int', false)).to_raise 'integer expected, got boolean' 583 | expect(fn('int', 1.234)).to_raise 'float has no integer representation' 584 | expect(fn('int', 1234e-3)).to_raise 'float has no integer representation' 585 | expect(fn('int', '1.234')).to_raise 'string has no integer representation' 586 | - it matches types: 587 | expect(fn('int', 1)).not_to_raise 'any error' 588 | expect(fn('int', 1.0)).not_to_raise 'any error' 589 | expect(fn('int', 0x1234)).not_to_raise 'any error' 590 | expect(fn('int', 1.234e3)).not_to_raise 'any error' 591 | - context with integer: 592 | - it diagnoses nil argument: 593 | expect(fn('integer', nil)).to_raise 'integer expected, got nil' 594 | - it diagnoses mismatched types: 595 | expect(fn('integer', false)).to_raise 'integer expected, got boolean' 596 | expect(fn('integer', 1.234)).to_raise 'float has no integer representation' 597 | expect(fn('integer', 1234e-3)).to_raise 'float has no integer representation' 598 | expect(fn('integer', '1.234')).to_raise 'string has no integer representation' 599 | - it matches types: 600 | expect(fn('integer', 1)).not_to_raise 'any error' 601 | expect(fn('integer', 1.0)).not_to_raise 'any error' 602 | expect(fn('integer', 0x1234)).not_to_raise 'any error' 603 | expect(fn('integer', 1.234e3)).not_to_raise 'any error' 604 | - context with constant string: 605 | - it diagnoses nil argument: 606 | expect(fn(':foo', nil)).to_raise ':foo expected, got nil' 607 | - it diagnoses mismatched types: 608 | expect(fn(':foo', false)).to_raise ':foo expected, got boolean' 609 | expect(fn(':foo', ':bar')).to_raise ':foo expected, got :bar' 610 | expect(fn(':foo', 'foo')).to_raise ':foo expected, got string' 611 | - it matches types: 612 | expect(fn(':foo', ':foo')).not_to_raise 'any error' 613 | - context with callable types: 614 | - before: 615 | ftable = setmetatable({0}, {__call = function() end}) 616 | - it diagnoses nil argument: 617 | expect(fn('callable', nil)).to_raise 'callable expected, got nil' 618 | expect(fn('func', nil)).to_raise 'function expected, got nil' 619 | expect(fn('functable', nil)).to_raise 'functable expected, got nil' 620 | expect(fn('function', nil)).to_raise 'function expected, got nil' 621 | expect(fn('functor', nil)).to_raise 'functable expected, got nil' 622 | - it diagnoses mismatched types: 623 | expect(fn('callable', {0})).to_raise 'callable expected, got table' 624 | expect(fn('func', ftable)).to_raise 'function expected, got functable' 625 | expect(fn('functable', fn)).to_raise 'functable expected, got function' 626 | expect(fn('function', ftable)).to_raise 'function expected, got functable' 627 | expect(fn('functor', fn)).to_raise 'functable expected, got function' 628 | - it matches types: 629 | expect(fn('callable', fn)).not_to_raise 'any error' 630 | expect(fn('callable', ftable)).not_to_raise 'any error' 631 | expect(fn('func', function() end)).not_to_raise 'any error' 632 | expect(fn('functable', ftable)).not_to_raise 'any error' 633 | expect(fn('function', fn)).not_to_raise 'any error' 634 | expect(fn('functor', ftable)).not_to_raise 'any error' 635 | - context with table of homogenous elements: 636 | - it diagnoses nil argument: 637 | expect(fn('table of boolean', nil)). 638 | to_raise 'table expected, got nil' 639 | expect(fn('table of booleans', nil)). 640 | to_raise 'table expected, got nil' 641 | - it diagnoses mismatched types: 642 | expect(fn('table of file', io.stderr)). 643 | to_raise 'table expected, got file' 644 | expect(fn('table of files', io.stderr)). 645 | to_raise 'table expected, got file' 646 | - it diagnoses mismatched element types: 647 | expect(fn('table of number', {false})). 648 | to_raise 'table of numbers expected, got boolean at index 1' 649 | expect(fn('table of numbers', {1, 2, '3'})). 650 | to_raise 'table of numbers expected, got string at index 3' 651 | expect(fn('table of numbers', {a=1, b=2, c='3'})). 652 | to_raise 'table of numbers expected, got string at index c' 653 | - it matches types: 654 | expect(fn('table of string', {})).not_to_raise 'any error' 655 | expect(fn('table of string', {'foo'})).not_to_raise 'any error' 656 | expect(fn('table of string', {'f', 'o', 'o'})).not_to_raise 'any error' 657 | expect(fn('table of string', {b='b', a='a', r='r'})).not_to_raise 'any error' 658 | - context with non-empty table types: 659 | - it diagnoses nil argument: 660 | expect(fn('#table', nil)). 661 | to_raise 'non-empty table expected, got nil' 662 | - it diagnoses mismatched types: 663 | expect(fn('#table', false)). 664 | to_raise 'non-empty table expected, got boolean' 665 | expect(fn('#table', {})). 666 | to_raise 'non-empty table expected, got empty table' 667 | - it matches types: 668 | expect(fn('#table', {0})).not_to_raise 'any error' 669 | - context with non-empty table of homogenous elements: 670 | - it diagnoses nil argument: 671 | expect(fn('#table of boolean', nil)). 672 | to_raise 'non-empty table expected, got nil' 673 | expect(fn('#table of booleans', nil)). 674 | to_raise 'non-empty table expected, got nil' 675 | - it diagnoses mismatched types: 676 | expect(fn('#table of file', {})). 677 | to_raise 'non-empty table expected, got empty table' 678 | expect(fn('#table of file', io.stderr)). 679 | to_raise 'non-empty table expected, got file' 680 | - it diagnoses mismatched element types: 681 | expect(fn('#table of number', {false})). 682 | to_raise 'non-empty table of numbers expected, got boolean at index 1' 683 | expect(fn('#table of numbers', {1, 2, '3'})). 684 | to_raise 'non-empty table of numbers expected, got string at index 3' 685 | expect(fn('#table of numbers', {a=1, b=2, c='3'})). 686 | to_raise 'non-empty table of numbers expected, got string at index c' 687 | - it matches types: 688 | expect(fn('#table of string', {'foo'})).not_to_raise 'any error' 689 | expect(fn('#table of string', {'f', 'o', 'o'})).not_to_raise 'any error' 690 | expect(fn('#table of string', {b='b', a='a', r='r'})).not_to_raise 'any error' 691 | - context with list: 692 | - it diagnonses nil argument: 693 | expect(fn('list', nil)). 694 | to_raise 'list expected, got nil' 695 | - it diagnoses mismatched types: 696 | expect(fn('list', false)). 697 | to_raise 'list expected, got boolean' 698 | expect(fn('list', {foo=1})). 699 | to_raise 'list expected, got table' 700 | expect(fn('list', Object)). 701 | to_raise 'list expected, got Object' 702 | - it matches types: 703 | expect(fn('list', {})).not_to_raise 'any error' 704 | expect(fn('list', {1})).not_to_raise 'any error' 705 | - context with list of homogenous elements: 706 | - it diagnoses nil argument: 707 | expect(fn('list of boolean', nil)). 708 | to_raise 'list expected, got nil' 709 | expect(fn('list of booleans', nil)). 710 | to_raise 'list expected, got nil' 711 | - it diagnoses mismatched types: 712 | expect(fn('list of file', io.stderr)). 713 | to_raise 'list expected, got file' 714 | expect(fn('list of files', io.stderr)). 715 | to_raise 'list expected, got file' 716 | expect(fn('list of files', {file=io.stderr})). 717 | to_raise 'list expected, got table' 718 | - it diagnoses mismatched element types: 719 | expect(fn('list of number', {false})). 720 | to_raise 'list of numbers expected, got boolean at index 1' 721 | expect(fn('list of numbers', {1, 2, '3'})). 722 | to_raise 'list of numbers expected, got string at index 3' 723 | - it matches types: 724 | expect(fn('list of string', {})).not_to_raise 'any error' 725 | expect(fn('list of string', {'foo'})).not_to_raise 'any error' 726 | expect(fn('list of string', {'f', 'o', 'o'})).not_to_raise 'any error' 727 | - context with non-empty list: 728 | - it diagnonses nil argument: 729 | expect(fn('#list', nil)). 730 | to_raise 'non-empty list expected, got nil' 731 | - it diagnoses mismatched types: 732 | expect(fn('#list', false)). 733 | to_raise 'non-empty list expected, got boolean' 734 | expect(fn('#list', {})). 735 | to_raise 'non-empty list expected, got empty list' 736 | expect(fn('#list', {foo=1})). 737 | to_raise 'non-empty list expected, got table' 738 | expect(fn('#list', Object)). 739 | to_raise 'non-empty list expected, got empty Object' 740 | expect(fn('#list', List {})). 741 | to_raise 'non-empty list expected, got empty List' 742 | - it matches types: 743 | expect(fn('#list', {1})).not_to_raise 'any error' 744 | - context with non-empty list of homogenous elements: 745 | - it diagnoses nil argument: 746 | expect(fn('#list of boolean', nil)). 747 | to_raise 'non-empty list expected, got nil' 748 | expect(fn('#list of booleans', nil)). 749 | to_raise 'non-empty list expected, got nil' 750 | - it diagnoses mismatched types: 751 | expect(fn('#list of file', {})). 752 | to_raise 'non-empty list expected, got empty table' 753 | expect(fn('#list of file', io.stderr)). 754 | to_raise 'non-empty list expected, got file' 755 | expect(fn('#list of files', {file=io.stderr})). 756 | to_raise 'non-empty list expected, got table' 757 | - it diagnoses mismatched element types: 758 | expect(fn('#list of number', {false})). 759 | to_raise 'non-empty list of numbers expected, got boolean at index 1' 760 | expect(fn('#list of numbers', {1, 2, '3'})). 761 | to_raise 'non-empty list of numbers expected, got string at index 3' 762 | - it matches types: 763 | expect(fn('#list of string', {'foo'})).not_to_raise 'any error' 764 | expect(fn('#list of string', {'f', 'o', 'o'})).not_to_raise 'any error' 765 | - context with container: 766 | - it diagnoses nil argument: 767 | expect(fn('List of boolean', nil)). 768 | to_raise 'List expected, got nil' 769 | expect(fn('List of booleans', nil)). 770 | to_raise 'List expected, got nil' 771 | - it diagnoses mismatched types: 772 | expect(fn('List of file', io.stderr)). 773 | to_raise 'List expected, got file' 774 | expect(fn('List of files', io.stderr)). 775 | to_raise 'List expected, got file' 776 | expect(fn('List of files', {file=io.stderr})). 777 | to_raise 'List expected, got table' 778 | - it diagnoses mismatched element types: 779 | expect(fn('List of number', List {false})). 780 | to_raise 'List of numbers expected, got boolean at index 1' 781 | expect(fn('List of numbers', List {1, 2, '3'})). 782 | to_raise 'List of numbers expected, got string at index 3' 783 | - it matches types: 784 | expect(fn('list of string', List {})).not_to_raise 'any error' 785 | expect(fn('list of string', List {'foo'})).not_to_raise 'any error' 786 | expect(fn('list of string', List {'f', 'o', 'o'})).not_to_raise 'any error' 787 | - context with object: 788 | - it diagnoses nil argument: 789 | expect(fn('object', nil)).to_raise 'object expected, got nil' 790 | expect(fn('Object', nil)).to_raise 'Object expected, got nil' 791 | expect(fn('Foo', nil)).to_raise 'Foo expected, got nil' 792 | expect(fn('any', nil)).to_raise 'any value expected, got nil' 793 | - it diagnoses mismatched types: 794 | expect(fn('object', {0})).to_raise 'object expected, got table' 795 | expect(fn('Object', {0})).to_raise 'Object expected, got table' 796 | expect(fn('object', {_type='Object'})).to_raise 'object expected, got table' 797 | expect(fn('Object', {_type='Object'})).to_raise 'Object expected, got table' 798 | expect(fn('Object', Foo)).to_raise 'Object expected, got Foo' 799 | expect(fn('Foo', {0})).to_raise 'Foo expected, got table' 800 | expect(fn('Foo', Object)).to_raise 'Foo expected, got Object' 801 | - it matches types: 802 | expect(fn('object', Object)).not_to_raise 'any error' 803 | expect(fn('object', Object {})).not_to_raise 'any error' 804 | expect(fn('object', Foo)).not_to_raise 'any error' 805 | expect(fn('object', Foo {})).not_to_raise 'any error' 806 | - it matches anything: 807 | expect(fn('any', true)).not_to_raise 'any error' 808 | expect(fn('any', {})).not_to_raise 'any error' 809 | expect(fn('any', Object)).not_to_raise 'any error' 810 | expect(fn('any', Foo {})).not_to_raise 'any error' 811 | - context with a list of valid types: 812 | - it diagnoses nil argument: 813 | expect(fn('string|table', nil)). 814 | to_raise 'string or table expected, got nil' 815 | expect(fn('string|list|#table', nil)). 816 | to_raise 'list, non-empty table or string expected, got nil' 817 | expect(fn('string|number|list|object', nil)). 818 | to_raise 'list, number, object or string expected, got nil' 819 | - it diagnoses mismatched elements: 820 | expect(fn('string|table', false)). 821 | to_raise 'string or table expected, got boolean' 822 | expect(fn('string|#table', {})). 823 | to_raise 'non-empty table or string expected, got empty table' 824 | expect(fn('string|number|#list|object', {})). 825 | to_raise 'non-empty list, number, object or string expected, got empty table' 826 | - it matches any type from a list: 827 | expect(fn('string|table', 'foo')).not_to_raise 'any error' 828 | expect(fn('string|table', {})).not_to_raise 'any error' 829 | expect(fn('string|table', {0})).not_to_raise 'any error' 830 | expect(fn('table|table', {})).not_to_raise 'any error' 831 | expect(fn('#table|table', {})).not_to_raise 'any error' 832 | - context with an optional type element: 833 | - it diagnoses mismatched elements: 834 | expect(fn('?boolean', 'string')). 835 | to_raise 'boolean or nil expected, got string' 836 | expect(fn('?boolean|:symbol', {})). 837 | to_raise ':symbol, boolean or nil expected, got empty table' 838 | - it matches nil against a single type: 839 | expect(fn('?any', nil)).not_to_raise 'any error' 840 | expect(fn('?boolean', nil)).not_to_raise 'any error' 841 | expect(fn('?string', nil)).not_to_raise 'any error' 842 | - it matches nil against a list of types: 843 | expect(fn('?boolean|table', nil)).not_to_raise 'any error' 844 | expect(fn('?string|table', nil)).not_to_raise 'any error' 845 | expect(fn('?table|#table', nil)).not_to_raise 'any error' 846 | expect(fn('?#table|table', nil)).not_to_raise 'any error' 847 | - it matches nil against a list of optional types: 848 | expect(fn('?boolean|?table', nil)).not_to_raise 'any error' 849 | expect(fn('?string|?table', nil)).not_to_raise 'any error' 850 | expect(fn('?table|?#table', nil)).not_to_raise 'any error' 851 | expect(fn('?#table|?table', nil)).not_to_raise 'any error' 852 | - it matches any named type: 853 | expect(fn('?any', false)).not_to_raise 'any error' 854 | expect(fn('?boolean', false)).not_to_raise 'any error' 855 | expect(fn('?string', 'string')).not_to_raise 'any error' 856 | - it matches any type from a list: 857 | expect(fn('?boolean|table', {})).not_to_raise 'any error' 858 | expect(fn('?string|table', {0})).not_to_raise 'any error' 859 | expect(fn('?table|#table', {})).not_to_raise 'any error' 860 | expect(fn('?#table|table', {})).not_to_raise 'any error' 861 | - it matches any type from a list with several optional specifiers: 862 | expect(fn('?boolean|?table', {})).not_to_raise 'any error' 863 | expect(fn('?string|?table', {0})).not_to_raise 'any error' 864 | expect(fn('?table|?table', {})).not_to_raise 'any error' 865 | expect(fn('?#table|?table', {})).not_to_raise 'any error' 866 | 867 | 868 | - describe argscheck: 869 | - before: | 870 | function mkstack(name, spec) 871 | return string.format([[ 872 | local argscheck = require 'typecheck'.argscheck -- line 1 873 | local function caller() -- line 2 874 | argscheck('%s', function() end) -- line 3 875 | end -- line 4 876 | caller() -- line 5 877 | ]], tostring(name), tostring(spec)) 878 | end 879 | 880 | f = M.argscheck 881 | 882 | mkmagic = function() 883 | return 'MAGIC' 884 | end 885 | wrapped = f('inner()', mkmagic) 886 | 887 | _, badarg, badresult = init(M, '', 'inner') 888 | id = function(...) 889 | return ... 890 | end 891 | 892 | - context with bad arguments: 893 | - 'it diagnoses missing argument #1': 894 | expect(f()).to_raise 'string expected, got no value' 895 | - 'it diagnoses argument #1 type not string': 896 | stringy = setmetatable({}, {__tostring = function() end}) 897 | expect(f(nil)).to_raise 'string expected, got nil' 898 | expect(f(stringy)).to_raise 'string expected' 899 | 900 | - it returns the wrapped function: 901 | expect(wrapped).not_to_be(inner) 902 | expect(wrapped()).to_be 'MAGIC' 903 | - it does not wrap the function when _ARGCHECK is disabled: | 904 | script = [[ 905 | require 'std._debug'(false) 906 | local argscheck = require 'typecheck'.argscheck 907 | local function inner() 908 | return 'MAGIC' 909 | end 910 | local wrapped = argscheck('inner(?any)', inner) 911 | os.exit(wrapped == inner and 0 or 1) 912 | ]] 913 | expect(luaproc(script)).to_succeed() 914 | 915 | - context used as an annotation: 916 | - before: 917 | wrapped = f 'mkmagic()' .. mkmagic 918 | - it returns the wrapped function: 919 | expect(wrapped).not_to_be(mkmagic) 920 | expect(wrapped()).to_be 'MAGIC' 921 | - it does not wrap the function when _ARGCHECK is disabled: | 922 | script = [[ 923 | require 'std._debug'(false) 924 | local argscheck = require 'typecheck'.argscheck 925 | local function inner() 926 | return 'MAGIC' 927 | end 928 | local wrapped = argscheck 'inner(?any)' .. inner 929 | os.exit(wrapped == inner and 0 or 1) 930 | ]] 931 | expect(luaproc(script)).to_succeed() 932 | 933 | - context when checking zero argument function: 934 | - it diagnoses too many arguments: 935 | expect(wrapped(false)).to_raise(badarg(1)) 936 | - it accepts correct argument types: 937 | expect(wrapped()).to_be 'MAGIC' 938 | 939 | - context with primitives: 940 | - before: 941 | wrapped = function(typespec) 942 | return f('inner(' .. typespec .. ')', mkmagic) 943 | end 944 | - it diagnoses missing argument: 945 | expect(wrapped('bool')()).to_raise 'boolean expected, got no value' 946 | expect(wrapped('boolean')()).to_raise 'boolean expected, got no value' 947 | expect(wrapped('file')()).to_raise 'FILE* expected, got no value' 948 | expect(wrapped('number')()).to_raise 'number expected, got no value' 949 | expect(wrapped('string')()).to_raise 'string expected, got no value' 950 | expect(wrapped('table')()).to_raise 'table expected, got no value' 951 | - it diagnoses nil argument: 952 | expect(wrapped('bool')(nil)).to_raise 'boolean expected, got nil' 953 | expect(wrapped('boolean')(nil)).to_raise 'boolean expected, got nil' 954 | expect(wrapped('file')(nil)).to_raise 'FILE* expected, got nil' 955 | expect(wrapped('number')(nil)).to_raise 'number expected, got nil' 956 | expect(wrapped('string')(nil)).to_raise 'string expected, got nil' 957 | expect(wrapped('table')(nil)).to_raise 'table expected, got nil' 958 | - it diagnoses mismatched types: 959 | expect(wrapped('bool')({0})).to_raise 'boolean expected, got table' 960 | expect(wrapped('boolean')({0})).to_raise 'boolean expected, got table' 961 | expect(wrapped('file')({0})).to_raise 'FILE* expected, got table' 962 | expect(wrapped('number')('1')).to_raise 'number expected, got string' 963 | expect(wrapped('number')('1.234')).to_raise 'number expected, got string' 964 | expect(wrapped('number')({0})).to_raise 'number expected, got table' 965 | expect(wrapped('string')({0})).to_raise 'string expected, got table' 966 | expect(wrapped('table')(false)).to_raise 'table expected, got boolean' 967 | - it matches types: 968 | expect(wrapped('bool')(true)).not_to_raise 'any error' 969 | expect(wrapped('boolean')(true)).not_to_raise 'any error' 970 | expect(wrapped('file')(io.stderr)).not_to_raise 'any error' 971 | expect(wrapped('number')(1)).not_to_raise 'any error' 972 | expect(wrapped('string')('s')).not_to_raise 'any error' 973 | expect(wrapped('table')({})).not_to_raise 'any error' 974 | expect(wrapped('table')(Object)).not_to_raise 'any error' 975 | 976 | - context when checking single argument function: 977 | - before: 978 | wrapped = f('inner(#table)', mkmagic) 979 | - it diagnoses missing arguments: 980 | expect(wrapped()).to_raise(badarg(1, 'non-empty table', 'no value')) 981 | - it diagnoses nil arguments: 982 | expect(wrapped(nil)).to_raise(badarg(1, 'non-empty table', 'nil')) 983 | - it diagnoses wrong argument types: 984 | expect(wrapped {}).to_raise(badarg(1, 'non-empty table', 'empty table')) 985 | - it diagnoses too many arguments: 986 | expect(wrapped({1}, 2, nop, '', false)).to_raise(badarg(1, 5)) 987 | - it accepts correct argument types: 988 | expect(wrapped({1})).to_be 'MAGIC' 989 | 990 | - context when checking multi-argument function: 991 | - before: 992 | wrapped = f('inner(table, function)', mkmagic) 993 | - it diagnoses missing arguments: 994 | expect(wrapped()).to_raise(badarg(1, 'table', 'no value')) 995 | expect(wrapped({})).to_raise(badarg(2, 'function', 'no value')) 996 | - it diagnoses nil arguments: 997 | expect(wrapped(nil)).to_raise(badarg(1, 'table', 'nil')) 998 | expect(wrapped({}, nil)).to_raise(badarg(2, 'function', 'nil')) 999 | - it diagnoses wrong argument types: 1000 | expect(wrapped(false)).to_raise(badarg(1, 'table', 'boolean')) 1001 | expect(wrapped({}, false)).to_raise(badarg(2, 'function', 'boolean')) 1002 | - it diagnoses too many arguments: 1003 | expect(wrapped({}, nop, false)).to_raise(badarg(3)) 1004 | - it accepts correct argument types: 1005 | expect(wrapped({}, nop)).to_be 'MAGIC' 1006 | 1007 | - context when checking nil argument function: 1008 | - before: 1009 | wrapped = f('inner(?int, string)', mkmagic) 1010 | - it diagnoses missing arguments: 1011 | expect(wrapped()).to_raise(badarg(1, 'integer or nil', 'no value')) 1012 | expect(wrapped(1)).to_raise(badarg(2, 'string', 'no value')) 1013 | - it diagnoses nil arguments: 1014 | expect(wrapped(nil)).not_to_raise(badarg(2, 'string', 'nil')) 1015 | expect(wrapped(1, nil)).to_raise(badarg(2, 'string', 'nil')) 1016 | - it diagnoses wrong argument types: 1017 | expect(wrapped(false)).to_raise(badarg(1, 'integer or nil', 'boolean')) 1018 | expect(wrapped(1, false)).to_raise(badarg(2, 'string', 'boolean')) 1019 | expect(wrapped(nil, false)).to_raise(badarg(2, 'string', 'boolean')) 1020 | - it diagnoses too many arguments: 1021 | expect(wrapped(1, 'foo', nop)).to_raise(badarg(3)) 1022 | expect(wrapped(nil, 'foo', nop)).to_raise(badarg(3)) 1023 | - it accepts correct argument types: 1024 | expect(wrapped(1, 'foo')).to_be 'MAGIC' 1025 | expect(wrapped(nil, 'foo')).to_be 'MAGIC' 1026 | 1027 | - context when checking optional multi-argument function: 1028 | - before: 1029 | wrapped = f('inner([int], string)', mkmagic) 1030 | - it diagnoses missing arguments: 1031 | expect(wrapped()).to_raise(badarg(1, 'integer or string', 'no value')) 1032 | expect(wrapped(1)).to_raise(badarg(2, 'string', 'no value')) 1033 | - it diagnoses nil arguments: 1034 | expect(wrapped(nil)).to_raise(badarg(1, 'integer or string', 'nil')) 1035 | expect(wrapped(1, nil)).to_raise(badarg(2, 'string', 'nil')) 1036 | - it diagnoses wrong argument types: 1037 | expect(wrapped(false)).to_raise(badarg(1, 'integer or string', 'boolean')) 1038 | - it diagnoses too many arguments: 1039 | expect(wrapped(1, 'two', nop)).to_raise(badarg(3)) 1040 | - it accepts correct argument types: 1041 | expect(wrapped('two')).to_be 'MAGIC' 1042 | expect(wrapped(1, 'two')).to_be 'MAGIC' 1043 | 1044 | - context when checking final optional multi-argument function: 1045 | - before: 1046 | wrapped = f('inner(?any, ?string, [any])', mkmagic) 1047 | it diagnoses missing arguments: 1048 | expect(wrapped()).to_raise(badarg(1, 'argument', 'no value')) 1049 | expect(wrapped(1)).to_raise(badarg(2, 'nil or string', 'no value')) 1050 | - it diagnoses wrong argument types: 1051 | expect(wrapped(1, false)).to_raise(badarg(2, 'nil or string', 'boolean')) 1052 | expect(wrapped(nil, false)).to_raise(badarg(2, 'nil or string', 'boolean')) 1053 | - it diagnoses too many arguments: 1054 | expect(wrapped(1, 'two', 3, false)).to_raise(badarg(4)) 1055 | expect(wrapped(nil, 'two', 3, false)).to_raise(badarg(4)) 1056 | expect(wrapped(1, nil, 3, false)).to_raise(badarg(4)) 1057 | expect(wrapped(nil, nil, 3, false)).to_raise(badarg(4)) 1058 | - it accepts correct argument types: 1059 | expect(wrapped(nil, 'two')).to_be 'MAGIC' 1060 | expect(wrapped(1, 'two')).to_be 'MAGIC' 1061 | expect(wrapped(nil, nil, 3)).to_be 'MAGIC' 1062 | expect(wrapped(1, nil, 3)).to_be 'MAGIC' 1063 | expect(wrapped(nil, 'two', 3)).to_be 'MAGIC' 1064 | expect(wrapped('one', 'two', 3)).to_be 'MAGIC' 1065 | 1066 | - context when checking final ellipsis function: 1067 | - before: 1068 | wrapped = f('inner(string, int...)', mkmagic) 1069 | - it diagnoses missing arguments: 1070 | expect(wrapped()).to_raise(badarg(1, 'string')) 1071 | expect(wrapped('foo')).to_raise(badarg(2, 'integer')) 1072 | - it diagnoses wrong argument types: 1073 | expect(wrapped(false)).to_raise(badarg(1, 'string', 'boolean')) 1074 | expect(wrapped('foo', false)).to_raise(badarg(2, 'integer', 'boolean')) 1075 | expect(wrapped('foo', 1, false)).to_raise(badarg(3, 'integer', 'boolean')) 1076 | expect(wrapped('foo', 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, false)). 1077 | to_raise(badarg(12, 'integer', 'boolean')) 1078 | - it accepts correct argument types: 1079 | expect(wrapped('foo', 1)).to_be 'MAGIC' 1080 | expect(wrapped('foo', 1, 2)).to_be 'MAGIC' 1081 | expect(wrapped('foo', 1, 2, 5)).to_be 'MAGIC' 1082 | 1083 | - context when checking optional final parameter: 1084 | - context with single argument: 1085 | - before: 1086 | wrapped = f('inner([int])', mkmagic) 1087 | - it diagnoses wrong argument types: 1088 | expect(wrapped(false)).to_raise(badarg(1, 'integer', 'boolean')) 1089 | - it diagnoses too many arguments: 1090 | expect(wrapped(1, nop)).to_raise(badarg(2)) 1091 | - it accepts correct argument types: 1092 | expect(wrapped()).to_be 'MAGIC' 1093 | expect(wrapped(1)).to_be 'MAGIC' 1094 | - context with trailing ellipsis: 1095 | - before: 1096 | wrapped = f('inner(string, [int]...)', mkmagic) 1097 | - it diagnoses missing arguments: 1098 | expect(wrapped()).to_raise(badarg(1, 'string')) 1099 | - it diagnoses wrong argument types: 1100 | expect(wrapped(false)).to_raise(badarg(1, 'string', 'boolean')) 1101 | expect(wrapped('foo', false)).to_raise(badarg(2, 'integer', 'boolean')) 1102 | expect(wrapped('foo', 1, false)).to_raise(badarg(3, 'integer', 'boolean')) 1103 | expect(wrapped('foo', 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, false)). 1104 | to_raise(badarg(12, 'integer', 'boolean')) 1105 | - it accepts correct argument types: 1106 | expect(wrapped('foo')).to_be 'MAGIC' 1107 | expect(wrapped('foo', 1)).to_be 'MAGIC' 1108 | expect(wrapped('foo', 1, 2)).to_be 'MAGIC' 1109 | expect(wrapped('foo', 1, 2, 5)).to_be 'MAGIC' 1110 | - context with inner ellipsis: 1111 | - before: 1112 | wrapped = f('inner(string, [int...])', mkmagic) 1113 | - it diagnoses missing arguments: 1114 | expect(wrapped()).to_raise(badarg(1, 'string')) 1115 | - it diagnoses wrong argument types: 1116 | expect(wrapped(false)).to_raise(badarg(1, 'string', 'boolean')) 1117 | expect(wrapped('foo', false)).to_raise(badarg(2, 'integer', 'boolean')) 1118 | expect(wrapped('foo', 1, false)).to_raise(badarg(3, 'integer', 'boolean')) 1119 | expect(wrapped('foo', 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, false)). 1120 | to_raise(badarg(12, 'integer', 'boolean')) 1121 | - it accepts correct argument types: 1122 | expect(wrapped('foo')).to_be 'MAGIC' 1123 | expect(wrapped('foo', 1)).to_be 'MAGIC' 1124 | expect(wrapped('foo', 1, 2)).to_be 'MAGIC' 1125 | expect(wrapped('foo', 1, 2, 5)).to_be 'MAGIC' 1126 | 1127 | - context when omitting self type: 1128 | - before: 1129 | me = { 1130 | wrapped = f('me:inner(string)', mkmagic) 1131 | } 1132 | _, badarg, badresult = init(M, '', 'me:inner') 1133 | - it diagnoses missing arguments: 1134 | expect(me:wrapped()).to_raise(badarg(1, 'string')) 1135 | - it diagnoses wrong argument types: 1136 | expect(me:wrapped(false)).to_raise(badarg(1, 'string', 'boolean')) 1137 | - it diagnoses too many arguments: 1138 | expect(me:wrapped('foo', false)).to_raise(badarg(2)) 1139 | - it accepts correct argument types: 1140 | expect(me:wrapped('foo')).to_be 'MAGIC' 1141 | 1142 | - context with too many args: 1143 | - before: 1144 | wrapped = f('inner([string], int)', mkmagic) 1145 | - it diagnoses missing arguments: 1146 | expect(wrapped()).to_raise(badarg(1, 'integer or string')) 1147 | expect(wrapped('one')).to_raise(badarg(2, 'integer')) 1148 | - it diagnoses wrong argument types: 1149 | expect(wrapped(false)).to_raise(badarg(1, 'integer or string', 'boolean')) 1150 | expect(wrapped('one', false)).to_raise(badarg(2, 'integer', 'boolean')) 1151 | - it diagnoses too many arguments: 1152 | expect(wrapped('one', 2, false)).to_raise(badarg(3)) 1153 | expect(wrapped(1, false)).to_raise(badarg(2)) 1154 | - it accepts correct argument types: 1155 | expect(wrapped(1)).to_be 'MAGIC' 1156 | expect(wrapped('one', 2)).to_be 'MAGIC' 1157 | 1158 | - context when checking single return value function: 1159 | - before: | 1160 | wrapped = f('inner([any...]) => #table', id) 1161 | - it diagnoses missing results: 1162 | expect(wrapped()).to_raise(badresult(1, 'non-empty table')) 1163 | - it diagnoses wrong result types: 1164 | expect(wrapped {}). 1165 | to_raise(badresult(1, 'non-empty table', 'empty table')) 1166 | - it diagnoses too many results: 1167 | expect(wrapped({1}, 2, nop, '', false)).to_raise(badresult(1, 5)) 1168 | - it accepts correct results: 1169 | expect({wrapped {1}}).to_equal {{1}} 1170 | 1171 | - context with variant single return value function: 1172 | - before: 1173 | wrapped = f('inner([any...]) => [?int]', id) 1174 | - it diagnoses wrong result types: 1175 | expect(wrapped(false)).to_raise(badresult(1, 'integer or nil', 'boolean')) 1176 | - it diagnoses too many results: 1177 | expect(wrapped(1, nop)).to_raise(badresult(2)) 1178 | - it accepts correct result types: 1179 | expect({wrapped()}).to_equal {} 1180 | expect({wrapped(1)}).to_equal {1} 1181 | 1182 | - context when checking multi-return value function: 1183 | - before: 1184 | wrapped = f('inner([any...]) => int, string', id) 1185 | - it diagnoses missing results: 1186 | expect(wrapped()).to_raise(badresult(1, 'integer')) 1187 | expect(wrapped(1)).to_raise(badresult(2, 'string')) 1188 | - it diagnoses wrong result types: 1189 | expect(wrapped(false)).to_raise(badresult(1, 'integer', 'boolean')) 1190 | expect(wrapped(1, false)).to_raise(badresult(2, 'string', 'boolean')) 1191 | - it diagnoses too many results: 1192 | expect(wrapped(1, 'two', false)).to_raise(badresult(3)) 1193 | - it accepts correct argument types: 1194 | expect({wrapped(1, 'two')}).to_equal {1, 'two'} 1195 | 1196 | - context when checking nil return specifier: 1197 | - before: 1198 | wrapped = f('inner(?any...) => ?int, string', id) 1199 | - it diagnoses wrong result types: 1200 | expect(wrapped(false)).to_raise(badresult(1, 'integer or nil', 'boolean')) 1201 | expect(wrapped(1, false)).to_raise(badresult(2, 'string', 'boolean')) 1202 | expect(wrapped(nil, false)).to_raise(badresult(2, 'string', 'boolean')) 1203 | - it diagnoses too many results: 1204 | expect(wrapped(1, 'foo', nop)).to_raise(badresult(3)) 1205 | expect(wrapped(nil, 'foo', nop)).to_raise(badresult(3)) 1206 | - it accepts correct result types: 1207 | expect({wrapped(1, 'foo')}).to_equal {1, 'foo'} 1208 | expect({wrapped(nil, 'foo')}).to_equal {[2] = 'foo'} 1209 | 1210 | - context when checking variant multi-return value function: 1211 | - before: 1212 | wrapped = f('inner([any...]) => int, string or string', id) 1213 | - it diagnoses missing results: 1214 | expect(wrapped()).to_raise(badresult(1, 'integer or string')) 1215 | expect(wrapped(1)).to_raise(badresult(2, 'string')) 1216 | - it diagnoses wrong result types: 1217 | expect(wrapped(false)).to_raise(badresult(1, 'integer or string', 'boolean')) 1218 | - it diagnoses too many results: 1219 | expect(wrapped(1, 'two', nop)).to_raise(badresult(3)) 1220 | - it accepts correct result types: 1221 | expect({wrapped('two')}).to_equal {'two'} 1222 | expect({wrapped(1, 'two')}).to_equal {1, 'two'} 1223 | 1224 | - context when checking variant nil,errmsg pattern function: 1225 | - before: 1226 | wrapped = f('inner([?any...]) => int, string or nil, string', id) 1227 | - it diagnoses missing results: 1228 | expect(wrapped(nil)).to_raise(badresult(2, 'string')) 1229 | expect(wrapped(1)).to_raise(badresult(2, 'string')) 1230 | - it diagnoses wrong result types: 1231 | expect(wrapped(false)).to_raise(badresult(1, 'integer or nil', 'boolean')) 1232 | expect(wrapped(1, false)).to_raise(badresult(2, 'string', 'boolean')) 1233 | - it diagnoses too many results: 1234 | expect(wrapped(1, 'two', nop)).to_raise(badresult(3)) 1235 | expect(wrapped(nil, 'errmsg', nop)).to_raise(badresult(3)) 1236 | - it accepts correct result types: 1237 | expect({wrapped(1, 'two')}).to_equal {1, 'two'} 1238 | expect({wrapped(nil, 'errmsg')}).to_equal {[2] = 'errmsg'} 1239 | 1240 | - context when checking optional multi-return value function: 1241 | - before: 1242 | wrapped = f('inner([any...]) => [int], string', id) 1243 | - it diagnoses missing results: 1244 | expect(wrapped()).to_raise(badresult(1, 'integer or string')) 1245 | expect(wrapped(1)).to_raise(badresult(2, 'string')) 1246 | - it diagnoses wrong result types: 1247 | expect(wrapped(false)).to_raise(badresult(1, 'integer or string', 'boolean')) 1248 | - it diagnoses too many results: 1249 | expect(wrapped(1, 'two', nop)).to_raise(badresult(3)) 1250 | - it accepts correct result types: 1251 | expect({wrapped('two')}).to_equal {'two'} 1252 | expect({wrapped(1, 'two')}).to_equal {1, 'two'} 1253 | 1254 | - context when checking final optional multi-return value function: 1255 | - before: 1256 | wrapped = f('inner([?any...]) => ?any, ?string, [any]', id) 1257 | - it diagnoses missing results: 1258 | expect({wrapped()}).to_raise(badresult(1, 'result', 'no value')) 1259 | expect({wrapped(nil)}).to_raise(badresult(2, 'nil or string', 'no value')) 1260 | expect({wrapped(1)}).to_raise(badresult(2, 'nil or string', 'no value')) 1261 | - it diagnoses wrong result types: 1262 | expect(wrapped(1, false)).to_raise(badresult(2, 'nil or string', 'boolean')) 1263 | expect(wrapped(nil, false)).to_raise(badresult(2, 'nil or string', 'boolean')) 1264 | - it diagnoses too many results: 1265 | expect(wrapped(1, 'two', 3, false)).to_raise(badresult(4)) 1266 | expect(wrapped(nil, 'two', 3, false)).to_raise(badresult(4)) 1267 | expect(wrapped(1, nil, 3, false)).to_raise(badresult(4)) 1268 | expect(wrapped(nil, nil, 3, false)).to_raise(badresult(4)) 1269 | - it accepts correct result types: 1270 | expect({wrapped(nil, 'two')}).to_equal {[2]='two'} 1271 | expect({wrapped(1, 'two')}).to_equal {1, 'two'} 1272 | expect({wrapped(nil, nil, 3)}).to_equal {[3]=3} 1273 | expect({wrapped(1, nil, 3)}).to_equal {1, [3]=3} 1274 | expect({wrapped(nil, 'two', 3)}).to_equal {[2]='two', [3]=3} 1275 | expect({wrapped('one', 'two', 3)}).to_equal {'one', 'two', 3} 1276 | 1277 | - context when checking optional final result: 1278 | - context with single result: 1279 | - before: 1280 | wrapped = f('inner([any...]) => [int]', id) 1281 | - it diagnoses wrong result types: 1282 | expect(wrapped(false)).to_raise(badresult(1, 'integer', 'boolean')) 1283 | - it diagnoses too many results: 1284 | expect(wrapped(1, nop)).to_raise(badresult(2)) 1285 | - it accepts correct result types: 1286 | expect({wrapped()}).to_equal {} 1287 | expect({wrapped(1)}).to_equal {1} 1288 | - context with trailing ellipsis: 1289 | - before: 1290 | wrapped = f('inner([any...]) => string, [int]...', id) 1291 | - it diagnoses missing results: 1292 | expect(wrapped()).to_raise(badresult(1, 'string')) 1293 | - it diagnoses wrong result types: 1294 | expect(wrapped(false)).to_raise(badresult(1, 'string', 'boolean')) 1295 | expect(wrapped('foo', false)).to_raise(badresult(2, 'integer', 'boolean')) 1296 | expect(wrapped('foo', 1, false)).to_raise(badresult(3, 'integer', 'boolean')) 1297 | expect(wrapped('foo', 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, false)). 1298 | to_raise(badresult(12, 'integer', 'boolean')) 1299 | - it accepts correct result types: 1300 | expect({wrapped('foo')}).to_equal {'foo'} 1301 | expect({wrapped('foo', 1)}).to_equal {'foo', 1} 1302 | expect({wrapped('foo', 1, 2)}).to_equal {'foo', 1, 2} 1303 | expect({wrapped('foo', 1, 2, 5)}).to_equal {'foo', 1, 2, 5} 1304 | - context with inner ellipsis: 1305 | - before: 1306 | wrapped = f('inner([any...]) => string, [int...]', id) 1307 | - it diagnoses missing results: 1308 | expect(wrapped()).to_raise(badresult(1, 'string')) 1309 | - it diagnoses wrong result types: 1310 | expect(wrapped(false)).to_raise(badresult(1, 'string', 'boolean')) 1311 | expect(wrapped('foo', false)).to_raise(badresult(2, 'integer', 'boolean')) 1312 | expect(wrapped('foo', 1, false)).to_raise(badresult(3, 'integer', 'boolean')) 1313 | expect(wrapped('foo', 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, false)). 1314 | to_raise(badresult(12, 'integer', 'boolean')) 1315 | - it accepts correct result types: 1316 | expect({wrapped('foo')}).to_equal {'foo'} 1317 | expect({wrapped('foo', 1)}).to_equal {'foo', 1} 1318 | expect({wrapped('foo', 1, 2)}).to_equal {'foo', 1, 2} 1319 | expect({wrapped('foo', 1, 2, 5)}).to_equal {'foo', 1, 2, 5} 1320 | 1321 | - context with too many results: 1322 | - before: 1323 | wrapped = f('inner([any...]) => [string], int', id) 1324 | - it diagnoses missing results: 1325 | expect(wrapped()).to_raise(badresult(1, 'integer or string')) 1326 | expect(wrapped 'one').to_raise(badresult(2, 'integer')) 1327 | - it diagnoses wrong result types: 1328 | expect(wrapped(false)). 1329 | to_raise(badresult(1, 'integer or string', 'boolean')) 1330 | expect(wrapped('one', false)). 1331 | to_raise(badresult(2, 'integer', 'boolean')) 1332 | - it diagnoses too many results: 1333 | expect(wrapped('one', 2, false)).to_raise(badresult(3)) 1334 | expect(wrapped(1, false)).to_raise(badresult(2)) 1335 | - it accepts correct argument types: 1336 | expect({wrapped(1)}).to_equal {1} 1337 | expect({wrapped('one', 2)}).to_equal {'one', 2} 1338 | 1339 | 1340 | - describe extramsg_mismatch: 1341 | - before: 1342 | f = M.extramsg_mismatch 1343 | 1344 | - context old calling convention: 1345 | - it returns the expected types: 1346 | expect(f 'nil').to_contain 'nil expected, ' 1347 | expect(f 'bool').to_contain 'boolean expected, ' 1348 | expect(f '?bool').to_contain 'boolean or nil expected, ' 1349 | expect(f 'string|table').to_contain 'string or table expected, ' 1350 | - it returns expected container types: 1351 | expect(f('table of int', nil, 1)).to_contain 'table of integers expected, ' 1352 | expect(f('table of int|bool', nil, 1)). 1353 | to_contain 'table of integers or booleans expected, ' 1354 | expect(f('table of int|bool|string', nil, 1)). 1355 | to_contain 'table of integers, booleans or strings expected, ' 1356 | expect(f('table of int|bool|string|table', nil, 1)). 1357 | to_contain 'table of integers, booleans, strings or tables expected, ' 1358 | - it returns the actual type: 1359 | expect(f 'int').to_contain ', got nil' 1360 | expect(f('int', false)).to_contain ', got boolean' 1361 | expect(f('int', {})).to_contain ', got empty table' 1362 | - it returns table field type: 1363 | expect(f('table of int', nil, 1)).to_contain ', got nil at index 1' 1364 | expect(f('table of int', 'two', 2)).to_contain ', got string at index 2' 1365 | expect(f('table of int|bool', 'five', 3)).to_contain ', got string at index 3' 1366 | - context new calling convention: 1367 | - it returns the expected types: 1368 | expect(f(1, 'nil', {n=1})).to_contain 'nil expected, ' 1369 | expect(f(1, 'bool', {n=1})).to_contain 'boolean expected, ' 1370 | expect(f(1, '?bool', {n=1})).to_contain 'boolean or nil expected, ' 1371 | expect(f(1, 'string|table', {n=1})).to_contain 'string or table expected, ' 1372 | - it returns the actual type: 1373 | expect(f(1, 'bool', {n=0})).to_contain ', got no value' 1374 | expect(f(1, 'bool', {n=1})).to_contain ', got nil' 1375 | expect(f(1, 'bool', {n=1, false})).to_contain ', got boolean' 1376 | expect(f(1, 'bool', {n=1, {}})).to_contain ', got empty table' 1377 | - it diagnoses missing atguments: 1378 | expect(f(3, 'bool', {n=2})).to_contain 'boolean expected, got no value' 1379 | expect(f(3, 'bool', {n=3})).to_contain 'boolean expected, got nil' 1380 | 1381 | 1382 | - describe extramsg_toomany: 1383 | - before: 1384 | f = M.extramsg_toomany 1385 | 1386 | - it returns the expected thing: 1387 | expect(f('mojo', 1, 2)).to_contain 'no more than 1 mojo' 1388 | - it uses singular thing when 1 is expected: 1389 | expect(f('argument', 1, 2)).to_contain 'no more than 1 argument' 1390 | - it uses plural thing otherwise: 1391 | expect(f('thing', 0, 3)).to_contain 'no more than 0 things' 1392 | expect(f('result', 2, 3)).to_contain 'no more than 2 results' 1393 | - it returns the actual count: 1394 | expect(f('bad', 0, 1)).to_contain ', got 1' 1395 | expect(f('bad', 99, 999)).to_contain ', got 999' 1396 | -------------------------------------------------------------------------------- /spec/spec_helper.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Gradual Function Type Checking for Lua 5.1, 5.2, 5.3 & 5.4 3 | Copyright (C) 2014-2023 Gary V. Vaughan 4 | ]] 5 | 6 | local inprocess = require 'specl.inprocess' 7 | local hell = require 'specl.shell' 8 | local std = require 'specl.std' 9 | 10 | badargs = require 'specl.badargs' 11 | 12 | package.path = std.package.normalize('./lib/?.lua', './lib/?/init.lua', package.path) 13 | 14 | 15 | -- Allow user override of LUA binary used by hell.spawn, falling 16 | -- back to environment PATH search for 'lua' if nothing else works. 17 | local LUA = os.getenv 'LUA' or 'lua' 18 | 19 | 20 | function nop() end 21 | 22 | 23 | local unpack = table.unpack or unpack 24 | 25 | 26 | local function xform_gsub(match, replace) 27 | return function(s) 28 | return (string.gsub(s, match, replace)) 29 | end 30 | end 31 | 32 | 33 | local XFORMS = { 34 | xform_gsub('|%?', '|nil|'), 35 | xform_gsub('|nil|', '|no value|'), 36 | xform_gsub('|any|', '|any value|'), 37 | xform_gsub('|#', '|non-empty '), 38 | xform_gsub('|func|', '|function|'), 39 | xform_gsub('|file|', '|FILE*|'), 40 | xform_gsub('^|', ''), 41 | xform_gsub('|$', ''), 42 | xform_gsub('|([^|]+)$', 'or %1'), 43 | xform_gsub('|', ', '), 44 | } 45 | 46 | 47 | -- In case we're not using a bleeding edge release of Specl... 48 | badargs.result = badargs.result or function(fname, i, want, got) 49 | if want == nil then 50 | -- numbers only for narg error 51 | i, want = i - 1, i 52 | end 53 | 54 | if got == nil and type(want) == 'number' then 55 | local s = "bad result #%d from '%s' (no more than %d result%s expected, got %d)" 56 | return string.format(s, i + 1, fname, i, i == 1 and '' or 's', want) 57 | end 58 | 59 | local function showarg(s) 60 | local r = '|' .. s .. '|' 61 | for _, fn in ipairs(XFORMS) do 62 | r = fn(r) 63 | end 64 | return r 65 | end 66 | 67 | return string.format("bad result #%d from '%s' (%s expected, got %s)", 68 | i, fname, showarg(want), got or 'no value') 69 | end 70 | 71 | 72 | -- Wrap up badargs function in a succinct single call. 73 | function init(M, mname, fname) 74 | local name = string.gsub(mname .. '.' .. fname, '^%.', '') 75 | return M[fname], 76 | function(...) 77 | return badargs.format(name, ...) 78 | end, 79 | function(...) 80 | return badargs.result(name, ...) 81 | end 82 | end 83 | 84 | 85 | local function mkscript(code) 86 | local f = os.tmpname() 87 | local h = io.open(f, 'w') 88 | h:write(code) 89 | h:close() 90 | return f 91 | end 92 | 93 | 94 | --- Run some Lua code with the given arguments and input. 95 | -- @string code valid Lua code 96 | -- @tparam[opt={}] string|table arg single argument, or table of 97 | -- arguments for the script invocation. 98 | -- @string[opt] stdin standard input contents for the script process 99 | -- @treturn specl.shell.Process|nil status of resulting process if 100 | -- execution was successful, otherwise nil 101 | function luaproc(code, arg, stdin) 102 | local f = mkscript(code) 103 | if type(arg) ~= 'table' then 104 | arg = {arg} 105 | end 106 | local cmd = {LUA, f, unpack(arg, 1, #arg)} 107 | -- inject env and stdin keys separately to avoid truncating `...` in 108 | -- cmd constructor 109 | cmd.env = { LUA_PATH=package.path, LUA_INIT='', LUA_INIT_5_2='' } 110 | cmd.stdin = stdin 111 | local proc = hell.spawn(cmd) 112 | os.remove(f) 113 | return proc 114 | end 115 | 116 | 117 | local function tabulate_output(code) 118 | local proc = luaproc(code) 119 | if proc.status ~= 0 then 120 | return error(proc.errout) 121 | end 122 | local r = {} 123 | string.gsub(proc.output, '(%S*)[%s]*', function(x) 124 | if x ~= '' then 125 | r[x] = true 126 | end 127 | end) 128 | return r 129 | end 130 | 131 | 132 | --- Show changes to tables wrought by a require statement. 133 | -- There are a few modes to this function, controlled by what named 134 | -- arguments are given. Lists new keys in T1 after `require 'import'`: 135 | -- 136 | -- show_apis {added_to=T1, by=import} 137 | -- 138 | -- List keys returned from `require 'import'`, which have the same 139 | -- value in T1: 140 | -- 141 | -- show_apis {from=T1, used_by=import} 142 | -- 143 | -- List keys from `require 'import'`, which are also in T1 but with 144 | -- a different value: 145 | -- 146 | -- show_apis {from=T1, enhanced_by=import} 147 | -- 148 | -- List keys from T2, which are also in T1 but with a different value: 149 | -- 150 | -- show_apis {from=T1, enhanced_in=T2} 151 | -- 152 | -- @tparam table argt one of the combinations above 153 | -- @treturn table a list of keys according to criteria above 154 | function show_apis(argt) 155 | return tabulate_output([[ 156 | local before, after = {}, {} 157 | for k in pairs(]] .. argt.added_to .. [[) do 158 | before[k] = true 159 | end 160 | 161 | local M = require ']] .. argt.by .. [[' 162 | for k in pairs(]] .. argt.added_to .. [[) do 163 | after[k] = true 164 | end 165 | 166 | for k in pairs(after) do 167 | if not before[k] then 168 | print(k) 169 | end 170 | end 171 | ]]) 172 | end 173 | 174 | 175 | do 176 | local matchers = require 'specl.matchers'.matchers 177 | 178 | -- Alias that doesn't tickle sc_error_message_uppercase. 179 | matchers.raise = matchers.error 180 | end 181 | -------------------------------------------------------------------------------- /spec/version_spec.yaml: -------------------------------------------------------------------------------- 1 | # Gradual Function Type Checking for Lua 5.1, 5.2, 5.3 & 5.4 2 | # Copyright (C) 2014-2023 Gary V. Vaughan 3 | 4 | before: 5 | this_module = 'typecheck.version' 6 | 7 | M = require(this_module) 8 | 9 | specify std.version: 10 | - context when required: 11 | - it returns a string: 12 | expect(type(M)).to_be 'string' 13 | - it does not touch the global table: 14 | expect(show_apis {added_to='_G', by=this_module}). 15 | to_equal {} 16 | 17 | - describe version: 18 | - it describes this module: 19 | expect(M).to_match '^Gradual Function Typechecks' 20 | - it ends with the release number: 21 | expect(M).to_match.any_of {' %d[%.%d]*$', 'git'} 22 | -------------------------------------------------------------------------------- /typecheck-git-1.rockspec: -------------------------------------------------------------------------------- 1 | local _MODREV, _SPECREV = 'git', '-1' 2 | 3 | package = 'typecheck' 4 | version = _MODREV .. _SPECREV 5 | 6 | rockspec_format = '3.0' 7 | 8 | description = { 9 | summary = 'Gradual type checking for Lua functions.', 10 | detailed = [[ 11 | A Luaish run-time gradual type checking system, for argument and 12 | return types at function boundaries with simple annotations that can 13 | be enabled or disabled for production code, with a Lua API modelled 14 | on the core Lua C language API. 15 | ]], 16 | homepage = 'http://gvvaughan.github.io/typecheck', 17 | issues_url = 'https://github.com/gvvaughan/typecheck/issues', 18 | license = 'MIT/X11', 19 | } 20 | 21 | source = { 22 | url = 'http://github.com/gvvaughan/typecheck/archive/v' .. _MODREV .. '.zip', 23 | dir = 'typecheck-' .. _MODREV, 24 | } 25 | 26 | dependencies = { 27 | 'lua >= 5.1, < 5.5', 28 | 'std._debug >= 1.0.1', 29 | } 30 | 31 | build_dependencies = { 32 | 'ldoc', 33 | } 34 | 35 | build = { 36 | modules = { 37 | ['typecheck'] = 'lib/typecheck/init.lua', 38 | ['typecheck.version'] = 'lib/typecheck/version.lua', 39 | }, 40 | copy_directories = {'doc'}, 41 | } 42 | 43 | test_dependencies = { 44 | 'ansicolors', 45 | 'luacov', 46 | 'specl', 47 | } 48 | 49 | test = { 50 | type = 'command', 51 | command = 'make check', 52 | } 53 | 54 | if _MODREV == 'git' then 55 | source = { 56 | url = 'git://github.com/gvvaughan/typecheck.git', 57 | } 58 | end 59 | --------------------------------------------------------------------------------