├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── algorithm.nelua ├── backtrace.nelua ├── bigint.nelua ├── datetime.nelua ├── docs ├── backtrace.md ├── ffi.md ├── fs.md └── nester.md ├── examples ├── bigint_pi_digits.nelua ├── bigint_rsa.nelua ├── json_example.nelua └── tuple_example.nelua ├── ffi.nelua ├── fs.nelua ├── heapqueue.nelua ├── inspector.nelua ├── json.nelua ├── nester.nelua ├── or_return.nelua ├── signal.nelua ├── sqlite3.nelua ├── tests ├── Makefile ├── algorithm_test.nelua ├── backtrace_test.nelua ├── bigint_test.nelua ├── datetime_test.nelua ├── ffi_module.nelua ├── ffi_test.nelua ├── fs_test.nelua ├── inspector_test.nelua ├── json_test.nelua ├── nester_test.nelua ├── signal_test.nelua └── sqlite3_test.nelua └── tuple.nelua /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.nelua text eol=lf linguist-language=lua 3 | *.h text eol=lf linguist-language=c 4 | *.c text eol=lf linguist-language=c 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.so 3 | *.a 4 | *.dll 5 | *.o 6 | *.sqlite3 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Eduardo Bart (https://github.com/edubart) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a collection of various useful libraries for Nelua, 2 | with the following characteristics: 3 | 4 | * All libraries are designed to work well with Nelua. 5 | * All libraries have minor tests and examples of use. 6 | * All libraries are classified as "Tier 2" quality, 7 | while the Nelua compiler is "Tier 1" quality, 8 | this means that APIs here are more unstable and less tested than the Nelua standard libraries. 9 | * All libraries binding other C libraries provide not just the bindings, 10 | but also a wrapper to make it easier to use it with Nelua. 11 | * All libraries wraps its API into a namespace style record, that is, it will not pollute the global environment. 12 | * Some libraries may be maintained irregularly. 13 | * Some libraries are not tested on all platforms. 14 | * Some libraries are self contained when possible. 15 | * Some libraries may not work on all platforms or architectures. 16 | 17 | # List of libraries 18 | 19 | * `backtrace` - Provides a way to get trace backs at runtime, using the popular [libbacktrace](https://github.com/ianlancetaylor/libbacktrace) library. 20 | * `ffi` - Cross platform FFI for Nelua, you can use it to load symbols from shared libraries, originally created by [Rabia](https://github.com/Rabios). 21 | * `fs` - Cross platform file system library, you can use manage files and directories. 22 | * `json` - Parse JSON into Nelua records, made in pure Nelua. 23 | * `nester` - Minimal unit testing framework inspired by [lester](https://github.com/edubart/lester). 24 | * `inspector` - Function module which receives any value and returns the contents as a string, inspired by [inspect.lua](https://github.com/kikito/inspect.lua/) 25 | * `bigint` - Arbitrary-precision arithmetic integer module based on GMP. 26 | * `datetime` - Date time utilities to convert ISO 8601 human readable time format to Unix timestamps. 27 | * `signal` - Utilities to catch and raise OS application signals. 28 | * `sqlite3` - A wrapper for [SQLite](https://www.sqlite.org/index.html), to execute SQL queries on its databases. 29 | * `heapqueue` - Implementation of priority queue using binary trees. 30 | * `algorithm` - Provides algorithms to work on containers, like `sort` and `removeif`. 31 | * `tuple` - Generic that implements tuples. 32 | 33 | # Examples 34 | 35 | Check the `examples` folder for checking for some libraries usage examples, 36 | also the `tests` folder to check example of its APIs. 37 | 38 | # License 39 | 40 | MIT, see LICENSE file. Some libraries have additional authors, and they are 41 | credited in the end of the library file. 42 | -------------------------------------------------------------------------------- /algorithm.nelua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Algorithm module contains various algorithms to be used in containers, 3 | such as sort and stable sort. 4 | ]] 5 | 6 | require 'allocators.general' 7 | 8 | global algorithm: type = @record{} 9 | 10 | -- Concept used to pass contiguous containers by reference. 11 | local as_span_concept: type = #[concept(function(x) 12 | local container_type = x.type:implicit_deref_type() 13 | if not container_type.is_contiguous or not container_type.subtype then 14 | return false, string.format("type '%s' cannot be a reference to a contiguous container", x.type) 15 | end 16 | return span.value(container_type.subtype) 17 | end)]# 18 | 19 | -- Auxiliary macro to compare values being sorted. 20 | ## local function sort_lt(comp, a, b) 21 | ## if comp.type.is_niltype then 22 | in #[a]# < #[b]# 23 | ## elseif comp.type.is_function then 24 | in comp(#[a]#, #[b]#) 25 | ## else 26 | ## static_error('`comp` is not comparison function') 27 | ## end 28 | ## end 29 | 30 | --[[ 31 | Sorts contiguous container `a` elements into ascending order. 32 | 33 | If `comp` is given, then it must be a function that receives two list elements 34 | and returns true when the first element must come before the second in the final order, 35 | so that, after the sort, `i <= j` implies `not comp(a[j],a[i])`. 36 | If `comp` is not given, then the standard operator `<` is used instead. 37 | 38 | The sort algorithm is stable, that is, 39 | different elements considered equal by the given order will preserve their relative positions. 40 | 41 | The sort algorithm may not be in-place, that is, 42 | it may allocates an auxiliary buffer temporarily to perform the sort operation, 43 | use `heapsort` if you wish to avoid that. 44 | ]] 45 | function algorithm.sort(a: as_span_concept, comp: auto): void 46 | -- Tim Sort 47 | local T: type = #[a.type.subtype]# 48 | local n: usize = #a 49 | if n <= 1 then return end 50 | local width: usize = 0 51 | do -- calculate width (minrun) 52 | local m: usize = n 53 | while m >= 64 do 54 | width = width | (m & 1) 55 | m = m >> 1 56 | end 57 | width = width + m 58 | end 59 | -- Insertion Sort 60 | local high: usize = n-1 61 | for left: usize=0,<=high,width do 62 | local right: usize = left+width-1 63 | if high < right then right = high end 64 | for i: usize = left + 1,right do 65 | local x: T = a[i] 66 | local j: usize = i 67 | while j >= left+1 and #[sort_lt]#(comp, x, a[j-1]) do 68 | a[j] = a[j-1] 69 | j = j - 1 70 | end 71 | a[j] = x 72 | end 73 | end 74 | -- Merge Sort 75 | if width > high then return end 76 | local temp: span(T) = general_allocator:spanalloc(@T, n) 77 | while width <= high do 78 | local i: usize = 0 79 | while i < high do 80 | local left_end: usize = i + width - 1 81 | local right: usize = left_end + 1 82 | if right > high then break end 83 | do -- merge 84 | local left_start: usize = i 85 | -- advance left start to do less left copy (optimization) 86 | while left_start <= left_end and not #[sort_lt]#(comp, a[right], a[left_start]) do 87 | left_start = left_start + 1 88 | end 89 | if left_start <= left_end then -- perform merge only if unsorted (optimization) 90 | temp[left_start] = a[right] 91 | right = right + 1 92 | local left: usize = left_start 93 | local index: usize = left_start + 1 94 | local right_end: usize = left_end + width 95 | if right_end > high then right_end = high end 96 | while left <= left_end and right <= right_end do 97 | if #[sort_lt]#(comp, a[right], a[left]) then 98 | temp[index] = a[right] 99 | right = right + 1 100 | else 101 | temp[index] = a[left] 102 | left = left + 1 103 | end 104 | index = index + 1 105 | end 106 | while left <= left_end do 107 | temp[index] = a[left] 108 | left = left + 1 109 | index = index + 1 110 | end 111 | -- note that we have skip unnecessary right copy (optimization) 112 | for j: usize=left_start, 0 then 141 | i = i - 1 142 | t = a[i] 143 | else 144 | n = n -1 145 | if n <= 0 then return end 146 | t = a[n] 147 | a[n] = a[0] 148 | end 149 | parent = i 150 | child = i * 2 + 1 151 | while child < n do 152 | if child + 1 < n and #[sort_lt]#(comp, a[child], a[child + 1]) then 153 | child = child + 1 154 | end 155 | if #[sort_lt]#(comp, t, a[child]) then 156 | a[parent] = a[child] 157 | parent = child 158 | child = parent * 2 + 1 159 | else 160 | break 161 | end 162 | end 163 | a[parent] = t 164 | end 165 | end 166 | 167 | --[[ 168 | Removes all elements satisfying criteria `pred` from contiguous container `a`, 169 | and returns the new size of the container after removing the elements. 170 | 171 | Removing is done by shifting the elements in such a way that the elements 172 | that are not to be removed appear in the beginning of the container. 173 | Relative order of the elements that remain is preserved 174 | and the physical size of the container is unchanged. 175 | ]] 176 | function algorithm.removeif(a: as_span_concept, pred: auto, param: auto): usize 177 | local j: usize = 0 178 | local n: usize = (@usize)(#a) 179 | for i:usize=0,<(@usize)(#a) do 180 | ## if param.type.is_niltype then 181 | local remove = pred(a[i]) 182 | ## else 183 | local remove = pred(a[i], param) 184 | ## end 185 | if not remove then 186 | a[j], a[i] = a[i], a[j] 187 | j = j + 1 188 | end 189 | end 190 | return j 191 | end 192 | 193 | --[[ 194 | Unpacks and returns the first `n` elements from contiguous container `a`. 195 | ]] 196 | function algorithm.unpackn(a: as_span_concept, n: usize ) 197 | ##[[ 198 | local rets = {} 199 | for i=0,n.value-1 do 200 | table.insert(rets, aster.KeyIndex{aster.Number{i}, aster.Id{'a'}}) 201 | end 202 | ]] 203 | return #[aster.unpack(rets)]# 204 | end 205 | 206 | --[[ 207 | Packs all elements of `...` and returns a fixed array with them. 208 | ]] 209 | function algorithm.packn(...: varargs): auto 210 | local T: type = #[select(1, ...).attr.type]# 211 | return (@[]T){...} 212 | end 213 | 214 | return algorithm 215 | -------------------------------------------------------------------------------- /backtrace.nelua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Backtrace module. 3 | 4 | This modules provides utilities to get trace backs of the call stack 5 | at runtime, this may be used to raise runtime errors with 6 | some call context information, useful for debugging. 7 | ]] 8 | 9 | ## linklib 'backtrace' 10 | 11 | -- The backtrace class. 12 | global Backtrace = @record{} 13 | 14 | --[[ 15 | The type of the error callback argument to backtrace functions. 16 | This function, if not NULL, will be called for certain error cases. 17 | The DATA argument is passed to the function that calls this one. 18 | The MSG argument is an error message. The ERRNUM argument, if 19 | greater than 0, holds an errno value. The MSG buffer may become 20 | invalid after this function returns. 21 | 22 | As a special case, the ERRNUM argument will be passed as -1 if no 23 | debug info can be found for the executable, but the function 24 | requires debug info (e.g., backtrace_full, backtrace_pcinfo). The 25 | MSG in this case will be something along the lines of "no debug 26 | info". Similarly, ERRNUM will be passed as -1 if there is no 27 | symbol table, but the function requires a symbol table (e.g., 28 | backtrace_syminfo). This may be used as a signal that some other 29 | approach should be tried. 30 | ]] 31 | global Backtrace.ErrorCallback = @function(data: pointer, msg: cstring, errnum: cint): void 32 | 33 | --[[ 34 | Create state information for the backtrace routines. This must be 35 | called before any of the other routines, and its return value must 36 | be passed to all of the other routines. FILENAME is the path name 37 | of the executable file; if it is NULL the library will try 38 | system-specific path names. If not NULL, FILENAME must point to a 39 | permanent buffer. If THREADED is non-zero the state may be 40 | accessed by multiple threads simultaneously, and the library will 41 | use appropriate atomic operations. If THREADED is zero the state 42 | may only be accessed by one thread at a time. This returns a state 43 | pointer on success, NULL on error. If an error occurs, this will 44 | call the ERROR_CALLBACK routine. 45 | 46 | Calling this function allocates resources that cannot be freed. 47 | There is no backtrace_free_state function. The state is used to 48 | cache information that is expensive to recompute. Programs are 49 | expected to call this function at most once and to save the return 50 | value for all later calls to backtrace functions. 51 | ]] 52 | function Backtrace.create( 53 | filename: cstring, 54 | threaded: cint, 55 | error_callback: Backtrace.ErrorCallback, 56 | data: pointer): *Backtrace 57 | end 58 | 59 | --[[ 60 | The type of the callback argument to the backtrace_full function. 61 | DATA is the argument passed to backtrace_full. PC is the program 62 | counter. FILENAME is the name of the file containing PC, or NULL 63 | if not available. LINENO is the line number in FILENAME containing 64 | PC, or 0 if not available. FUNCTION is the name of the function 65 | containing PC, or NULL if not available. This should return 0 to 66 | continuing tracing. The FILENAME and FUNCTION buffers may become 67 | invalid after this function returns. 68 | ]] 69 | global Backtrace.FullCallback = @function( 70 | data: pointer, 71 | pc: usize, 72 | filename: cstring, 73 | lineno: cint, 74 | func: cstring): cint 75 | 76 | --[[ 77 | Get a full stack backtrace. SKIP is the number of frames to skip; 78 | passing 0 will start the trace with the function calling 79 | backtrace_full. DATA is passed to the callback routine. If any 80 | call to CALLBACK returns a non-zero value, the stack backtrace 81 | stops, and backtrace returns that value; this may be used to limit 82 | the number of stack frames desired. If all calls to CALLBACK 83 | return 0, backtrace returns 0. The backtrace_full function will 84 | make at least one call to either CALLBACK or ERROR_CALLBACK. This 85 | function requires debug info for the executable. 86 | ]] 87 | function Backtrace.full( 88 | state: *Backtrace, 89 | skip: cint, 90 | callback: Backtrace.FullCallback, 91 | error_callback: Backtrace.ErrorCallback, 92 | data: pointer): cint 93 | end 94 | 95 | --[[ 96 | The type of the callback argument to the backtrace_simple function. 97 | DATA is the argument passed to simple_backtrace. PC is the program 98 | counter. This should return 0 to continue tracing. 99 | ]] 100 | global Backtrace.SimpleCallback = @function(data: pointer, pc: usize): cint 101 | 102 | --[[ 103 | Get a simple backtrace. SKIP is the number of frames to skip, as 104 | in backtrace. DATA is passed to the callback routine. If any call 105 | to CALLBACK returns a non-zero value, the stack backtrace stops, 106 | and backtrace_simple returns that value. Otherwise 107 | backtrace_simple returns 0. The backtrace_simple function will 108 | make at least one call to either CALLBACK or ERROR_CALLBACK. This 109 | function does not require any debug info for the executable. 110 | ]] 111 | function Backtrace.simple( 112 | state: *Backtrace, 113 | skip: cint, 114 | callback: Backtrace.SimpleCallback, 115 | error_callback: Backtrace.ErrorCallback, 116 | data: pointer): cint 117 | end 118 | 119 | --[[ 120 | Print the current backtrace in a user readable format to a FILE. 121 | SKIP is the number of frames to skip, as in backtrace_full. Any 122 | error messages are printed to stderr. This function requires debug 123 | info for the executable. 124 | ]] 125 | function Backtrace.print( 126 | state: *Backtrace, 127 | skip: cint, 128 | file: pointer) 129 | end 130 | 131 | --[[ 132 | Given PC, a program counter in the current program, call the 133 | callback function with filename, line number, and function name 134 | information. This will normally call the callback function exactly 135 | once. However, if the PC happens to describe an inlined call, and 136 | the debugging information contains the necessary information, then 137 | this may call the callback function multiple times. This will make 138 | at least one call to either CALLBACK or ERROR_CALLBACK. This 139 | returns the first non-zero value returned by CALLBACK, or 0. 140 | ]] 141 | function Backtrace.pcinfo( 142 | state: *Backtrace, 143 | pc: usize, 144 | callback: Backtrace.FullCallback, 145 | error_callback: Backtrace.ErrorCallback, 146 | data: pointer): cint 147 | end 148 | 149 | --[[ 150 | The type of the callback argument to backtrace_syminfo. DATA and 151 | PC are the arguments passed to backtrace_syminfo. SYMNAME is the 152 | name of the symbol for the corresponding code. SYMVAL is the 153 | value and SYMSIZE is the size of the symbol. SYMNAME will be NULL 154 | if no error occurred but the symbol could not be found. 155 | ]] 156 | global Backtrace.SyminfoCallback = @function( 157 | data: pointer, 158 | pc: usize, 159 | symname: cstring, 160 | symval: usize, 161 | symsize: usize): void 162 | 163 | --[[ 164 | Given ADDR, an address or program counter in the current program, 165 | call the callback information with the symbol name and value 166 | describing the function or variable in which ADDR may be found. 167 | This will call either CALLBACK or ERROR_CALLBACK exactly once. 168 | This returns 1 on success, 0 on failure. This function requires 169 | the symbol table but does not require the debug info. Note that if 170 | the symbol table is present but ADDR could not be found in the 171 | table, CALLBACK will be called with a NULL SYMNAME argument. 172 | Returns 1 on success, 0 on error. 173 | ]] 174 | function Backtrace.syminfo( 175 | state: *Backtrace, 176 | addr: usize, 177 | callback: Backtrace.SyminfoCallback, 178 | error_callback: Backtrace.ErrorCallback, 179 | data: pointer): cint 180 | end 181 | 182 | require 'stringbuilder' 183 | require 'string' 184 | 185 | local function traceback_callback(data: pointer, pc: usize, filename: cstring, lineno: cint, func: cstring): cint 186 | local sb: *stringbuilder = (@*stringbuilder)(data) 187 | sb:writef('\n\t0x%016x', pc) 188 | sb:writef(' %s', func and func or "???"_cstring) 189 | sb:writef(' %s', filename and filename or "???"_cstring) 190 | sb:writef(':%d', lineno) 191 | return 0 192 | end 193 | 194 | -- Initialize a global backtrace instance. 195 | local function traceback_error_callback(data: pointer, msg: cstring, errnum: cint): void 196 | error((@stringview)(msg)) 197 | end 198 | 199 | -- Returns a string with a traceback of the call stack. 200 | -- The optional message string is appended at the beginning of the traceback. 201 | -- An optional level number tells at which level to start the traceback (default is 1, the function calling traceback). 202 | function Backtrace:traceback(message: facultative(stringview), level: facultative(integer)): string 203 | local sb: stringbuilder 204 | sb:prepare(4096) 205 | ## if not message.type.is_niltype then 206 | sb:writef("%s\n", message) 207 | ## end 208 | sb:write('stack traceback:') 209 | ## if level.type.is_niltype then 210 | local level: integer = 1 211 | ## end 212 | self:full(level, traceback_callback, traceback_error_callback, &sb) 213 | return sb:promote() 214 | end 215 | 216 | local function backtrace_create_error_callback(data: pointer, msg: cstring, errnum: cint): void 217 | error((@stringview)(msg)) 218 | end 219 | 220 | -- Initialize a global backtrace instance. 221 | local argv: *[0]cstring 222 | 223 | -- The global backtrace instance. 224 | global backtrace: *Backtrace = Backtrace.create(argv[0], 0, backtrace_create_error_callback, nilptr) 225 | -------------------------------------------------------------------------------- /datetime.nelua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Module with utilities to convert unix timestamps from/to ISO 8601 date time. 3 | The algorithms in this file is described in https://howardhinnant.github.io/date_algorithms.html 4 | ]] 5 | 6 | require 'string' 7 | require 'math' 8 | require 'os' 9 | 10 | local datetime = @record{} 11 | 12 | -- Returns true if `y` is a leap year in the civil calendar, else false. 13 | local function is_leap(y: int64): boolean 14 | return y % 4 == 0 and (y % 100 ~= 0 or y % 400 == 0) 15 | end 16 | 17 | --[[ 18 | Preconditions: m is in [1, 12] 19 | Returns the number of days in the month m of common year. 20 | The result is always in the range [28, 31]. 21 | ]] 22 | local function last_day_of_month_common_year(m: uint64): uint64 23 | local a: []uint8 = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} 24 | return a[m-1] 25 | end 26 | 27 | --[[ 28 | Preconditions: m is in [1, 12] 29 | Returns the number of days in the month m of leap year. 30 | The result is always in the range [29, 31]. 31 | ]] 32 | local function last_day_of_month_leap_year(m: uint64): uint64 33 | local a: []uint8 = {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} 34 | return a[m-1] 35 | end 36 | 37 | --[[ 38 | Preconditions: m is in [1, 12] 39 | Returns the number of days in the month m of year y 40 | The result is always in the range [28, 31]. 41 | ]] 42 | local function last_day_of_month(y: int64, m: uint64): uint64 43 | return (m ~= 2 or not is_leap(y)) and last_day_of_month_common_year(m) or 29 44 | end 45 | 46 | --[[ 47 | Returns number of days since civil 1970-01-01. 48 | Negative values indicate days prior to 1970-01-01. 49 | Preconditions: 50 | y-m-d represents a date in the civil (Gregorian) calendar 51 | m is in [1, 12] 52 | d is in [1, last_day_of_month(y, m)] 53 | ]] 54 | local function days_from_civil(y: int64, m: uint64, d: uint64): int64 55 | if m <= 2 then y = y -1 end 56 | local era: int64 = y // 400 57 | local yoe: uint64 = (@uint64)(y - era * 400) -- [0, 399] 58 | local doy: uint64 = (153*(m > 2 and m-3 or m+9) + 2)//5 + d-1 -- [0, 365] 59 | local doe: uint64 = yoe * 365 + yoe//4 - yoe//100 + doy -- [0, 146096] 60 | return era * 146097 + (@int64)(doe) - 719468 61 | end 62 | 63 | -- Returns year/month/day triple in civil calendar. 64 | local function civil_from_days(z: int64): (int64, int64, int64) 65 | z = z + 719468 66 | local era: int64 = z // 146097 67 | local doe: uint64 = (@uint64)(z - era * 146097) -- [0, 146096] 68 | local yoe: uint64 = (doe - doe//1460 + doe//36524 - doe//146096) // 365 -- [0, 399] 69 | local y: int64 = (@int64)(yoe) + era * 400 70 | local doy: uint64 = doe - (365*yoe + yoe//4 - yoe//100) -- [0, 365] 71 | local mp: uint64 = (5*doy + 2)//153 -- [0, 11] 72 | local d: uint64 = doy - (153*mp+2)//5 + 1 -- [1, 31] 73 | local m: uint64 = mp < 10 and mp+3 or mp-9 -- [1, 12] 74 | return y + (m <= 2 and 1 or 0), m, d 75 | end 76 | 77 | --[[ 78 | Converts an ISO 8601 date time from string `s` to an Unix timestamp in milliseconds. 79 | The input string must be in the format 'YYYY-MM-DDThh:mm:ss.SSSZ'. 80 | The ISO 8601 specification is vast and other formats are intentionally not supported. 81 | An error is raised in case of invalid inputs. 82 | ]] 83 | function datetime.iso8601_to_unixmillis(s: string): int64 84 | local ok: boolean, matches: sequence(string) = s:matchview"([+-]?%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)%.(%d+)Z" 85 | assert(ok, "malformed ISO-8601 input: format does not match 'YYYY-MM-DDThh:mm:ss.SSSZ'") 86 | local yea: int64, mon: int64, day: int64, hour: int64, min: int64, sec: int64, milli: int64 = 87 | tointeger(matches[1]), tointeger(matches[2]), tointeger(matches[3]), tointeger(matches[4]), tointeger(matches[5]), tointeger(matches[6]), tointeger(matches[7]) 88 | assert(mon >= 1 and mon <= 12, 'malformed ISO-8601 input: invalid month') 89 | assert(day >= 1 and day <= last_day_of_month(yea, mon), 'malformed ISO-8601 input: invalid hour') 90 | assert(min >= 0 and min <= 59, 'malformed ISO-8601 input: invalid minute') 91 | assert(sec >= 0 and sec <= 60, 'malformed ISO-8601 input: invalid second') 92 | assert(#matches[7] == 3 and milli >= 0 and milli <= 1000, 'malformed ISO-8601 input: invalid millisecond') 93 | return days_from_civil(yea, mon, day)*86400000 + 94 | hour*3600000 + 95 | min*60000 + 96 | sec*1000 + 97 | milli 98 | end 99 | 100 | --[[ 101 | The inverse of `datetime.iso8601_to_unixmillis`, 102 | returning a string in the format 'YYYY-MM-DDThh:mm:ss.SSSZ'. 103 | ]] 104 | function datetime.unixmillis_to_iso8601(millis: int64): string 105 | local secs: int64 = millis // 1000 106 | local yea: int64, mon: int64, day: int64 = civil_from_days(secs // 86400) 107 | local daysec: int64 = secs % 86400 108 | local hou: int64 = daysec // 3600 109 | local housec: int64 = daysec % 3600 110 | local min: int64 = housec // 60 111 | local sec: int64 = housec % 60 112 | local milli: int64 = millis % 1000 113 | return string.format("%04d-%02d-%02dT%02d:%02d:%02d.%03dZ", yea, mon, day, hou, min, sec, milli) 114 | end 115 | 116 | -- Returns the current Unix time in milliseconds. 117 | function datetime.unixmillis(): int64 118 | local secs: integer, nsecs: integer = os.realtime() 119 | assert(secs >= 0, 'failed to get current time') 120 | return (@int64)(secs) * 1000 + (@int64)(nsecs) // 1000000 121 | end 122 | 123 | -- Returns the current Unix time in ISO 8601. 124 | function datetime.iso8601(): string 125 | return datetime.unixmillis_to_iso8601(datetime.unixmillis()) 126 | end 127 | 128 | 129 | return datetime 130 | -------------------------------------------------------------------------------- /docs/backtrace.md: -------------------------------------------------------------------------------- 1 | ## Back trace module 2 | 3 | Backtrace module. 4 | 5 | This modules provides utilities to get trace backs of the call stack 6 | at runtime, this may be used to raise runtime errors with 7 | some call context information, useful for debugging. 8 | 9 | ### Backtrace 10 | 11 | ```nelua 12 | global Backtrace = @record{} 13 | ``` 14 | 15 | The backtrace class. 16 | 17 | ### Backtrace.ErrorCallback 18 | 19 | ```nelua 20 | global Backtrace.ErrorCallback = @function(data: pointer, msg: cstring, errnum: cint): void 21 | ``` 22 | 23 | The type of the error callback argument to backtrace functions. 24 | This function, if not NULL, will be called for certain error cases. 25 | The DATA argument is passed to the function that calls this one. 26 | The MSG argument is an error message. The ERRNUM argument, if 27 | greater than 0, holds an errno value. The MSG buffer may become 28 | invalid after this function returns. 29 | 30 | As a special case, the ERRNUM argument will be passed as -1 if no 31 | debug info can be found for the executable, but the function 32 | requires debug info (e.g., backtrace_full, backtrace_pcinfo). The 33 | MSG in this case will be something along the lines of "no debug 34 | info". Similarly, ERRNUM will be passed as -1 if there is no 35 | symbol table, but the function requires a symbol table (e.g., 36 | backtrace_syminfo). This may be used as a signal that some other 37 | approach should be tried. 38 | 39 | ### Backtrace.create 40 | 41 | ```nelua 42 | function Backtrace.create( 43 | filename: cstring, 44 | threaded: cint, 45 | error_callback: Backtrace.ErrorCallback, 46 | data: pointer): *Backtrace 47 | ``` 48 | 49 | Create state information for the backtrace routines. This must be 50 | called before any of the other routines, and its return value must 51 | be passed to all of the other routines. FILENAME is the path name 52 | of the executable file; if it is NULL the library will try 53 | system-specific path names. If not NULL, FILENAME must point to a 54 | permanent buffer. If THREADED is non-zero the state may be 55 | accessed by multiple threads simultaneously, and the library will 56 | use appropriate atomic operations. If THREADED is zero the state 57 | may only be accessed by one thread at a time. This returns a state 58 | pointer on success, NULL on error. If an error occurs, this will 59 | call the ERROR_CALLBACK routine. 60 | 61 | Calling this function allocates resources that cannot be freed. 62 | There is no backtrace_free_state function. The state is used to 63 | cache information that is expensive to recompute. Programs are 64 | expected to call this function at most once and to save the return 65 | value for all later calls to backtrace functions. 66 | 67 | ### Backtrace.FullCallback 68 | 69 | ```nelua 70 | global Backtrace.FullCallback = @function( 71 | data: pointer, 72 | pc: usize, 73 | filename: cstring, 74 | lineno: cint, 75 | func: cstring): cint 76 | ``` 77 | 78 | The type of the callback argument to the backtrace_full function. 79 | DATA is the argument passed to backtrace_full. PC is the program 80 | counter. FILENAME is the name of the file containing PC, or NULL 81 | if not available. LINENO is the line number in FILENAME containing 82 | PC, or 0 if not available. FUNCTION is the name of the function 83 | containing PC, or NULL if not available. This should return 0 to 84 | continuing tracing. The FILENAME and FUNCTION buffers may become 85 | invalid after this function returns. 86 | 87 | ### Backtrace.full 88 | 89 | ```nelua 90 | function Backtrace.full( 91 | state: *Backtrace, 92 | skip: cint, 93 | callback: Backtrace.FullCallback, 94 | error_callback: Backtrace.ErrorCallback, 95 | data: pointer): cint 96 | ``` 97 | 98 | Get a full stack backtrace. SKIP is the number of frames to skip; 99 | passing 0 will start the trace with the function calling 100 | backtrace_full. DATA is passed to the callback routine. If any 101 | call to CALLBACK returns a non-zero value, the stack backtrace 102 | stops, and backtrace returns that value; this may be used to limit 103 | the number of stack frames desired. If all calls to CALLBACK 104 | return 0, backtrace returns 0. The backtrace_full function will 105 | make at least one call to either CALLBACK or ERROR_CALLBACK. This 106 | function requires debug info for the executable. 107 | 108 | ### Backtrace.SimpleCallback 109 | 110 | ```nelua 111 | global Backtrace.SimpleCallback = @function(data: pointer, pc: usize): cint 112 | ``` 113 | 114 | The type of the callback argument to the backtrace_simple function. 115 | DATA is the argument passed to simple_backtrace. PC is the program 116 | counter. This should return 0 to continue tracing. 117 | 118 | ### Backtrace.simple 119 | 120 | ```nelua 121 | function Backtrace.simple( 122 | state: *Backtrace, 123 | skip: cint, 124 | callback: Backtrace.SimpleCallback, 125 | error_callback: Backtrace.ErrorCallback, 126 | data: pointer): cint 127 | ``` 128 | 129 | Get a simple backtrace. SKIP is the number of frames to skip, as 130 | in backtrace. DATA is passed to the callback routine. If any call 131 | to CALLBACK returns a non-zero value, the stack backtrace stops, 132 | and backtrace_simple returns that value. Otherwise 133 | backtrace_simple returns 0. The backtrace_simple function will 134 | make at least one call to either CALLBACK or ERROR_CALLBACK. This 135 | function does not require any debug info for the executable. 136 | 137 | ### Backtrace.print 138 | 139 | ```nelua 140 | function Backtrace.print( 141 | state: *Backtrace, 142 | skip: cint, 143 | file: pointer) 144 | ``` 145 | 146 | Print the current backtrace in a user readable format to a FILE. 147 | SKIP is the number of frames to skip, as in backtrace_full. Any 148 | error messages are printed to stderr. This function requires debug 149 | info for the executable. 150 | 151 | ### Backtrace.pcinfo 152 | 153 | ```nelua 154 | function Backtrace.pcinfo( 155 | state: *Backtrace, 156 | pc: usize, 157 | callback: Backtrace.FullCallback, 158 | error_callback: Backtrace.ErrorCallback, 159 | data: pointer): cint 160 | ``` 161 | 162 | Given PC, a program counter in the current program, call the 163 | callback function with filename, line number, and function name 164 | information. This will normally call the callback function exactly 165 | once. However, if the PC happens to describe an inlined call, and 166 | the debugging information contains the necessary information, then 167 | this may call the callback function multiple times. This will make 168 | at least one call to either CALLBACK or ERROR_CALLBACK. This 169 | returns the first non-zero value returned by CALLBACK, or 0. 170 | 171 | ### Backtrace.SyminfoCallback 172 | 173 | ```nelua 174 | global Backtrace.SyminfoCallback = @function( 175 | data: pointer, 176 | pc: usize, 177 | symname: cstring, 178 | symval: usize, 179 | symsize: usize): void 180 | ``` 181 | 182 | The type of the callback argument to backtrace_syminfo. DATA and 183 | PC are the arguments passed to backtrace_syminfo. SYMNAME is the 184 | name of the symbol for the corresponding code. SYMVAL is the 185 | value and SYMSIZE is the size of the symbol. SYMNAME will be NULL 186 | if no error occurred but the symbol could not be found. 187 | 188 | ### Backtrace.syminfo 189 | 190 | ```nelua 191 | function Backtrace.syminfo( 192 | state: *Backtrace, 193 | addr: usize, 194 | callback: Backtrace.SyminfoCallback, 195 | error_callback: Backtrace.ErrorCallback, 196 | data: pointer): cint 197 | ``` 198 | 199 | Given ADDR, an address or program counter in the current program, 200 | call the callback information with the symbol name and value 201 | describing the function or variable in which ADDR may be found. 202 | This will call either CALLBACK or ERROR_CALLBACK exactly once. 203 | This returns 1 on success, 0 on failure. This function requires 204 | the symbol table but does not require the debug info. Note that if 205 | the symbol table is present but ADDR could not be found in the 206 | table, CALLBACK will be called with a NULL SYMNAME argument. 207 | Returns 1 on success, 0 on error. 208 | 209 | ### Backtrace:traceback 210 | 211 | ```nelua 212 | function Backtrace:traceback(message: facultative(stringview), level: facultative(integer)): string 213 | ``` 214 | 215 | Returns a string with a traceback of the call stack. 216 | The optional message string is appended at the beginning of the traceback. 217 | An optional level number tells at which level to start the traceback (default is 1, the function calling traceback). 218 | 219 | ### backtrace 220 | 221 | ```nelua 222 | global backtrace: *Backtrace 223 | ``` 224 | 225 | The global backtrace instance. 226 | 227 | --- 228 | -------------------------------------------------------------------------------- /docs/ffi.md: -------------------------------------------------------------------------------- 1 | ## FFI module 2 | 3 | Cross platform foreign function interface (FFI) module. 4 | This library can be used to load/unload shared libraries and symbols from it. 5 | 6 | ### ffi 7 | 8 | ```nelua 9 | global ffi = @record{ 10 | handle: pointer, 11 | } 12 | ``` 13 | 14 | FFI record, containing a handle for the library. 15 | 16 | ### ffi.load 17 | 18 | ```nelua 19 | function ffi.load(file: cstring): ffi 20 | ``` 21 | 22 | Loads shared library from filename `file` and returns an FFI handle. 23 | In case the load failed, then `isloaded` will return false. 24 | 25 | ### ffi:isloaded 26 | 27 | ```nelua 28 | function ffi:isloaded(): boolean 29 | ``` 30 | 31 | Checks weather the shared library is successfully loaded. 32 | 33 | ### ffi:get 34 | 35 | ```nelua 36 | function ffi:get(name: cstring): pointer 37 | ``` 38 | 39 | Retrieve symbol `name` from the shared library, returning its pointer. 40 | In case it does not exist, returns nilptr. 41 | 42 | ### ffi:unload 43 | 44 | ```nelua 45 | function ffi:unload(): boolean 46 | ``` 47 | 48 | Unloads shared library, returns `true` when it is successfully unloaded. 49 | 50 | --- 51 | -------------------------------------------------------------------------------- /docs/fs.md: -------------------------------------------------------------------------------- 1 | ## File system module 2 | 3 | File system module. 4 | 5 | Contains various utilities to manages files, directories and links. 6 | 7 | Most functions from this library returns 3 values: 8 | * First is the operation result, which may be false or empty in case the operation failed. 9 | * Second is an error message, in case the operation failed. 10 | * Third is a system dependent error code, which is zero when the operation succeeds. 11 | 12 | ### fs 13 | 14 | ```nelua 15 | global fs = @record{} 16 | ``` 17 | 18 | The file system namespace. 19 | 20 | ### fs.sep 21 | 22 | ```nelua 23 | global fs.sep: string 24 | ``` 25 | 26 | Character separator for directories. 27 | 28 | ### fs.StatKind 29 | 30 | ```nelua 31 | global fs.StatKind = @enum(uint32){ 32 | INVALID = 0, 33 | FILE, 34 | DIRECTORY, 35 | LINK, 36 | SOCKET, 37 | PIPE, 38 | CHARACTER, 39 | BLOCK, 40 | OTHER, 41 | } 42 | ``` 43 | 44 | File status kind. 45 | 46 | ### fs.StatInfo 47 | 48 | ```nelua 49 | global fs.StatInfo = @record{ 50 | kind: fs.StatKind, 51 | dev: uint64, 52 | ino: uint64, 53 | nlink: uint64, 54 | mode: uint32, 55 | uid: uint32, 56 | gid: uint32, 57 | rdev: uint64, 58 | size: int64, 59 | atime: int64, 60 | mtime: int64, 61 | ctime: int64, 62 | blksize: int64, -- not available in Windows 63 | blocks: int64, -- not available in Windows 64 | } 65 | ``` 66 | 67 | File status information. 68 | 69 | ### fs.isdir 70 | 71 | ```nelua 72 | function fs.isdir(path: string): boolean 73 | ``` 74 | 75 | Checks if a directory exists. 76 | 77 | ### fs.isfile 78 | 79 | ```nelua 80 | function fs.isfile(path: string): boolean 81 | ``` 82 | 83 | Checks if a file exists. 84 | 85 | ### fs.cwdir 86 | 87 | ```nelua 88 | function fs.cwdir(): (string, string, integer) 89 | ``` 90 | 91 | Returns the current working directory. 92 | 93 | ### fs.chdir 94 | 95 | ```nelua 96 | function fs.chdir(path: string): (boolean, string, integer) 97 | ``` 98 | 99 | Changes the current working directory. 100 | 101 | ### fs.chmod 102 | 103 | ```nelua 104 | function fs.chmod(path: string, mode: uint32): (boolean, string, integer) 105 | ``` 106 | 107 | Changes permissions of a file. 108 | 109 | ### fs.chown 110 | 111 | ```nelua 112 | function fs.chown(path: string, uid: uint32, gid: uint32): (boolean, string, integer) 113 | ``` 114 | 115 | Changes ownership of a file. 116 | 117 | ### fs.mkdir 118 | 119 | ```nelua 120 | function fs.mkdir(path: string): (boolean, string, integer) 121 | ``` 122 | 123 | Creates a directory. 124 | 125 | ### fs.mkfile 126 | 127 | ```nelua 128 | function fs.mkfile(path: string, contents: facultative(string)): (boolean, string, integer) 129 | ``` 130 | 131 | Creates a file into `path` and write `contents`. 132 | If `contents` is not present then an empty is created. 133 | If the file exists, it will be overwritten. 134 | 135 | ### fs.mklink 136 | 137 | ```nelua 138 | function fs.mklink(oldpath: string, newpath: string, hard: facultative(boolean)): (boolean, string, integer) 139 | ``` 140 | 141 | Creates a link for object `oldpath` at `newpath`. 142 | By default symbolic links are created, unless `hardlink` is true. 143 | 144 | ### fs.rmdir 145 | 146 | ```nelua 147 | function fs.rmdir(path: string): (boolean, string, integer) 148 | ``` 149 | 150 | Removes a directory. 151 | 152 | ### fs.rmfile 153 | 154 | ```nelua 155 | function fs.rmfile(path: string): (boolean, string, integer) 156 | ``` 157 | 158 | Removes a file. 159 | 160 | ### fs.move 161 | 162 | ```nelua 163 | function fs.move(oldpath: string, newpath: string): (boolean, string, integer) 164 | ``` 165 | 166 | Moves a file or directory. 167 | 168 | ### fs.touch 169 | 170 | ```nelua 171 | function fs.touch(path: string): (boolean, string, integer) 172 | ``` 173 | 174 | Updates access and modification time of a file. 175 | 176 | ### fs.stat 177 | 178 | ```nelua 179 | function fs.stat(path: string, linkstat: facultative(boolean)): (fs.StatInfo, string, integer) 180 | ``` 181 | 182 | Gets file status information. 183 | 184 | ### fs.readfile 185 | 186 | ```nelua 187 | function fs.readfile(path: string): (string, string, integer) 188 | ``` 189 | 190 | Reads all contents from a file. 191 | 192 | ### fs.readlink 193 | 194 | ```nelua 195 | function fs.readlink(path: string): (string, string, integer) 196 | ``` 197 | 198 | Reads value from a symbolic link file. 199 | 200 | ### fs.cpfile 201 | 202 | ```nelua 203 | function fs.cpfile(oldpath: string, newpath: string): (boolean, string, integer) 204 | ``` 205 | 206 | Copies file from `oldpath` to `newpath`. 207 | If a file exists in `newpath`, it will be overwritten. 208 | File permissions are preserved. 209 | 210 | --- 211 | -------------------------------------------------------------------------------- /docs/nester.md: -------------------------------------------------------------------------------- 1 | ## Nester module 2 | 3 | Nester is a minimal unit testing framework for Nelua with a focus on being simple to use. 4 | 5 | ## Features 6 | 7 | * Minimal, just one file. 8 | * Self contained, no external dependencies. 9 | * Simple and hackable when needed. 10 | * Use `describe` and `it` blocks to describe tests. 11 | * Supports `before` and `after` handlers. 12 | * Colored output. 13 | * Configurable via the script or with environment variables. 14 | * Quiet mode, to use in live development. 15 | * Optionally filter tests by name. 16 | * Show location of tests errors. 17 | * Show time to complete tests. 18 | 19 | ## Usage 20 | 21 | Copy `nester.nelua` file to a project and require it, 22 | and use it the following way: 23 | 24 | ```lua 25 | require 'nester' 26 | 27 | -- Customize nester configuration. 28 | nester.stop_on_fail = false 29 | 30 | nester.describe('my project', function() 31 | nester.before(function(name: string) 32 | -- This function is run before every test. 33 | end) 34 | 35 | nester.describe('module1', function() -- Describe blocks can be nested. 36 | nester.it('feature1', function() 37 | expect.equal('something', 'something') -- Pass. 38 | end) 39 | 40 | nester.it('feature2', function() 41 | expect.truthy(false) -- Fail. 42 | end) 43 | end) 44 | end) 45 | 46 | nester.report() -- Print overall statistic of the tests run. 47 | nester.exit() -- Exit with success if all tests passed. 48 | ``` 49 | 50 | ## Customizing output with environment variables 51 | 52 | To customize the output of nester externally, 53 | you can set the following environment variables before running a test suite: 54 | 55 | * `NESTER_QUIET="true"`, omit print of passed tests. 56 | * `NESTER_COLORED="false"`, disable colored output. 57 | * `NESTER_SHOW_TRACEBACK="false"`, disable traceback on test failures. 58 | * `NESTER_SHOW_ERROR="false"`, omit print of error description of failed tests. 59 | * `NESTER_STOP_ON_FAIL="true"`, stop on first test failure. 60 | * `NESTER_UTF8TERM="false"`, disable printing of UTF-8 characters. 61 | * `NESTER_FILTER="some text"`, filter the tests that should be run. 62 | 63 | Note that these configurations can be changed via script too, check the documentation. 64 | 65 | ### nester 66 | 67 | ```nelua 68 | global nester: type = @record{} 69 | ``` 70 | 71 | The nester module. 72 | 73 | ### nester.quiet 74 | 75 | ```nelua 76 | global nester.quiet: boolean 77 | ``` 78 | 79 | Whether lines of passed tests should not be printed. False by default. 80 | 81 | ### nester.colored 82 | 83 | ```nelua 84 | global nester.colored: boolean 85 | ``` 86 | 87 | Whether the output should be colorized. True by default. 88 | 89 | ### nester.show_error 90 | 91 | ```nelua 92 | global nester.show_error: boolean 93 | ``` 94 | 95 | Whether the error description of a test failure should be shown. True by default. 96 | 97 | ### nester.stop_on_fail 98 | 99 | ```nelua 100 | global nester.stop_on_fail: boolean 101 | ``` 102 | 103 | Whether test suite should exit on first test failure. False by default. 104 | 105 | ### nester.utf8term 106 | 107 | ```nelua 108 | global nester.utf8term: boolean 109 | ``` 110 | 111 | Whether we can print UTF-8 characters to the terminal. True by default when supported. 112 | 113 | ### nester.filter 114 | 115 | ```nelua 116 | global nester.filter: string 117 | ``` 118 | 119 | A string with a Lua pattern to filter tests. Empty by default. 120 | 121 | ### nester.seconds 122 | 123 | ```nelua 124 | global nester.seconds: auto 125 | ``` 126 | 127 | Function to retrieve time in seconds with milliseconds precision, `os.now` by default. 128 | 129 | ### nester.exit 130 | 131 | ```nelua 132 | function nester.exit(): void 133 | ``` 134 | 135 | Exit the application with success code if all tests passed, or failure code otherwise. 136 | 137 | ### nester.describe 138 | 139 | ```nelua 140 | function nester.describe(name: string, func: function()): void 141 | ``` 142 | 143 | Describe a block of tests, which consists in a set of tests. 144 | Describes can be nested. 145 | `name` is a string used to describe the block. 146 | `func` a function containing all the tests or other describes. 147 | 148 | ### nester.it 149 | 150 | ```nelua 151 | function nester.it(name: string, func: function()): void 152 | ``` 153 | 154 | Declare a test, which consists of a set of assertions. 155 | Where `name` is the test name, 156 | and `func` is the function containing all assertions. 157 | 158 | ### nester.before 159 | 160 | ```nelua 161 | function nester.before(func: function(string)): void 162 | ``` 163 | 164 | Set a function that is called before every test inside a describe block. 165 | A single string containing the name of the test about to be run will be passed to `func`. 166 | 167 | ### nester.after 168 | 169 | ```nelua 170 | function nester.after(func: function(string)): void 171 | ``` 172 | 173 | Set a function that is called after every test inside a describe block. 174 | A single string containing the name of the test that was finished will be passed to `func`. 175 | The function is executed independently if the test passed or failed. 176 | 177 | ### nester.report 178 | 179 | ```nelua 180 | function nester.report(): boolean 181 | ``` 182 | 183 | Pretty print statistics of all test runs. 184 | With total success, total failures and run time in seconds. 185 | 186 | ### expect 187 | 188 | ```nelua 189 | global expect: type = @record{} 190 | ``` 191 | 192 | Expect module, containing utility function for doing assertions inside a test. 193 | 194 | ### expect.equal 195 | 196 | ```nelua 197 | function expect.equal(a: auto, b: auto): void 198 | ``` 199 | 200 | Checks if `a` is equals to `b`, if not raises a test error. 201 | 202 | ### expect.not_equal 203 | 204 | ```nelua 205 | function expect.not_equal(a: auto, b: auto): void 206 | ``` 207 | 208 | Checks if `a` is different from `b`, if not raises a test error. 209 | 210 | ### expect.truthy 211 | 212 | ```nelua 213 | function expect.truthy(v: boolean): void 214 | ``` 215 | 216 | Checks if `v` is true, if not raises a test error. 217 | 218 | ### expect.falsy 219 | 220 | ```nelua 221 | function expect.falsy(v: boolean): void 222 | ``` 223 | 224 | Checks if `v` is false, if not raises a test error. 225 | 226 | ### expect.error 227 | 228 | ```nelua 229 | function expect.error(msg: facultative(string)): void 230 | ``` 231 | 232 | Raises test error message `msg`. 233 | 234 | ### expect.assert 235 | 236 | ```nelua 237 | function expect.assert(cond: boolean, msg: string): void 238 | ``` 239 | 240 | Raises test error message `msg` if `cond` is false. 241 | 242 | --- 243 | -------------------------------------------------------------------------------- /examples/bigint_pi_digits.nelua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Example of calculating 1000 digits of PI using BigInt. 3 | 4 | I uses the Machin tecnhique for computing PI digits, 5 | See https://en.wikipedia.org/wiki/Machin-like_formula for details. 6 | ]] 7 | 8 | -- Un comment the following if you have GMP installed in your system to increase speed. 9 | -- ## BIGINT_USE_GMP = true 10 | 11 | local BigInt = require 'bigint' 12 | 13 | local function arctan_denom(x: integer, ndigits: integer): BigInt 14 | local one = BigInt.pow(10, ndigits) 15 | local d = BigInt.from(x) 16 | local d2 = -(d*d) 17 | local i = 0 18 | local sum = BigInt.from(0) 19 | repeat 20 | local term = one // (d * (2*i + 1)) 21 | sum = sum + term 22 | d = d * d2 23 | i = i + 1 24 | until term == 0 25 | return sum 26 | end 27 | 28 | local function compute_pi(ndigits: integer): string 29 | local ndigits2 = ndigits + 10 30 | local pi = 4*(4*arctan_denom(5, ndigits2) - arctan_denom(239, ndigits2)) 31 | local pistr = tostring(pi) 32 | return pistr:sub(1,1)..'.'..pistr:sub(2, ndigits + 1) 33 | end 34 | 35 | local pi = compute_pi(1000) 36 | print('The first 1000 pi digits are:') 37 | print(pi) 38 | assert(pi == '3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679821480865132823066470938446095505822317253594081284811174502841027019385211055596446229489549303819644288109756659334461284756482337867831652712019091456485669234603486104543266482133936072602491412737245870066063155881748815209209628292540917153643678925903600113305305488204665213841469519415116094330572703657595919530921861173819326117931051185480744623799627495673518857527248912279381830119491298336733624406566430860213949463952247371907021798609437027705392171762931767523846748184676694051320005681271452635608277857713427577896091736371787214684409012249534301465495853710507922796892589235420199561121290219608640344181598136297747713099605187072113499999983729780499510597317328160963185950244594553469083026425223082533446850352619311881710100031378387528865875332083814206171776691473035982534904287554687311595628638823537875937519577818577805321712268066130019278766111959092164201989') 39 | -------------------------------------------------------------------------------- /examples/bigint_rsa.nelua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Minimal RSA cryptosystem using example BigInt. 3 | See https://en.wikipedia.org/wiki/RSA_(cryptosystem) for details. 4 | ]] 5 | 6 | local BigInt = require 'bigint' 7 | 8 | -- 1. Choose two distinct primes 9 | local p = BigInt.from('5011956100955230700919988831') 10 | local q = BigInt.from('5989833698158308593254005067') 11 | print('p = ' .. tostring(p)) 12 | print('q = ' .. tostring(q)) 13 | 14 | -- 2. Compute n = p * q 15 | local n = p * q 16 | print('n = ' .. tostring(n)) 17 | assert(n == BigInt.from('30020783547191766561527759475184666413598963507657406677')) 18 | 19 | -- 3. Compute the totient 20 | local phi_n = BigInt.lcm(p - 1, q - 1) 21 | print('phi_n = ' .. tostring(phi_n)) 22 | assert(phi_n == BigInt.from('5003463924531961093587959910697146102414237368913902130')) 23 | 24 | -- 4. Choose any number e that 1 < e < phi_n and is coprime to phi_n 25 | local e = BigInt.from('65537') 26 | print('e = ' .. tostring(e)) 27 | 28 | -- 5. Compute d, the modular multiplicative inverse 29 | local d = BigInt.invmod(e, phi_n) 30 | print('d = ' .. tostring(d)) 31 | assert(d == BigInt.from('2768292749187922993934715143535384861582621221551460873')) 32 | 33 | -- The public key is (n, e), implement the encrypt function 34 | local function encrypt(msg: BigInt): BigInt 35 | return BigInt.powmod(msg, e, n) 36 | end 37 | 38 | -- The private key is (n, d), implement the decrypt function 39 | local function decrypt(msg: BigInt): BigInt 40 | return BigInt.powmod(msg, d, n) 41 | end 42 | 43 | -- Test encrypt and decrypt 44 | print('-- Message encryption test --') 45 | local msg = 'Hello world!' 46 | print('Message: '..msg) 47 | local x = BigInt.frombytes(msg) 48 | assert(x < n) 49 | print('x = 0x' .. x:tostring(16) .. ' (original message)') 50 | local c = encrypt(x) 51 | print('c = 0x' .. c:tostring(16) .. ' (encrypted message)') 52 | assert(c == BigInt.from('0x81fa941a0bf7a387f0ad060b90a5cd251be4031b4df39a')) 53 | local m = decrypt(c) 54 | print('m = 0x' .. m:tostring(16) .. ' (decrypted message)') 55 | assert(m == x) 56 | local decoded = BigInt.tobytes(m) 57 | print('Decrypted: '..decoded) 58 | assert(decoded == msg) 59 | -------------------------------------------------------------------------------- /examples/json_example.nelua: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'vector' 3 | require 'hashmap' 4 | 5 | local Person = @record{ 6 | name: string, 7 | age: integer, 8 | female: boolean, 9 | children: vector(Person), 10 | wallet: hashmap(string, integer), 11 | } 12 | 13 | -- Parsing 14 | local john_json = [[ 15 | { 16 | "name": "John", 17 | "age": 30, 18 | "female": false, 19 | "children": [ 20 | { 21 | "name": "Ana", 22 | "age": 1, 23 | "female": true 24 | }, 25 | { 26 | "name": "Paul", 27 | "age": 3, 28 | "female": false 29 | } 30 | ], 31 | "wallet": { 32 | "USD": 400, 33 | "EUR": 30 34 | } 35 | } 36 | ]] 37 | 38 | local john: Person = json.parse(john_json, @Person) 39 | print('== Parsing') 40 | print('name:', john.name) 41 | print('age:', john.age) 42 | print('female:', john.female) 43 | print('children:') 44 | for k,child in ipairs(john.children) do 45 | print(child.name, child.age) 46 | end 47 | print('wallet:') 48 | for currency,amount in pairs(john.wallet) do 49 | print(currency, amount) 50 | end 51 | 52 | -- Emitting 53 | local json: string = json.emit(john) 54 | print('== Emitting') 55 | print(json) 56 | -------------------------------------------------------------------------------- /examples/tuple_example.nelua: -------------------------------------------------------------------------------- 1 | require 'tuple' 2 | 3 | local pair = tuple.pack(true, 10) 4 | print(pair[0], pair[1]) 5 | 6 | local first_value, second_value = tuple.unpack(pair) 7 | print(first_value, second_value) 8 | -------------------------------------------------------------------------------- /ffi.nelua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Cross platform foreign function interface (FFI) module. 3 | This library can be used to load/unload shared libraries and symbols from it. 4 | ]] 5 | 6 | ##[[ 7 | if ccinfo.is_unix then 8 | cinclude '' 9 | if ccinfo.is_linux then -- we only need to link 'dl' on linux 10 | linklib 'dl' 11 | end 12 | elseif ccinfo.is_windows then 13 | cinclude '' 14 | linklib 'kernel32' 15 | elseif ccinfo.is_haiku or ccinfo.is_beos then 16 | cinclude '' 17 | else 18 | static_error 'ffi module is not unsupported in your platform' 19 | end 20 | ]] 21 | 22 | ## if ccinfo.is_unix then 23 | local RTLD_NOW: cint 24 | local RTLD_GLOBAL: cint 25 | local function dlopen(file: cstring, mode: cint): pointer end 26 | local function dlsym(handle: pointer, name: cstring): pointer end 27 | local function dlclose(handle: pointer): cint end 28 | ## elseif ccinfo.is_windows then 29 | local function LoadLibraryA(file: cstring): pointer end 30 | local function GetProcAddress(handle: pointer, name: cstring): pointer end 31 | local function FreeLibrary(handle: pointer): cint end 32 | ## elseif ccinfo.is_haiku or ccinfo.is_beos then 33 | local B_SYMBOL_TYPE_ANY: cint 34 | local B_OK: cint 35 | local function load_add_on(file: cstring): cint end 36 | local function get_image_symbol(image: cint, name: cstring, mode: cint, symbol_handle: pointer): cint end 37 | local function unload_add_on(image: cint): cint end 38 | ## end 39 | 40 | -- FFI record, containing a handle for the library. 41 | global ffi = @record{ 42 | handle: pointer, 43 | } 44 | 45 | --[[ 46 | Loads shared library from filename `file` and returns an FFI handle. 47 | In case the load failed, then `isloaded` will return false. 48 | ]] 49 | function ffi.load(file: cstring): ffi 50 | ## if ccinfo.is_unix then 51 | return ffi{handle=dlopen(file, RTLD_NOW | RTLD_GLOBAL)} 52 | ## elseif ccinfo.is_windows then 53 | return ffi{handle=LoadLibraryA(file)} 54 | ## elseif ccinfo.is_haiku or ccinfo.is_beos then 55 | local image: isize = load_add_on(file) 56 | if image <= 0 then return ffi{handle=nilptr} end 57 | return ffi{handle=(@pointer)(image)} 58 | ## end 59 | end 60 | 61 | -- Checks weather the shared library is successfully loaded. 62 | function ffi:isloaded(): boolean 63 | return self.handle ~= nilptr 64 | end 65 | 66 | --[[ 67 | Retrieve symbol `name` from the shared library, returning its pointer. 68 | In case it does not exist, returns nilptr. 69 | ]] 70 | function ffi:get(name: cstring): pointer 71 | if self.handle == nilptr then return nilptr end 72 | ## if ccinfo.is_unix then 73 | return dlsym(self.handle, name) 74 | ## elseif ccinfo.is_windows then 75 | return GetProcAddress(self.handle, name) 76 | ## elseif ccinfo.is_haiku or ccinfo.is_beos then 77 | local sym: pointer 78 | return get_image_symbol((@isize)(self.handle), name, B_SYMBOL_TYPE_ANY, &sym) == B_OK and sym or nilptr 79 | ## end 80 | end 81 | 82 | -- Unloads shared library, returns `true` when it is successfully unloaded. 83 | function ffi:unload(): boolean 84 | if self.handle == nilptr then return false end 85 | ## if ccinfo.is_unix then 86 | local ok: boolean = dlclose(self.handle) == 0 87 | ## elseif ccinfo.is_windows then 88 | local ok: boolean = FreeLibrary(self.handle) ~= 0 89 | ## elseif ccinfo.is_haiku or ccinfo.is_beos then 90 | local ok: boolean = unload_add_on((@isize)(self.handle)) == 0 91 | ## end 92 | if ok then self.handle = nilptr end 93 | return ok 94 | end 95 | 96 | return ffi 97 | 98 | --[[ 99 | MIT License 100 | 101 | Copyright (c) 2021 Rabia Alhaffar (https://github.com/Rabios) 102 | Copyright (c) 2021 Eduardo Bart (https://github.com/edubart) 103 | 104 | Permission is hereby granted, free of charge, to any person obtaining a copy 105 | of this software and associated documentation files (the "Software"), to deal 106 | in the Software without restriction, including without limitation the rights 107 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 108 | copies of the Software, and to permit persons to whom the Software is 109 | furnished to do so, subject to the following conditions: 110 | 111 | The above copyright notice and this permission notice shall be included in all 112 | copies or substantial portions of the Software. 113 | 114 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 115 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 116 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 117 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 118 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 119 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 120 | SOFTWARE. 121 | ]] 122 | -------------------------------------------------------------------------------- /fs.nelua: -------------------------------------------------------------------------------- 1 | --[[ 2 | File system module. 3 | 4 | Contains various utilities to manages files, directories and links. 5 | 6 | Most functions from this library returns 3 values: 7 | * First is the operation result, which may be false or empty in case the operation failed. 8 | * Second is an error message, in case the operation failed. 9 | * Third is a system dependent error code, which is zero when the operation succeeds. 10 | ]] 11 | 12 | require 'string' 13 | 14 | ---------------------------------------------------------------------------------------------------- 15 | 16 | ## if ccinfo.is_windows then -- Import Windows APIs 17 | -- fcntl.h 18 | local O_BINARY: cint ',const> 19 | local O_RDONLY: cint ',const> 20 | local O_WRONLY: cint ',const> 21 | local O_CREAT: cint ',const> 22 | local O_TRUNC: cint ',const> 23 | -- io.h 24 | local function open(file: cstring, oflag: cint, mode: cuint): cint '> end 25 | local function close(fd: cint): cint '> end 26 | local function read(fd: cint, buf: pointer, n: cuint): cint '> end 27 | local function write(fd: cint, buf: pointer, n: cuint): cint '> end 28 | local function unlink(name: cstring): cint '> end 29 | -- direct.h 30 | local function chdir(path: cstring): cint '> end 31 | local function getcwd(buf: cstring, size: csize): cstring '> end 32 | local function mkdir(path: cstring): cint '> end 33 | local function rmdir(path: cstring): cint '> end 34 | -- sys/stat.h 35 | local stat_t: type ',ctypedef'_stat64'> = @record{ 36 | st_dev: cint, 37 | st_ino: cshort, 38 | st_mode: cushort, 39 | st_nlink: culong, 40 | st_uid: cshort, 41 | st_gid: cshort, 42 | st_rdev: cint, 43 | st_size: int64, 44 | st_atime: int64, 45 | st_mtime: int64, 46 | st_ctime: int64, 47 | } 48 | local _S_IREAD: cuint ',const> 49 | local _S_IWRITE: cuint ',const> 50 | local _S_IFREG: cuint ',const> 51 | local _S_IFDIR: cuint ',const> 52 | local _S_IFCHR: cuint ',const> 53 | local _S_IFLNK: cuint = 0x400 54 | local function S_ISREG(mode: cuint): boolean return mode & _S_IFREG ~= 0 end 55 | local function S_ISDIR(mode: cuint): boolean return mode & _S_IFDIR ~= 0 end 56 | local function S_ISCHR(mode: cuint): boolean return mode & _S_IFCHR ~= 0 end 57 | local function S_ISLNK(mode: cuint): boolean return mode & _S_IFLNK ~= 0 end 58 | local function S_ISFIFO(mode: cuint): boolean return false end 59 | local function S_ISSOCK(mode: cuint): boolean return false end 60 | local function S_ISBLK(mode: cuint): boolean return false end 61 | local function stat(file: cstring, buf: *stat_t): cint '> end 62 | local function fstat(fd: cint, buf: *stat_t): cint '> end 63 | local function chmod(file: cstring, mode: cuint): cint '> end 64 | -- sys/utime.h 65 | local utimbuf: type ',ctypedef'_utime64'> = @record{ 66 | actime: int64, 67 | modtime: int64 68 | } 69 | local function utime(file: cstring, times: *utimbuf): cint '> end 70 | -- windows.h 71 | local WIN32_FILE_ATTRIBUTE_DATA: type = @record{ 72 | dwFileAttributes: clong 73 | } 74 | local GetFileExInfoStandard: cint 75 | local GENERIC_READ: clong 76 | local FILE_SHARE_READ: clong 77 | local FILE_SHARE_WRITE: clong 78 | local OPEN_EXISTING: clong 79 | local FILE_ATTRIBUTE_NORMAL: clong 80 | local FILE_NAME_OPENED: clong 81 | local FILE_ATTRIBUTE_REPARSE_POINT: clong 82 | local INVALID_HANDLE_VALUE: pointer 83 | local function GetFileAttributesEx(lpFileName: cstring, fInfoLevelId: cint, lpFileInformation: pointer): cint end 84 | local function CreateSymbolicLinkA(to: cstring, from: cstring, flags: clong): byte end 85 | local function CreateHardLinkA(to: cstring, from: cstring, attrs: pointer): cint end 86 | local function GetLastError(): clong end 87 | local function CloseHandle(hObject: pointer): cint end 88 | local function CreateFileA( 89 | lpFileName: cstring, 90 | dwDesiredAccess: clong, 91 | dwShareMode: clong, 92 | lpSecurityAttributes: pointer, 93 | dwCreationDisposition: clong, 94 | dwFlagsAndAttributes: clong, 95 | hTemplateFile: pointer 96 | ): pointer end 97 | local function GetFinalPathNameByHandleA( 98 | hFile: pointer, 99 | lpszFilePath: cstring, 100 | cchFilePath: clong, 101 | dwFlags: clong 102 | ): clong end 103 | ## else -- Import POSIX APIs 104 | -- fcntl.h 105 | local O_BINARY: cint = 0 106 | local O_RDONLY: cint ',const> 107 | local O_WRONLY: cint ',const> 108 | local O_CREAT: cint ',const> 109 | local O_TRUNC: cint ',const> 110 | local function open(file: cstring, oflag: cint, mode: cuint): cint '> end 111 | -- unistd.h 112 | local function getcwd(buf: cstring, size: csize): cstring '> end 113 | local function chdir(path: cstring): cint '> end 114 | local function rmdir(path: cstring): cint '> end 115 | local function symlink(from: cstring, to: cstring): cint '> end 116 | local function unlink(name: cstring): cint '> end 117 | local function link(from: cstring, to: cstring): cint '> end 118 | local function readlink(path: cstring, buf: cstring, len: csize): clong '> end 119 | local function chown(file: cstring, owner: cuint, group: cuint): cint '> end 120 | local function close(fd: cint): cint '> end 121 | local function read(fd: cint, buf: pointer, n: csize): clong '> end 122 | local function write(fd: cint, buf: pointer, n: csize): clong '> end 123 | -- sys/stat.h 124 | local stat_t: type ',cincomplete,ctypedef'stat'> = @record{ 125 | st_dev: culong, 126 | st_ino: culong, 127 | st_nlink: culong, 128 | st_mode: cuint, 129 | st_uid: cuint, 130 | st_gid: cuint, 131 | st_rdev: culong, 132 | st_size: clong, 133 | st_blksize: clong, 134 | st_blocks: clong, 135 | st_atime: int64, 136 | st_mtime: int64, 137 | st_ctime: int64, 138 | } 139 | local S_IRUSR: cuint ',const> 140 | local S_IWUSR: cuint ',const> 141 | local S_IXUSR: cuint ',const> 142 | local S_IRGRP: cuint ',const> 143 | local S_IWGRP: cuint ',const> 144 | local S_IXGRP: cuint ',const> 145 | local S_IROTH: cuint ',const> 146 | local S_IWOTH: cuint ',const> 147 | local S_IXOTH: cuint ',const> 148 | local function S_ISREG(mode: cuint): boolean '> end 149 | local function S_ISDIR(mode: cuint): boolean '> end 150 | local function S_ISCHR(mode: cuint): boolean '> end 151 | local function S_ISLNK(mode: cuint): boolean '> end 152 | local function S_ISFIFO(mode: cuint): boolean '> end 153 | local function S_ISSOCK(mode: cuint): boolean '> end 154 | local function S_ISBLK(mode: cuint): boolean '> end 155 | local function stat(file: cstring, buf: *stat_t): cint '> end 156 | local function fstat(fd: cint, buf: *stat_t): cint '> end 157 | local function lstat(file: cstring, buf: *stat_t): cint '> end 158 | local function chmod(file: cstring, mode: cuint): cint '> end 159 | local function mkdir(path: cstring, mode: cuint): cint '> end 160 | -- sys/utime.h 161 | local utimbuf: type ',ctypedef> = @record{ 162 | actime: int64, 163 | modtime: int64 164 | } 165 | local function utime(file: cstring, times: *utimbuf): cint '> end 166 | ## end 167 | -- C APIs 168 | local function rename(old: cstring, new: cstring): cint '> end 169 | local function strerror(errnum: cint): cstring '> end 170 | local errno: cint '> 171 | local EINTR: cint '> 172 | 173 | ---------------------------------------------------------------------------------------------------- 174 | 175 | -- The file system namespace. 176 | global fs = @record{} 177 | 178 | -- Character separator for directories. 179 | global fs.sep: string = #[ccinfo.is_windows and '\\' or '/']# 180 | 181 | -- File status kind. 182 | global fs.StatKind = @enum(uint32){ 183 | INVALID = 0, 184 | FILE, 185 | DIRECTORY, 186 | LINK, 187 | SOCKET, 188 | PIPE, 189 | CHARACTER, 190 | BLOCK, 191 | OTHER, 192 | } 193 | 194 | -- File status information. 195 | global fs.StatInfo = @record{ 196 | kind: fs.StatKind, 197 | dev: uint64, 198 | ino: uint64, 199 | nlink: uint64, 200 | mode: uint32, 201 | uid: uint32, 202 | gid: uint32, 203 | rdev: uint64, 204 | size: int64, 205 | atime: int64, 206 | mtime: int64, 207 | ctime: int64, 208 | blksize: int64, -- not available in Windows 209 | blocks: int64, -- not available in Windows 210 | } 211 | 212 | -- Maximum path name size. 213 | local MAX_PATH_SIZE = 4096 214 | 215 | -- Chunk size when reading or writing files. 216 | local IO_CHUNK_SIZE = 4096 217 | 218 | local function result_from_errno(ok: boolean): (boolean, string, integer) 219 | if not ok then 220 | return false, strerror(errno), errno 221 | end 222 | return true, (@string){}, 0 223 | end 224 | 225 | local function kind_from_mode(mode: cuint): fs.StatKind 226 | if S_ISREG(mode) then 227 | return fs.StatKind.FILE 228 | elseif S_ISDIR(mode) then 229 | return fs.StatKind.DIRECTORY 230 | elseif S_ISLNK(mode) then 231 | return fs.StatKind.LINK 232 | elseif S_ISSOCK(mode) then 233 | return fs.StatKind.SOCKET 234 | elseif S_ISFIFO(mode) then 235 | return fs.StatKind.PIPE 236 | elseif S_ISCHR(mode) then 237 | return fs.StatKind.CHARACTER 238 | elseif S_ISBLK(mode) then 239 | return fs.StatKind.BLOCK 240 | end 241 | return fs.StatKind.OTHER 242 | end 243 | 244 | -- Checks if a directory exists. 245 | function fs.isdir(path: string): boolean 246 | local st: stat_t 247 | return stat(path, &st) == 0 and S_ISDIR(st.st_mode) 248 | end 249 | 250 | -- Checks if a file exists. 251 | function fs.isfile(path: string): boolean 252 | local st: stat_t 253 | return stat(path, &st) == 0 and not S_ISDIR(st.st_mode) 254 | end 255 | 256 | -- Returns the current working directory. 257 | function fs.cwdir(): (string, string, integer) 258 | local buf: [MAX_PATH_SIZE]cchar 259 | if getcwd(&buf, #buf) == nilptr then 260 | return (@string){}, strerror(errno), errno 261 | end 262 | return string.copy(&buf), (@string){}, 0 263 | end 264 | 265 | -- Changes the current working directory. 266 | function fs.chdir(path: string): (boolean, string, integer) 267 | return result_from_errno(chdir(path) == 0) 268 | end 269 | 270 | -- Changes permissions of a file. 271 | function fs.chmod(path: string, mode: uint32): (boolean, string, integer) 272 | return result_from_errno(chmod(path, mode) == 0) 273 | end 274 | 275 | -- Changes ownership of a file. 276 | function fs.chown(path: string, uid: uint32, gid: uint32): (boolean, string, integer) 277 | ## if ccinfo.is_windows then 278 | return false, 'not implemented yet', -1 279 | ## else 280 | return result_from_errno(chown(path, uid, gid) == 0) 281 | ## end 282 | end 283 | 284 | -- Creates a directory. 285 | function fs.mkdir(path: string): (boolean, string, integer) 286 | ## if ccinfo.is_windows then 287 | return result_from_errno(mkdir(path) == 0) 288 | ## else 289 | local mode: cuint = S_IRUSR|S_IWUSR|S_IXUSR|S_IRGRP|S_IWGRP|S_IXGRP|S_IROTH|S_IXOTH 290 | return result_from_errno(mkdir(path, mode) == 0) 291 | ## end 292 | end 293 | 294 | --[[ 295 | Creates a file into `path` and write `contents`. 296 | If `contents` is not present then an empty is created. 297 | If the file exists, it will be overwritten. 298 | ]] 299 | function fs.mkfile(path: string, contents: facultative(string)): (boolean, string, integer) 300 | ## if ccinfo.is_windows then 301 | local fd: cint = open(path, O_CREAT|O_WRONLY|O_TRUNC|O_BINARY, _S_IREAD|_S_IWRITE) 302 | ## else 303 | local fd: cint = open(path, O_CREAT|O_WRONLY|O_TRUNC|O_BINARY, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH) 304 | ## end 305 | if fd < 0 then 306 | return result_from_errno(false) 307 | end 308 | defer close(fd) end 309 | ## if not contents.type.is_niltype then 310 | if contents.size > 0 then 311 | local writtenbytes: usize = 0 312 | repeat -- keep writing until all bytes are written 313 | local remaining: usize = contents.size - writtenbytes 314 | if remaining > IO_CHUNK_SIZE then remaining = IO_CHUNK_SIZE end 315 | local writebytes: isize = write(fd, &contents.data[writtenbytes], remaining) 316 | if writebytes < 0 and errno ~= EINTR then 317 | return result_from_errno(false) 318 | elseif writebytes > 0 then 319 | -- the write may be incomplete, keep writing until finished 320 | writtenbytes = writtenbytes + (@usize)(writebytes) 321 | end 322 | until writtenbytes >= contents.size 323 | end 324 | ## end 325 | return true, (@string){}, 0 326 | end 327 | 328 | --[[ 329 | Creates a link for object `oldpath` at `newpath`. 330 | By default symbolic links are created, unless `hardlink` is true. 331 | ]] 332 | function fs.mklink(oldpath: string, newpath: string, hard: facultative(boolean)): (boolean, string, integer) 333 | ## if not hard.is_niltype then 334 | local hard: boolean = false 335 | ## end 336 | ## if ccinfo.is_windows then 337 | local res: cint 338 | if hard then 339 | res = CreateSymbolicLinkA(newpath, oldpath, fs.isdir(oldpath) and 1 or 0) 340 | else 341 | res = CreateHardLinkA(newpath, oldpath, nilptr) 342 | end 343 | if res == 0 then 344 | return false, 'windows error', GetLastError() 345 | end 346 | return true, (@string){}, 0 347 | ## else 348 | return result_from_errno((hard and link(oldpath, newpath) or symlink(oldpath, newpath)) == 0) 349 | ## end 350 | end 351 | 352 | -- Removes a directory. 353 | function fs.rmdir(path: string): (boolean, string, integer) 354 | return result_from_errno(rmdir(path) == 0) 355 | end 356 | 357 | -- Removes a file. 358 | function fs.rmfile(path: string): (boolean, string, integer) 359 | return result_from_errno(unlink(path) == 0) 360 | end 361 | 362 | -- Moves a file or directory. 363 | function fs.move(oldpath: string, newpath: string): (boolean, string, integer) 364 | return result_from_errno(rename(oldpath, newpath) == 0) 365 | end 366 | 367 | -- Updates access and modification time of a file. 368 | function fs.touch(path: string): (boolean, string, integer) 369 | return result_from_errno(utime(path, nilptr) == 0) 370 | end 371 | 372 | -- Gets file status information. 373 | function fs.stat(path: string, linkstat: facultative(boolean)): (fs.StatInfo, string, integer) 374 | ## if linkstat.type.is_niltype then 375 | local linkstat: boolean = false 376 | ## end 377 | local st: stat_t 378 | local res: cint 379 | if linkstat then 380 | ## if ccinfo.is_windows then 381 | local win32buffer: WIN32_FILE_ATTRIBUTE_DATA 382 | if GetFileAttributesEx(path, GetFileExInfoStandard, &win32buffer) ~= 0 and 383 | win32buffer.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT ~= 0 then 384 | st.st_mode = _S_IFLNK 385 | else 386 | res = stat(path, &st) 387 | end 388 | ## else 389 | res = lstat(path, &st) 390 | ## end 391 | else 392 | res = stat(path, &st) 393 | end 394 | if res ~= 0 then 395 | return (@fs.StatInfo){}, strerror(errno), errno 396 | end 397 | local si: fs.StatInfo = { 398 | kind = kind_from_mode(st.st_mode), 399 | dev = st.st_dev, 400 | ino = st.st_ino, 401 | nlink = st.st_nlink, 402 | mode = st.st_mode, 403 | uid = st.st_uid, 404 | gid = st.st_gid, 405 | rdev = st.st_rdev, 406 | size = st.st_size, 407 | atime = st.st_atime, 408 | mtime = st.st_mtime, 409 | ctime = st.st_ctime, 410 | } 411 | ## if not ccinfo.is_windows then 412 | si.blksize = st.st_blksize 413 | si.blocks = st.st_blocks 414 | ## end 415 | return si, (@string){}, 0 416 | end 417 | 418 | -- Reads all contents from a file. 419 | function fs.readfile(path: string): (string, string, integer) 420 | local fd: cint = open(path, O_RDONLY|O_BINARY, 0) 421 | if fd < 0 then 422 | return (@string){}, strerror(errno), errno 423 | end 424 | defer close(fd) end 425 | local sb: stringbuilder 426 | repeat -- read in chunks 427 | local p: span(byte) = sb:prepare(IO_CHUNK_SIZE) 428 | if p:empty() then 429 | sb:destroy() 430 | return (@string){}, 'out of buffer memory', -1 431 | end 432 | local nr: isize = read(fd, p.data, IO_CHUNK_SIZE) 433 | if nr < 0 and errno ~= EINTR then 434 | sb:destroy() 435 | return (@string){}, strerror(errno), errno 436 | elseif nr > 0 then 437 | sb:commit((@usize)(nr)) 438 | end 439 | until nr < IO_CHUNK_SIZE 440 | return sb:promote(), (@string){}, 0 441 | end 442 | 443 | -- Reads value from a symbolic link file. 444 | function fs.readlink(path: string): (string, string, integer) 445 | local buf: [MAX_PATH_SIZE+1]cchar 446 | ## if ccinfo.is_windows then 447 | local h: pointer = CreateFileA(path, GENERIC_READ, 448 | FILE_SHARE_READ | FILE_SHARE_WRITE, nilptr, 449 | OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nilptr); 450 | if h == INVALID_HANDLE_VALUE then 451 | return (@string){}, 'windows error', GetLastError() 452 | end 453 | defer CloseHandle(h) end 454 | local len: clong = GetFinalPathNameByHandleA(h, &buf, #buf, FILE_NAME_OPENED) 455 | if len < 0 then 456 | return (@string){}, 'windows error', GetLastError() 457 | end 458 | ## else 459 | local len: clong = readlink(path, &buf, #buf) 460 | if len < 0 then 461 | return (@string){}, strerror(errno), errno 462 | end 463 | ## end 464 | if len == #buf then 465 | return (@string){}, 'buffer truncated', -1 466 | end 467 | return string{data=&buf,size=(@usize)(len)}:copy(), (@string){}, 0 468 | end 469 | 470 | --[[ 471 | Copies file from `oldpath` to `newpath`. 472 | If a file exists in `newpath`, it will be overwritten. 473 | File permissions are preserved. 474 | ]] 475 | function fs.cpfile(oldpath: string, newpath: string): (boolean, string, integer) 476 | -- acquire a file descriptor for the input file 477 | local ifd: cint = open(oldpath, O_RDONLY|O_BINARY, 0) 478 | if ifd < 0 then return result_from_errno(false) end 479 | defer close(ifd) end 480 | -- retrieve mode information from the input file 481 | local si: stat_t 482 | if fstat(ifd, &si) ~= 0 then return result_from_errno(false) end 483 | -- create an output file and acquire its FD 484 | local ofd: cint = open(newpath, O_CREAT|O_WRONLY|O_TRUNC|O_BINARY, si.st_mode) 485 | if ofd < 0 then return result_from_errno(false) end 486 | defer close(ofd) end 487 | -- perform file copy in chunks until end of file 488 | repeat 489 | local buf: [IO_CHUNK_SIZE]cchar 490 | local readbytes: isize = read(ifd, &buf[0], #buf) 491 | if readbytes < 0 then return result_from_errno(false) end 492 | if readbytes > 0 then -- read some bytes 493 | local writtenbytes: usize = 0 494 | repeat -- keep writing until all read bytes are written 495 | local writebytes: isize = write(ofd, &buf[writtenbytes], (@usize)(readbytes) - writtenbytes) 496 | if writebytes < 0 and errno ~= EINTR then 497 | return result_from_errno(false) 498 | elseif writebytes > 0 then 499 | -- the write may be incomplete, keep writing until finished 500 | writtenbytes = writtenbytes + (@usize)(writebytes) 501 | end 502 | until writtenbytes >= (@usize)(readbytes) 503 | end 504 | until readbytes == 0 505 | return true, (@string){}, 0 506 | end 507 | 508 | --[[ 509 | TODO: 510 | * lock/unlock file APIs 511 | * use string representation for file modes? 512 | * `walkdir` iterator to walk over all entries from a directory 513 | * convert system error codes to library error codes 514 | ]] 515 | 516 | return fs 517 | -------------------------------------------------------------------------------- /heapqueue.nelua: -------------------------------------------------------------------------------- 1 | --[[ 2 | The heap queue library provides an priority queue using binary trees. 3 | 4 | Heap queues designed such that its first element is always the smallest of the elements 5 | it contains, according to some ordering criteria. 6 | 7 | It is similar to a heap, where elements can be inserted at any moment, 8 | and only the minimum heap element is retrieved (the one at the top in the queue). 9 | ]] 10 | 11 | require 'iterators' 12 | 13 | ## local function make_heapqueueT(T, CompareFunc, Allocator) 14 | ## static_assert(traits.is_type(T), "invalid type '%s'", T) 15 | ## if not Allocator then 16 | require 'allocators.default' 17 | ## Allocator = DefaultAllocator 18 | ## end 19 | 20 | local Allocator: type = #[Allocator]# 21 | local T: type = @#[T]# 22 | 23 | -- Heap queue record defined when instantiating the generic `heapqueue` with type `T`. 24 | local heapqueueT: type = @record{ 25 | data: span(T), 26 | size: usize, 27 | allocator: Allocator 28 | } 29 | 30 | ##[[ 31 | local heapqueueT = heapqueueT.value 32 | heapqueueT.is_contiguous = true 33 | heapqueueT.is_container = true 34 | heapqueueT.is_heapqueue = true 35 | heapqueueT.subtype = T 36 | ]] 37 | 38 | ## if not CompareFunc then 39 | ## function CompareFunc(a, b) 40 | in #[a]# < #[b]# 41 | ## end 42 | ## elseif CompareFunc.is_generic then 43 | ## CompareFunc = CompareFunc.func 44 | ## end 45 | 46 | --[[ 47 | Creates a heap queue using a custom allocator instance. 48 | Useful only when using instanced allocators. 49 | ]] 50 | function heapqueueT.make(allocator: Allocator): heapqueueT 51 | local v: heapqueueT 52 | v.allocator = allocator 53 | return v 54 | end 55 | 56 | --[[ 57 | Removes all elements from the queue. 58 | The internal storage buffer is not freed, and it may be reused. 59 | ]] 60 | function heapqueueT:clear(): void 61 | self.size = 0 62 | end 63 | 64 | --[[ 65 | Free queue resources and resets it to a zeroed state. 66 | Useful only when not using the garbage collector. 67 | ]] 68 | function heapqueueT:destroy(): void 69 | self.allocator:spandealloc(self.data) 70 | self.data = (@span(T))() 71 | self.size = 0 72 | end 73 | 74 | -- Effectively the same as `destroy`, called when a to-be-closed variable goes out of scope. 75 | function heapqueueT:__close(): void 76 | self:destroy() 77 | end 78 | 79 | -- Reserve at least `n` elements in the queue storage. 80 | function heapqueueT:reserve(n: usize): void 81 | if likely(self.data.size >= n) then return end 82 | self.data = self.allocator:xspanrealloc(self.data, n) 83 | end 84 | 85 | -- Grow queue storage to accommodate at least one more element, used internally. 86 | local function heapqueueT_grow(self: *heapqueueT): void 87 | local cap: usize = 1 88 | if likely(self.data.size ~= 0) then 89 | cap = self.data.size * 2 90 | check(cap > self.data.size, 'capacity overflow') 91 | end 92 | self.data = self.allocator:xspanrealloc(self.data, cap) 93 | end 94 | 95 | -- Shift up queue items in order to maintain the heap property. 96 | local function heapqueueT_shiftup(self: *heapqueueT, index: usize): void 97 | while index > 0 do 98 | local parent_index: usize = (index - 1) // 2 99 | local child: T = self.data[index] 100 | local parent: T = self.data[parent_index] 101 | if not #[CompareFunc]#(child, parent) then break end 102 | self.data[parent_index] = child 103 | self.data[index] = parent 104 | index = parent_index 105 | end 106 | end 107 | 108 | -- Shift down queue items in order to maintain the heap property. 109 | local function heapqueueT_shiftdown(self: *heapqueueT, index: usize): void 110 | while true do 111 | local smallest_index: usize = index 112 | local current: T = self.data[smallest_index] 113 | local smallest: T = current 114 | local left_index: usize = 2*index + 1 115 | if left_index < self.size then 116 | local left: T = self.data[left_index] 117 | if #[CompareFunc]#(left, smallest) then 118 | smallest_index = left_index 119 | smallest = left 120 | end 121 | local right_index: usize = left_index + 1 122 | if right_index < self.size then 123 | local right: T = self.data[right_index] 124 | if #[CompareFunc]#(right, smallest) then 125 | smallest_index = right_index 126 | smallest = right 127 | end 128 | end 129 | end 130 | if smallest_index == index then return end 131 | self.data[smallest_index] = current 132 | self.data[index] = smallest 133 | index = smallest_index 134 | end 135 | end 136 | 137 | --[[ 138 | Inserts element `v` into the queue. 139 | Queue elements are shifted in order to maintain the heap property. 140 | ]] 141 | function heapqueueT:push(v: T): void 142 | local newsize: usize = self.size + 1 143 | if unlikely(newsize > self.data.size) then 144 | heapqueueT_grow(self) 145 | end 146 | self.data[self.size] = v 147 | heapqueueT_shiftup(self, self.size) 148 | self.size = newsize 149 | end 150 | 151 | --[[ 152 | Removes element at position `pos` in the queue and returns its value. 153 | Queue elements are shifted in order to maintain the heap property. 154 | The position `pos` must be valid (within queue bounds). 155 | ]] 156 | function heapqueueT:remove(pos: usize): T 157 | check(pos < self.size, 'position out of bounds') 158 | local newsize: usize = self.size - 1 159 | local v: T = self.data[pos] 160 | self.size = newsize 161 | if likely(pos ~= newsize) then 162 | self.data[pos] = self.data[newsize] 163 | heapqueueT_shiftdown(self, pos) 164 | heapqueueT_shiftup(self, pos) 165 | end 166 | return v 167 | end 168 | 169 | --[[ 170 | Removes the highest priority element in queue, returning its value. 171 | Queue elements are shifted in order to maintain the heap property. 172 | The heap queue must not be empty. 173 | ]] 174 | function heapqueueT:pop(): T 175 | check(self.size > 0, 'attempt to pop an empty heapqueue') 176 | local newsize: usize = self.size - 1 177 | self.size = newsize 178 | local v: T = self.data[0] 179 | if likely(newsize > 0) then 180 | self.data[0] = self.data[newsize] 181 | heapqueueT_shiftdown(self, 0) 182 | end 183 | return v 184 | end 185 | 186 | -- Returns the value of the highest priority element in queue. 187 | function heapqueueT:top(): T 188 | check(self.size > 0, 'attempt to access top of an empty heapqueue') 189 | return self.data[0] 190 | end 191 | 192 | -- Returns the number of elements the heap queue can store before triggering a reallocation. 193 | function heapqueueT:capacity(): isize 194 | return (@isize)(self.data.size) 195 | end 196 | 197 | --[[ 198 | Returns reference to element at position `pos`. 199 | Note that `pos` is not necessarily in priority order. 200 | Position `pos` must be valid (within queue bounds). 201 | The reference will remain valid until the queue is modified. 202 | Used when indexing elements with square brackets (`[]`). 203 | ]] 204 | function heapqueueT:__atindex(pos: usize): *T 205 | check(pos < self.size, 'position out of bounds') 206 | return &self.data[pos] 207 | end 208 | 209 | --[[ 210 | Returns the number of elements in the queue. 211 | Used by the length operator (`#`). 212 | ]] 213 | function heapqueueT:__len(): isize 214 | return (@isize)(self.size) 215 | end 216 | 217 | -- Concept matching fixed arrays of T. 218 | local an_arrayT: type = #[concept(function(x) 219 | if x.type:is_array_of(T) then 220 | return true 221 | end 222 | return false, string.format("no viable conversion from '%s' to '%s'", x.type, heapqueueT) 223 | end, function(node) 224 | return node.tag == 'InitList' and types.ArrayType(T, #node) 225 | end)]# 226 | 227 | --[[ 228 | Initializes queue elements from a fixed array. 229 | Used to initialize queue elements with curly braces (`{}`). 230 | ]] 231 | function heapqueueT.__convert(values: an_arrayT): heapqueueT 232 | local self: heapqueueT 233 | self:reserve(#values) 234 | for i:usize=0,<#values do 235 | self:push(values[i]) 236 | end 237 | return self 238 | end 239 | 240 | ## return heapqueueT 241 | ## end 242 | 243 | --[[ 244 | Generic used to instantiate a heap queue type in the form of `heapqueue(T, CompareFunc, Allocator)`. 245 | 246 | Argument `T` is the value type that the queue will store. 247 | Argument `CompareFunc` is a function to compare `T` values, 248 | in case absent then the `<` operator is used. 249 | Argument `Allocator` is an allocator type for the container storage, 250 | in case absent then `DefaultAllocator` is used. 251 | ]] 252 | global heapqueue: type = #[generalize(make_heapqueueT)]# 253 | 254 | return heapqueue 255 | -------------------------------------------------------------------------------- /inspector.nelua: -------------------------------------------------------------------------------- 1 | --[=[ 2 | The inspector module. 3 | 4 | This module it's a function that receives any value and 5 | returns the whole content of the passed value as a string. 6 | 7 | Inspired by the [inspect.lua](https://github.com/kikito/inspect.lua/) library 8 | 9 | ## Usage 10 | 11 | Copy `inspector.nelua` file to a project and require it, then just 12 | call the `inspector` function passing the value to be inspected: 13 | 14 | ```lua 15 | require 'inspector' 16 | 17 | local vec2 = @record{ 18 | x: number, 19 | y: number 20 | } 21 | 22 | local position: vec2 = { 2, 3 } 23 | 24 | -- Just call inspector function passing the value to be inspected (it can be anything). 25 | -- note: if you're not using GC, then you must free the value, otherwise it'll leak. 26 | local pos_contents = inspector(position) 27 | 28 | -- print the inspected position 29 | print(pos_contents) 30 | 31 | --[[ output: 32 | (@vec2){ 33 | x = 2.0_f64, 34 | y = 3.0_f64, 35 | } 36 | ]] 37 | ``` 38 | ]=] 39 | 40 | require 'string' 41 | local stringbuilder = require 'stringbuilder' 42 | 43 | -- meta-utils 44 | ##[=[ 45 | local function is_simple_primitive(type) 46 | return type.is_boolean or type.is_scalar or type.is_stringy 47 | end 48 | 49 | local short_suffixes = { 50 | integer = '_i', 51 | uinteger = '_u', 52 | number = '_n', 53 | byte = '_b', 54 | isize = '_is', 55 | int8 = '_i8', 56 | int16 = '_i16', 57 | int32 = '_i32', 58 | int64 = '_i64', 59 | int128 = '_i128', 60 | usize = '_us', 61 | uint8 = '_u8', 62 | uint16 = '_u16', 63 | uint32 = '_u32', 64 | uint64 = '_u64', 65 | uint128 = '_u128', 66 | float32 = '_f32', 67 | float64 = '_f64', 68 | float128 = '_f128', 69 | 70 | cchar = '_cc', 71 | cschar = '_csc', 72 | cshort = '_cs', 73 | cint = '_ci', 74 | clong = '_cl', 75 | clonglong = '_cll', 76 | cptrdiff = '_cpd', 77 | cuchar = '_cuc', 78 | cushort = '_cus', 79 | cuint = '_cui', 80 | culong = '_cul', 81 | culonglong = '_cull', 82 | csize = '_cz', 83 | cfloat = '_cf', 84 | cdouble = '_cd', 85 | clongdouble = '_cld', 86 | } 87 | 88 | local function inspect_procedure_value(value) 89 | static_assert(value.type.is_procedure, 'value is not a procedure!') 90 | return tostring(value.type) 91 | end 92 | ]=] 93 | 94 | -- concepts 95 | local a_simple_primitive = #[concept(function(attr) return is_simple_primitive(attr.type) end)]# 96 | local a_array = #[concept(function(attr) return attr.type.is_array end)]# 97 | local a_enum_value = #[concept(function(attr) return attr.type.is_enum end)]# 98 | local a_record_value = #[concept(function(attr) return attr.type.is_record end)]# 99 | local a_non_aggregate = #[concept(function(attr) return not attr.type.is_aggregate end)]# 100 | local a_pointer = #[concept(function(attr) return attr.type.is_pointer end)]# 101 | local a_stringy = #[concept(function(attr) return attr.type.is_cstring or attr.type.is_string end)]# 102 | local a_function = #[concept(function(attr) return attr.type.is_function end)]# 103 | local a_union = #[concept(function(attr) return attr.type.is_union end)]# 104 | 105 | -- specialized inspector functions 106 | local function inspect_union_value(value: a_union, sb: *stringbuilder) 107 | sb:write(#['(@' .. tostring(value.type) .. ")() --[[ @" .. value.type:typedesc() .. " ]]"]#) 108 | end 109 | 110 | local function inspect_function_value(value: a_function, ctx: string , sb: *stringbuilder) 111 | ## if ctx.value == 'field' then 112 | ## local type_fn = ' --[[ ' .. inspect_procedure_value(value) .. ' ]]' 113 | 114 | if value == nilptr then 115 | sb:write(#['nilptr' .. type_fn]#) 116 | else 117 | sb:write((@pointer)(value), #[type_fn]#) 118 | end 119 | 120 | ## elseif ctx.value == 'array element' then 121 | sb:write((@pointer)(value)) 122 | 123 | ## else 124 | sb:write(#[inspect_procedure_value(value)]#) 125 | ## end 126 | end 127 | 128 | local function inspect_pointer_value(value: a_pointer, sb: *stringbuilder) 129 | if value then 130 | sb:write(value, #[' --[[ @' .. tostring(value.type) .. ' ]]']#) 131 | else 132 | sb:write(#['nilptr --[[ @' .. tostring(value.type) .. ' ]]']#) 133 | end 134 | end 135 | 136 | local function inspect_stringy_value(value: a_stringy, sb: *stringbuilder) 137 | sb:write('"', value, '"') 138 | ## if value.type.is_cstring then 139 | sb:write(' --[[ @cstring ]]') 140 | ## end 141 | end 142 | 143 | local function inspect_simple_value(value: a_simple_primitive, use_suffix: boolean , sb: *stringbuilder) 144 | ## local type_suffix = short_suffixes[value.type.name] 145 | 146 | ## if type_suffix and use_suffix.value then 147 | sb:write(value, #[type_suffix]#) 148 | ## else 149 | sb:write(value) 150 | ## end 151 | end 152 | 153 | local function inspect_enum_value(value: a_enum_value, sb: *stringbuilder) 154 | sb:write(#[tostring(value.type) .. '.']#) 155 | 156 | -- Note: this unrolls various `if`s, just to find and write to sb the right enum field 157 | -- correspondent to the value 158 | ## for _, field in ipairs(value.type.fields) do 159 | if value == #[field.value]# then 160 | sb:write(#[field.name]#) 161 | end 162 | ## end 163 | 164 | sb:writef(#[' --[[ %d'.. short_suffixes[value.type.subtype.name] ..' ]]']#, value) 165 | end 166 | 167 | local function non_aggregate_value(value: a_non_aggregate, use_suffix: boolean , sb: *stringbuilder) 168 | ## if value.type.is_enum then 169 | inspect_enum_value(value, sb) 170 | 171 | ## elseif is_simple_primitive(value.type) then 172 | inspect_simple_value(value, use_suffix, sb) 173 | 174 | ## elseif value.type.is_pointer then 175 | inspect_pointer_value(value, sb) 176 | 177 | ## elseif value.type.is_nilptr then 178 | sb:write('nilptr') 179 | 180 | ## else 181 | sb:write('Unknown') 182 | ## end 183 | end 184 | 185 | ## local function MACRO_inspect_array_value(value, inspect_array_value_fn_name, sb) 186 | do 187 | #[sb]#:write(#['(@' .. tostring(value.type) .. '){ ']#) 188 | 189 | for i = 0, < #(#[value]#) do 190 | ## if value.type.subtype.is_stringy then 191 | inspect_stringy_value(#[value]#[i], sb) 192 | 193 | ## elseif value.type.subtype.is_function then 194 | inspect_function_value(#[value]#[i], 'array element', sb) 195 | 196 | ## elseif value.type.subtype.is_union then 197 | inspect_union_value(#[value]#[i], sb) 198 | 199 | ## elseif not value.type.subtype.is_aggregate then 200 | non_aggregate_value(#[value]#[i], false, sb) 201 | 202 | ## else 203 | ## if value.type.subtype.is_record then 204 | inspect_record_value(#[value]#[i], 0, false, sb) 205 | 206 | ## elseif value.type.subtype.is_array then 207 | #|inspect_array_value_fn_name|#(#[value]#[i], sb) 208 | ## end 209 | ## end 210 | 211 | if i < #value - 1 then 212 | #[sb]#:write(', ') 213 | end 214 | end 215 | 216 | #[sb]#:write(" }") 217 | end 218 | ## end 219 | 220 | local function inspect_record_value(value: a_record_value, level: integer, use_type_prefix: boolean, sb: *stringbuilder) 221 | local function inspect_array_value(value: a_array, sb: *stringbuilder) 222 | MACRO_inspect_array_value!(value, 'inspect_array_value', sb) 223 | end 224 | 225 | if use_type_prefix then 226 | sb:write(#['(@' .. tostring(value.type) .. '){\n']#) 227 | else 228 | sb:write('{\n') 229 | end 230 | 231 | ## for _, field in ipairs(value.type.fields) do 232 | sb:writebyte(' '_b, 2*(level+1)) 233 | sb:write(#[field.name .. ' = ']#) 234 | 235 | ## if field.type.is_stringy then 236 | inspect_stringy_value(value.#|field.name|#, sb) 237 | 238 | ## elseif field.type.is_function then 239 | inspect_function_value(value.#|field.name|#, 'field', sb) 240 | 241 | ## elseif field.type.is_union then 242 | inspect_union_value(value.#|field.name|#, sb) 243 | 244 | ## elseif not field.type.is_aggregate then 245 | non_aggregate_value(value.#|field.name|#, true, sb) 246 | 247 | ## else 248 | ## if field.type.is_record then 249 | inspect_record_value(value.#|field.name|#, (level + 1), true, sb) 250 | 251 | ## elseif field.type.is_array then 252 | inspect_array_value(value.#|field.name|#, sb) 253 | 254 | ## else 255 | sb:write('Unknown') 256 | ## end 257 | ## end 258 | 259 | sb:write(',\n') 260 | ## end 261 | 262 | sb:writebyte(' '_b, 2*level) 263 | sb:write('}') 264 | end 265 | 266 | local function inspect_array_value(value: a_array, sb: *stringbuilder) 267 | MACRO_inspect_array_value!(value, 'inspect_array_value', sb) 268 | end 269 | 270 | -- The inspector function, pass any value and the contents will be returned as a string (which should be freed). 271 | global function inspector(value: auto): string 272 | local sb: stringbuilder 273 | 274 | ## if value.type.is_type then 275 | sb:write(#['@'..value.value:typedesc()]#) 276 | 277 | ## elseif value.type.is_stringy then 278 | inspect_stringy_value(value, sb) 279 | 280 | ## elseif value.type.is_union then 281 | inspect_union_value(value, sb) 282 | 283 | ## elseif value.type.is_procedure then 284 | sb:write(#[inspect_procedure_value(value)]#) 285 | 286 | ## elseif not value.type.is_aggregate then 287 | non_aggregate_value(value, true, sb) 288 | 289 | ## else 290 | ## if value.type.is_record then 291 | inspect_record_value(value, 0, true, sb) 292 | 293 | ## elseif value.type.is_array then 294 | inspect_array_value(value, sb) 295 | 296 | ## else 297 | sb:write('Unknown') 298 | ## end 299 | ## end 300 | 301 | return sb:promote() 302 | end 303 | -------------------------------------------------------------------------------- /json.nelua: -------------------------------------------------------------------------------- 1 | --[[ 2 | The json module. 3 | 4 | This module contains utilities to parse chunks JSON texts into 5 | Nelua types and vice-versa. 6 | 7 | TODO: 8 | * Handle allocation errors on `json.emit`. 9 | * Escape strings in `json.emit`. 10 | ]] 11 | 12 | local string = require 'string' 13 | local strconv = require 'detail.strconv' 14 | local strchar = require 'detail.strchar' 15 | 16 | -- The JSON module. 17 | global json = @record{} 18 | 19 | -- Helper to check if `c` is a number digit (matches [A-Fa-f.+-]). 20 | local function isnumdigit(c: byte): boolean 21 | return strchar.isxdigit(c) or -- hex digit 22 | c == '+'_b or c == '-'_b or -- sign 23 | c == '.'_b -- fractional 24 | end 25 | 26 | -- Helper to convert an integer to its corresponding UTF-8 byte sequence. 27 | local function utf8esc(x: uint32): ([8]byte, int32) 28 | local buf: [8]byte 29 | local n: int32 = 1 30 | check(x <= 0x7FFFFFFF_u) 31 | if x < 0x80 then -- ASCII? 32 | buf[7] = (@byte)(x) 33 | else -- need continuation bytes 34 | local mfb: usize = 0x3f -- maximum that fits in first byte 35 | repeat -- add continuation bytes 36 | buf[8 - n] = (@byte)(0x80 | (x & 0x3f)) 37 | n = n + 1 38 | x = x >> 6 -- removed added bits 39 | mfb = mfb >> 1 -- now there is one less bit available in first byte 40 | until x <= mfb -- still needs continuation byte? 41 | buf[8 - n] = (@byte)((~mfb << 1) | x) -- add first byte 42 | end 43 | memory.move(&buf[0], &buf[8-n], n) 44 | memory.zero(&buf[n], 8-n) 45 | return buf, n 46 | end 47 | 48 | -- Helper to remove escape sequences from a JSON string. 49 | local function unescape(s: string): (string, string) 50 | if s.size == 0 then return (@string){}, (@string){} end 51 | if not memory.scan(s.data, '\\'_b, s.size) then return string.copy(s), (@string){} end 52 | local ns: string = string.create(s.size) 53 | local i: usize, j: usize = 0, 0 54 | while i < s.size do 55 | local c: byte = s.data[i] 56 | i = i + 1 57 | if likely(c ~= '\\'_b) then 58 | ns.data[j] = c 59 | else 60 | c = s.data[i] 61 | i = i + 1 62 | switch c do 63 | case '"'_b, '\\'_b, '/'_b then ns.data[j] = c 64 | case 'b'_b then ns.data[j] = '\b'_b 65 | case 't'_b then ns.data[j] = '\t'_b 66 | case 'n'_b then ns.data[j] = '\n'_b 67 | case 'f'_b then ns.data[j] = '\f'_b 68 | case 'r'_b then ns.data[j] = '\r'_b 69 | case 'u'_b then 70 | for k: usize=i,= s.size or not strchar.isxdigit(s.data[k]) then 72 | ns:destroy() 73 | return ns, 'expected 4 hexadecimal digits in utf8 sequence' 74 | end 75 | end 76 | local ok: boolean, hex: integer = strconv.str2int((@string){data=&s.data[i],size=4}, 16) 77 | if not ok then return ns, 'malformed hexadecimal integer in utf8 sequence' end 78 | local utf8buf: [8]byte, len: int32 = utf8esc(hex) 79 | memory.copy(&ns.data[j], &utf8buf[0], len) 80 | i = i + 4 81 | j = j + (@usize)(len) - 1 82 | else 83 | ns:destroy() 84 | return ns, 'unexpected string escape sequence' 85 | end 86 | end 87 | j = j + 1 88 | end 89 | ns.data[j] = 0 90 | ns.size = j 91 | return ns, (@string){} 92 | end 93 | 94 | --[[ 95 | Parses JSON text from `chunk` into type `T`. 96 | Returns value of type `T`, plus an error message in case of errors and number of characters parsed. 97 | 98 | This parser creates a specialized JSON parser at compile-time, 99 | it's like a JSON parser compiler. 100 | 101 | This parser follow these rules: 102 | * `T` is used as a schema and must always be a `record`, `hashmap`, `vector,` or `sequence` type. 103 | * The schema `T` cannot be incomplete or have missing fields, otherwise a parse error will occur. 104 | * The schema `T` types cannot mismatch the JSON chunk, otherwise parse error will occur. 105 | * The schema `T` must always be a `record`, `hashmap`, `vector,` or `sequence` type. 106 | * Missing fields in the JSON chunk will be initialized to zeros. 107 | * Fields with `null` value in the JSON chunk will be initialized to zeros. 108 | * Parsed strings and containers allocates new memory. 109 | * Follows JSON 4 spec (JSON 5 is not supported). 110 | 111 | The following types are handled: 112 | * `vector` and `sequence`, parsed from JSON arrays. 113 | * `hashmap` where `K` is a `string`, parsed from JSON objects. 114 | * `record` types, parsed from JSON objects. 115 | * `string`, parsed from JSON strings. 116 | * `boolean`, parsed from JSON `true` or `false`. 117 | * All primitive integer and float types are parsed from JSON numbers. 118 | * Records may be nested. 119 | ]] 120 | function json.parse(chunk: string, T: type): (auto, string, usize) 121 | local res: T 122 | local i: usize = 0 123 | local s: *[0]byte, slen: usize = chunk.data, chunk.size 124 | local tmpinit: usize 125 | local c: byte 126 | local err: string 127 | local tmpstr: string, tmpbool: boolean 128 | -- macros utils 129 | ## local function skip_whitespaces() 130 | while i < slen and strchar.isspace(s[i]) do i = i + 1 end 131 | ## end 132 | ## local function peek_char() 133 | c = i < slen and s[i] or 0 134 | ## end 135 | ## local function skip_comma_peek_char(ec) 136 | ## peek_char() 137 | if c ~= ','_b then break end 138 | i = i + 1 139 | ## skip_whitespaces() peek_char() 140 | ## end 141 | ## local function expect_token(ec, noskip) 142 | if i >= slen or s[i] ~= #[string.byte(ec)]# then 143 | return res, #['expected token `'..ec..'`, perhaps JSON schema has incompatible fields']#, i 144 | end 145 | i = i + 1 146 | ## if not noskip then skip_whitespaces() end 147 | ## end 148 | ## local function expect_string() 149 | ## expect_token('"', true) 150 | tmpinit = i 151 | while i < slen and (s[i] ~= '"'_b or s[i-1] == '\\'_b) do i = i + 1 end 152 | tmpstr = string{data=&s[tmpinit], size=i-tmpinit} 153 | ## expect_token'"' 154 | ## end 155 | ## local function expect_number() 156 | ## peek_char() 157 | if c == '"'_b then 158 | ## expect_string() 159 | else 160 | tmpinit = i 161 | while i < slen and isnumdigit(s[i]) do i = i + 1 end 162 | tmpstr = string{data=&s[tmpinit], size=i-tmpinit} 163 | end 164 | ## skip_whitespaces() 165 | ## end 166 | ## local function expect_boolean() 167 | tmpinit = i 168 | while i < slen and strchar.islower(s[i]) do i = i + 1 end 169 | tmpstr = string{data=&s[tmpinit], size=i-tmpinit} 170 | ## skip_whitespaces() 171 | ## end 172 | ## local function expect_value(vtype) 173 | local v: #[vtype]#; 174 | if i+3 < slen and s[i] == 'n'_b and s[i+1] == 'u'_b and s[i+2] == 'l'_b and s[i+3] == 'l'_b then -- null? 175 | i = i + 4 176 | else 177 | ## if vtype.is_pointer then 178 | local pv: #[vtype]# 179 | do 180 | ## expect_value(vtype.subtype) 181 | pv = new(v) 182 | end 183 | v = pv 184 | ## elseif vtype.is_float then 185 | ## expect_number() 186 | local ok: boolean 187 | ok, v = strconv.str2num(tmpstr) 188 | if not ok then 189 | err = 'malformed JSON number' 190 | end 191 | ## elseif vtype.is_integral then 192 | ## expect_number() 193 | local ok: boolean 194 | ok, v = strconv.str2int(tmpstr, 10) 195 | if not ok then 196 | err = 'malformed JSON integer' 197 | end 198 | ## elseif vtype.is_boolean then 199 | ## expect_boolean() 200 | v = tmpstr == 'true' 201 | if not v and tmpstr ~= 'false' then 202 | err = 'malformed JSON boolean' 203 | end 204 | ## elseif vtype.is_string then 205 | ## expect_string() 206 | local err: string 207 | v, err = unescape(tmpstr) 208 | ## elseif vtype.is_record then 209 | local err: string, advlen: usize 210 | v, err, advlen = json.parse(string{data=&s[i],size=slen-i}, #[vtype]#) 211 | i = i + advlen 212 | ## else static_error("cannot parse element of type '%s'", vtype) end 213 | end 214 | ## end 215 | -- begin parsing 216 | ## skip_whitespaces() -- skip leading whitespaces 217 | ## if res.type.is_vector or res.type.is_sequence then -- parse array into vector/sequence 218 | ## expect_token'[' peek_char() 219 | while c ~= ']'_byte do 220 | ## expect_value(res.type.subtype) 221 | res:push(v) 222 | if err.size > 0 then return res, err, i end 223 | ## skip_comma_peek_char() 224 | end 225 | ## expect_token']' 226 | ## elseif res.type.is_hashmap then -- parse object into hashmap 227 | local fieldname: string 228 | ## expect_token'{' peek_char() 229 | while c ~= '}'_byte do 230 | ## expect_string() 231 | fieldname = tmpstr 232 | ## expect_token':' expect_value(res.type.V) 233 | res[string.copy(fieldname)] = v 234 | if err.size > 0 then return res, err, i end 235 | ## skip_comma_peek_char() 236 | end 237 | ## expect_token'}' 238 | ## elseif res.type.is_record then -- parse object into a record 239 | local fieldname: string 240 | ## expect_token'{' peek_char() 241 | while c ~= '}'_byte do 242 | ## expect_string() 243 | fieldname = tmpstr 244 | ## expect_token':' 245 | ## for _,field in ipairs(res.type.fields) do 246 | if fieldname == #[field.name]# then 247 | ## expect_value(field.type) 248 | res.#|field.name|# = v 249 | if err.size > 0 then return res, err, i end 250 | goto nextfield 251 | end 252 | ## end 253 | return res, "JSON has invalid or incompatible fields with its schema", i 254 | ::nextfield:: 255 | ## skip_comma_peek_char() 256 | end 257 | ## expect_token'}' 258 | ## else static_error("cannot parse JSON into type '%s'", res.type) end 259 | return res, (@string){}, i 260 | end 261 | 262 | --[[ 263 | Emit JSON text from `obj` returning a string. 264 | If argument `sb` is present, 265 | then the JSON is is actually emitted into to the string builder and returns nothing. 266 | 267 | The `obj` type should follow same rules as `json.parse`. 268 | ]] 269 | function json.emit(obj: auto, sb: facultative(*stringbuilder)) 270 | ## local returnstr = sb.type.is_niltype 271 | ## if returnstr then 272 | local sbo: stringbuilder 273 | local sb: *stringbuilder = &sbo 274 | ## end 275 | -- consider pointers 276 | ## local derefobjtype = obj.type:implicit_deref_type() 277 | ## if obj.type.is_pointer then 278 | if obj == nilptr then 279 | sb:write('null') 280 | goto finish 281 | end 282 | ## end 283 | -- emit 284 | ## if obj.type.is_scalar or obj.type.is_boolean then 285 | sb:write(obj) 286 | ## elseif obj.type.is_pointer and (obj.type.subtype.is_scalar or obj.type.subtype.is_boolean) then 287 | sb:write($obj) 288 | ## elseif obj.type.is_stringy then 289 | sb:write('"', obj, '"') 290 | ## elseif obj.type.is_pointer and obj.type.subtype.is_string then 291 | sb:write('"', $obj, '"') 292 | ## elseif obj.type.is_niltype then 293 | sb:write('null') 294 | ## elseif derefobjtype.is_vector or derefobjtype.is_sequence or derefobjtype.is_array or derefobjtype.is_span then 295 | -- emit contiguous container as an array 296 | local T: type = #[derefobjtype.subtype]# 297 | sb:write'[' 298 | for i: isize, v: T in ipairs(obj) do 299 | json.emit(v, sb) 300 | sb:write',' 301 | end 302 | if #obj > 0 then sb:rollback(1) end -- remove extra comma 303 | sb:write']' 304 | ## elseif derefobjtype.is_hashmap then -- emit hashmap as an object 305 | local K: type, V: type = #[derefobjtype.K]#, #[derefobjtype.V]# 306 | sb:write'{' 307 | for k: K, v: V in pairs(obj) do 308 | json.emit(k, sb) 309 | sb:write':' 310 | json.emit(v, sb) 311 | sb:write',' 312 | end 313 | if #obj > 0 then sb:rollback(1) end -- remove extra comma 314 | sb:write'}' 315 | ## elseif derefobjtype.is_record then -- emit record as an object 316 | sb:write'{' 317 | ## for i,field in ipairs(derefobjtype.fields) do 318 | sb:write('"', #[field.name]#, '":') 319 | json.emit(obj.#|field.name|#, sb) 320 | ## if i<#derefobjtype.fields then 321 | sb:write',' 322 | ## end 323 | ## end 324 | sb:write'}' 325 | ## else static_error("cannot emit JSON from type '%s'", objtype) end 326 | ::finish:: 327 | ## if returnstr then 328 | return sb:promote() 329 | ## end 330 | end 331 | 332 | return json 333 | -------------------------------------------------------------------------------- /nester.nelua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Nester is a minimal unit testing framework for Nelua with a focus on being simple to use. 3 | 4 | ## Features 5 | 6 | * Minimal, just one file. 7 | * Self contained, no external dependencies. 8 | * Simple and hackable when needed. 9 | * Use `describe` and `it` blocks to describe tests. 10 | * Supports `before` and `after` handlers. 11 | * Colored output. 12 | * Configurable via the script or with environment variables. 13 | * Quiet mode, to use in live development. 14 | * Optionally filter tests by name. 15 | * Show location of tests errors. 16 | * Show time to complete tests. 17 | 18 | ## Usage 19 | 20 | Copy `nester.nelua` file to a project and require it, 21 | and use it the following way: 22 | 23 | ```lua 24 | require 'nester' 25 | 26 | -- Customize nester configuration. 27 | nester.stop_on_fail = false 28 | 29 | nester.describe('my project', function() 30 | nester.before(function(name: string) 31 | -- This function is run before every test. 32 | end) 33 | 34 | nester.describe('module1', function() -- Describe blocks can be nested. 35 | nester.it('feature1', function() 36 | expect.equal('something', 'something') -- Pass. 37 | end) 38 | 39 | nester.it('feature2', function() 40 | expect.truthy(false) -- Fail. 41 | end) 42 | end) 43 | end) 44 | 45 | nester.report() -- Print overall statistic of the tests run. 46 | nester.exit() -- Exit with success if all tests passed. 47 | ``` 48 | 49 | ## Customizing output with environment variables 50 | 51 | To customize the output of nester externally, 52 | you can set the following environment variables before running a test suite: 53 | 54 | * `NESTER_QUIET="true"`, omit print of passed tests. 55 | * `NESTER_COLORED="false"`, disable colored output. 56 | * `NESTER_SHOW_TRACEBACK="false"`, disable traceback on test failures. 57 | * `NESTER_SHOW_ERROR="false"`, omit print of error description of failed tests. 58 | * `NESTER_STOP_ON_FAIL="true"`, stop on first test failure. 59 | * `NESTER_UTF8TERM="false"`, disable printing of UTF-8 characters. 60 | * `NESTER_FILTER="some text"`, filter the tests that should be run. 61 | 62 | Note that these configurations can be changed via script too, check the documentation. 63 | ]] 64 | 65 | require 'errorhandling' 66 | require 'io' 67 | require 'os' 68 | require 'sequence' 69 | require 'coroutine' 70 | 71 | -- Returns whether the terminal supports UTF-8 characters. 72 | local function is_utf8term(): boolean 73 | local lang: string = os.getenv('LANG') 74 | return lang:find('[uU][tT][fF]%-?8$') > 0 75 | end 76 | 77 | -- Returns whether a system environment variable is "true". 78 | local function getboolenv(varname: string, default: boolean): boolean 79 | local val: string = os.getenv(varname) 80 | if val == 'true' then 81 | return true 82 | elseif val == 'false' then 83 | return false 84 | end 85 | return default 86 | end 87 | 88 | -- Returns the concatenation of all strings from `v` separated by `sep`. 89 | local function concat_strings(v: sequence(string), sep: string): string 90 | local sb: stringbuilder 91 | for i:integer=1,<#v-1 do 92 | sb:write(v[i], sep) 93 | end 94 | if #v > 0 then 95 | sb:write(v[#v]) 96 | end 97 | return sb:promote() 98 | end 99 | 100 | -- The nester module. 101 | global nester: type = @record{} 102 | -- Whether lines of passed tests should not be printed. False by default. 103 | global nester.quiet: boolean = getboolenv('NESTER_QUIET', false) 104 | -- Whether the output should be colorized. True by default. 105 | global nester.colored: boolean = getboolenv('NESTER_COLORED', true) 106 | -- Whether the error description of a test failure should be shown. True by default. 107 | global nester.show_error: boolean = getboolenv('NESTER_SHOW_ERROR', true) 108 | -- Whether test suite should exit on first test failure. False by default. 109 | global nester.stop_on_fail: boolean = getboolenv('NESTER_STOP_ON_FAIL', false) 110 | -- Whether we can print UTF-8 characters to the terminal. True by default when supported. 111 | global nester.utf8term: boolean = getboolenv('NESTER_UTF8TERM', is_utf8term()) 112 | -- A string with a Lua pattern to filter tests. Empty by default. 113 | global nester.filter: string = os.getenv('NESTER_FILTER') 114 | -- Function to retrieve time in seconds with milliseconds precision, `os.now` by default. 115 | global nester.seconds: auto = os.now 116 | 117 | -- Variables used internally for the nester state. 118 | local nester_start: number = 0 119 | local has_started: boolean = false 120 | local last_succeeded: boolean = false 121 | local level: integer = 0 122 | local successes: integer = 0 123 | local total_successes: integer = 0 124 | local failures: integer = 0 125 | local total_failures: integer = 0 126 | local start: number = 0 127 | local befores: sequence(sequence(function(name: string))) 128 | local afters: sequence(sequence(function(name: string))) 129 | local names: sequence(string) 130 | local last_error_filename: string 131 | local last_error_lineno: integer 132 | 133 | -- Color codes. 134 | local Color = @enum{ 135 | Reset = 0, 136 | Bright, 137 | Red, 138 | Green, 139 | Blue, 140 | Magenta, 141 | } 142 | 143 | -- Helper to translate a color code into a terminal color code. 144 | local function color(key: Color): string 145 | if nester.colored then 146 | switch key do 147 | case Color.Reset then return '\27[0m' 148 | case Color.Bright then return '\27[1m' 149 | case Color.Red then return '\27[31m' 150 | case Color.Green then return '\27[32m' 151 | case Color.Blue then return '\27[34m' 152 | case Color.Magenta then return '\27[35m' 153 | end 154 | end 155 | return '' 156 | end 157 | 158 | -- Exit the application with success code if all tests passed, or failure code otherwise. 159 | function nester.exit(): void 160 | -- cleanup before exit 161 | befores:destroy() 162 | afters:destroy() 163 | names:destroy() 164 | nester.filter:destroy() 165 | os.exit(total_failures == 0) 166 | end 167 | 168 | --[[ 169 | Describe a block of tests, which consists in a set of tests. 170 | Describes can be nested. 171 | `name` is a string used to describe the block. 172 | `func` a function containing all the tests or other describes. 173 | ]] 174 | function nester.describe(name: string, func: function()): void 175 | if level == 0 then -- get start time for top level describe blocks 176 | start = nester.seconds() 177 | if not has_started then 178 | nester_start = start 179 | end 180 | end 181 | -- setup describe block variables 182 | failures = 0 183 | successes = 0 184 | level = level + 1 185 | names:push(name) 186 | befores:push{} 187 | afters:push{} 188 | -- run the describe block 189 | func() 190 | -- cleanup describe block 191 | local seq = afters:pop() 192 | seq:destroy() 193 | seq = befores:pop() 194 | seq:destroy() 195 | names:pop() 196 | level = level - 1 197 | -- pretty print statistics for top level describe block 198 | if level == 0 and not nester.quiet and (successes > 0 or failures > 0) then 199 | io.write(failures == 0 and color(Color.Green) or color(Color.Red), '[====] ', 200 | color(Color.Magenta), name, color(Color.Reset), ' | ', 201 | color(Color.Green), successes, color(Color.Reset), ' successes / ') 202 | if failures > 0 then 203 | io.write(color(Color.Red), failures, color(Color.Reset), ' failures / ') 204 | end 205 | io.write(color(Color.Bright)) 206 | io.writef('%.6f', nester.seconds() - start) 207 | io.write(color(Color.Reset), ' seconds\n') 208 | end 209 | end 210 | 211 | -- Pretty print the line on the test file where an error happened. 212 | local function show_error_line(filename: string, lineno: integer): void 213 | io.write(' (', color(Color.Blue), filename, color(Color.Reset), 214 | ':', color(Color.Bright), lineno, color(Color.Reset), ')') 215 | end 216 | 217 | -- Pretty print the test name, with breadcrumb for the describe blocks. 218 | local function show_test_name(name: string): void 219 | for _,descname in ipairs(names) do 220 | io.write(color(Color.Magenta), descname, color(Color.Reset), ' | ') 221 | end 222 | io.write(color(Color.Bright), name, color(Color.Reset)) 223 | end 224 | 225 | local function nester_it(name: string, func: function(), filename: string, lineno: integer): void 226 | -- skip the test if it does not match the filter 227 | if #nester.filter > 0 then 228 | names:push(name) 229 | local fullname: string = concat_strings(names, ' | ') 230 | names:pop() 231 | if fullname:find(nester.filter) == 0 then 232 | return 233 | end 234 | end 235 | -- execute before handlers. 236 | for _,levelbefores in ipairs(befores) do 237 | for _,beforefn in ipairs(levelbefores) do 238 | beforefn(name) 239 | end 240 | end 241 | -- run the test, capturing errors if any 242 | local success: boolean, err: string = pcall(func) 243 | local errfilename: string, errlineno: integer = last_error_filename, last_error_lineno 244 | last_error_filename = (@string){} 245 | last_error_lineno = 0 246 | -- count successes and failures 247 | if success then 248 | successes = successes + 1 249 | total_successes = total_successes + 1 250 | else 251 | failures = failures + 1 252 | total_failures = total_failures + 1 253 | end 254 | -- print the test run. 255 | if not nester.quiet then -- show test status and complete test name 256 | if success then 257 | io.write(color(Color.Green), '[PASS] ', color(Color.Reset)) 258 | else 259 | io.write(color(Color.Red), '[FAIL] ', color(Color.Reset)) 260 | end 261 | show_test_name(name) 262 | if not success then 263 | show_error_line(filename, lineno) 264 | end 265 | io.write('\n') 266 | else 267 | if success then -- show just a character hinting that the test succeeded 268 | local o: string = (nester.utf8term and nester.colored) and '\226\151\143' or 'o' 269 | io.write(color(Color.Green), o, color(Color.Reset)) 270 | else -- show complete test name on failure 271 | io.write(last_succeeded and '\n' or '', color(Color.Red), '[FAIL] ', color(Color.Reset)) 272 | show_test_name(name) 273 | show_error_line(filename, lineno) 274 | io.write('\n') 275 | end 276 | end 277 | if not success and #err > 0 and nester.show_error then 278 | if #errfilename > 0 and errlineno > 0 then 279 | io.write(color(Color.Blue), errfilename, color(Color.Reset), 280 | ':', color(Color.Bright), errlineno, color(Color.Reset), ': ') 281 | end 282 | io.write(err, '\n\n') 283 | end 284 | io.flush() 285 | -- stop on failure 286 | if not success and nester.stop_on_fail then 287 | if nester.quiet then 288 | io.write('\n') 289 | io.flush() 290 | end 291 | nester.exit() 292 | end 293 | -- execute after handlers 294 | for _,levelafters in ipairs(afters) do 295 | for _,afterfn in ipairs(levelafters) do 296 | afterfn(name) 297 | end 298 | end 299 | last_succeeded = success 300 | end 301 | 302 | --[[ 303 | Declare a test, which consists of a set of assertions. 304 | Where `name` is the test name, 305 | and `func` is the function containing all assertions. 306 | ]] 307 | function nester.it(name: string, func: function()): void 308 | ## local polysrcloc = polysrcloc 309 | nester_it(name, func, #[polysrcloc.srcname or '']#, #[polysrcloc.lineno or 0]#) 310 | end 311 | 312 | --[[ 313 | Set a function that is called before every test inside a describe block. 314 | A single string containing the name of the test about to be run will be passed to `func`. 315 | ]] 316 | function nester.before(func: function(string)): void 317 | befores[level]:push(func) 318 | end 319 | 320 | --[[ 321 | Set a function that is called after every test inside a describe block. 322 | A single string containing the name of the test that was finished will be passed to `func`. 323 | The function is executed independently if the test passed or failed. 324 | ]] 325 | function nester.after(func: function(string)): void 326 | afters[level]:push(func) 327 | end 328 | 329 | --[[ 330 | Pretty print statistics of all test runs. 331 | With total success, total failures and run time in seconds. 332 | ]] 333 | function nester.report(): boolean 334 | local now: number = nester.seconds() 335 | io.write(nester.quiet and '\n' or '', 336 | color(Color.Green), total_successes, color(Color.Reset), ' successes / ', 337 | color(Color.Red), total_failures, color(Color.Reset), ' failures / ', 338 | color(Color.Bright)) 339 | io.writef('%.6f seconds', now - (nester_start or now)) 340 | io.write(color(Color.Reset), '\n') 341 | io.flush() 342 | return total_failures == 0 343 | end 344 | 345 | -- Expect module, containing utility function for doing assertions inside a test. 346 | global expect: type = @record{} 347 | 348 | -- Checks if `a` is equals to `b`, if not raises a test error. 349 | function expect.equal(a: auto, b: auto): void 350 | ## local polysrcloc = polysrcloc 351 | if not (a == b) then 352 | last_error_filename = #[polysrcloc.srcname or '']# 353 | last_error_lineno = #[polysrcloc.lineno or 0]# 354 | local msg: string = string.format('expected value be equal\nfirst value:\n%s\nsecond value:\n%s', a, b) 355 | error(msg, 0) 356 | end 357 | end 358 | 359 | -- Checks if `a` is different from `b`, if not raises a test error. 360 | function expect.not_equal(a: auto, b: auto): void 361 | ## local polysrcloc = polysrcloc 362 | if a == b then 363 | last_error_filename = #[polysrcloc.srcname or '']# 364 | last_error_lineno = #[polysrcloc.lineno or 0]# 365 | local msg: string = string.format('expected values to be not equal\nfirst value:\n%s\nsecond value:\n%s', a, b) 366 | error(msg, 0) 367 | end 368 | end 369 | 370 | -- Checks if `v` is true, if not raises a test error. 371 | function expect.truthy(v: boolean): void 372 | ## local polysrcloc = polysrcloc 373 | if not v then 374 | last_error_filename = #[polysrcloc.srcname or '']# 375 | last_error_lineno = #[polysrcloc.lineno or 0]# 376 | error('expected expression to be true, but got false', 0) 377 | end 378 | end 379 | 380 | -- Checks if `v` is false, if not raises a test error. 381 | function expect.falsy(v: boolean): void 382 | ## local polysrcloc = polysrcloc 383 | if v then 384 | last_error_filename = #[polysrcloc.srcname or '']# 385 | last_error_lineno = #[polysrcloc.lineno or 0]# 386 | error('expected expression to be false, but got true', 0) 387 | end 388 | end 389 | 390 | -- Raises test error message `msg`. 391 | function expect.error(msg: facultative(string)): void 392 | ## local polysrcloc = polysrcloc 393 | ## if msg.type.is_niltype then 394 | local msg: string = 'error raised' 395 | ## end 396 | last_error_filename = #[polysrcloc.srcname or '']# 397 | last_error_lineno = #[polysrcloc.lineno or 0]# 398 | error(msg, 0) 399 | end 400 | 401 | -- Raises test error message `msg` if `cond` is false. 402 | function expect.assert(cond: auto, msg: string): auto 403 | ## local polysrcloc = polysrcloc 404 | if not cond or #msg > 0 then 405 | error(msg, 0) 406 | end 407 | return cond 408 | end 409 | -------------------------------------------------------------------------------- /or_return.nelua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Utility that forward `expr` if its second result is an empty error message, 3 | otherwise returns the current scope function with its first return zero initialized, 4 | plus the error message. 5 | ]] 6 | ## function or_return(expr) 7 | local res, errmsg: string = #[expr]# 8 | if #errmsg > 0 then 9 | ##[[ 10 | local rettype1 = 11 | context.state.funcscope and 12 | context.state.funcscope.funcsym and 13 | context.state.funcscope.funcsym.type and 14 | context.state.funcscope.funcsym.type.rettypes[1] 15 | static_assert(rettype1, 'current function has no typed return') 16 | ]] 17 | return (@#[rettype1]#)(), errmsg 18 | end 19 | in res 20 | ## end 21 | -------------------------------------------------------------------------------- /signal.nelua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Module with utilities to catch and raise OS application signals. 3 | ]] 4 | 5 | local signal: type = @record{} 6 | 7 | local signal.SIGABRT: cint '> 8 | local signal.SIGFPE: cint '> 9 | local signal.SIGILL: cint '> 10 | local signal.SIGINT: cint '> 11 | local signal.SIGSEGV: cint '> 12 | local signal.SIGTERM: cint '> 13 | 14 | local signal.SIG_DFL: function(cint): void '> 15 | local signal.SIG_IGN: function(cint): void '> 16 | local signal.SIG_ERR: function(cint): void '> 17 | 18 | -- Returns last errno message plus its code. 19 | local function geterrno(): (string, integer) 20 | local errno: cint '> 21 | local function strerror(errnum: cint): cstring '> end 22 | return strerror(errno), errno 23 | end 24 | 25 | --[[ 26 | Setup application to call `handler` when it receives signal `sig`. 27 | Returns true when success, otherwise false plus error message and code. 28 | ]] 29 | function signal.setsignal(sig: cint, handler: function(cint): void): (boolean, string, integer) 30 | ## cinclude '@unistd.h' 31 | ## cemit '#if defined(_POSIX_VERSION)' 32 | local sigset_t: type ',cincomplete> = @record{} 33 | local sigaction_t: type ',ctypedef'sigaction',cincomplete> = @record{ 34 | sa_handler: function(cint), 35 | sa_mask: sigset_t, 36 | } 37 | local function sigemptyset(set: *sigset_t): cint '> end 38 | local function sigaction(sig: cint, act: *sigaction_t, oact: *sigaction_t): cint end 39 | local sa: sigaction_t = {sa_handler = handler} 40 | sigemptyset(&sa.sa_mask) -- do not mask any signal 41 | local res: cint = sigaction(sig, &sa, nilptr) 42 | if res ~= 0 then return false, geterrno() end 43 | ## cemit '#else' 44 | local function c_signal(sig: cint, handler: function(cint): void): function(cint): void '> end 45 | local res: function(cint): void = c_signal(sig, handler) 46 | if res == signal.SIG_ERR then return false, geterrno() end 47 | ## cemit '#endif' 48 | return true, (@string){}, 0 49 | end 50 | 51 | -- Raises signal `sig`. 52 | function signal.raise(sig: cint): (boolean, string, integer) 53 | local function c_raise(sig: cint): cint '> end 54 | local res: cint = c_raise(sig) 55 | if res ~= 0 then return false, geterrno() end 56 | return true, (@string){}, 0 57 | end 58 | 59 | return signal 60 | -------------------------------------------------------------------------------- /sqlite3.nelua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Minimal module with helpers for easy work with SQLite3. 3 | ]] 4 | 5 | require 'string' 6 | 7 | ---------------------------------------------------------------------------------------------------- 8 | -- Import SQLite3 API 9 | 10 | ##[[ 11 | cinclude '' 12 | linklib 'sqlite3' 13 | ]] 14 | 15 | local sqlite3: type = @record{} 16 | local function sqlite3_close(a1: *sqlite3): cint end 17 | local function sqlite3_close_v2(a1: *sqlite3): cint end 18 | local function sqlite3_exec(a1: *sqlite3, sql: cstring, callback: function(pointer, cint, *cstring, *cstring): cint, a4: pointer, errmsg: *cstring): cint end 19 | local function sqlite3_free(a1: pointer): void end 20 | local function sqlite3_open_v2(filename: cstring, ppDb: **sqlite3, flags: cint, zVfs: cstring): cint end 21 | local function sqlite3_errmsg(a1: *sqlite3): cstring end 22 | local function sqlite3_errstr(a1: cint): cstring end 23 | local sqlite3_stmt: type = @record{} 24 | local function sqlite3_prepare_v2(db: *sqlite3, zSql: cstring, nByte: cint, ppStmt: **sqlite3_stmt, pzTail: *cstring): cint end 25 | local function sqlite3_column_count(pStmt: *sqlite3_stmt): cint end 26 | local function sqlite3_step(a1: *sqlite3_stmt): cint end 27 | local function sqlite3_column_double(a1: *sqlite3_stmt, iCol: cint): float64 end 28 | local function sqlite3_column_int64(a1: *sqlite3_stmt, iCol: cint): int64 end 29 | local function sqlite3_column_text(a1: *sqlite3_stmt, iCol: cint): *cuchar end 30 | local function sqlite3_finalize(pStmt: *sqlite3_stmt): cint end 31 | local function sqlite3_db_cacheflush(a1: *sqlite3): cint end 32 | 33 | ---------------------------------------------------------------------------------------------------- 34 | -- SQLite3 module 35 | 36 | local SQLite3: type = @record{ 37 | db: *sqlite3 38 | } 39 | 40 | local SQLite3.Stmt: type = @record{ 41 | stmt: *sqlite3_stmt 42 | } 43 | 44 | local SQLite3.Error: type = @enum(int32){ 45 | OK = 0, 46 | ERROR = 1, 47 | INTERNAL = 2, 48 | PERM = 3, 49 | ABORT = 4, 50 | BUSY = 5, 51 | LOCKED = 6, 52 | NOMEM = 7, 53 | READONLY = 8, 54 | INTERRUPT = 9, 55 | IOERR = 10, 56 | CORRUPT = 11, 57 | NOTFOUND = 12, 58 | FULL = 13, 59 | CANTOPEN = 14, 60 | PROTOCOL = 15, 61 | EMPTY = 16, 62 | SCHEMA = 17, 63 | TOOBIG = 18, 64 | CONSTRAINT = 19, 65 | MISMATCH = 20, 66 | MISUSE = 21, 67 | NOLFS = 22, 68 | AUTH = 23, 69 | FORMAT = 24, 70 | RANGE = 25, 71 | NOTADB = 26, 72 | NOTICE = 27, 73 | WARNING = 28, 74 | ROW = 100, 75 | DONE = 101, 76 | } 77 | 78 | local SQLite3.OpenFlags: type = @enum(int32){ 79 | READONLY = 0x00000001, 80 | READWRITE = 0x00000002, 81 | CREATE = 0x00000004, 82 | DELETEONCLOSE = 0x00000008, 83 | EXCLUSIVE = 0x00000010, 84 | AUTOPROXY = 0x00000020, 85 | URI = 0x00000040, 86 | MEMORY = 0x00000080, 87 | MAIN_DB = 0x00000100, 88 | TEMP_DB = 0x00000200, 89 | TRANSIENT_DB = 0x00000400, 90 | MAIN_JOURNAL = 0x00000800, 91 | TEMP_JOURNAL = 0x00001000, 92 | SUBJOURNAL = 0x00002000, 93 | SUPER_JOURNAL = 0x00004000, 94 | NOMUTEX = 0x00008000, 95 | FULLMUTEX = 0x00010000, 96 | SHAREDCACHE = 0x00020000, 97 | PRIVATECACHE = 0x00040000, 98 | WAL = 0x00080000, 99 | NOFOLLOW = 0x01000000, 100 | EXRESCODE = 0x02000000, 101 | MASTER_JOURNAL = 0x00004000, 102 | } 103 | 104 | -- Close statement (used to cleanup for loops). 105 | function SQLite3.Stmt:__close(): void 106 | if not self.stmt then return end 107 | local res: SQLite3.Error = sqlite3_finalize(self.stmt) 108 | assert(res == SQLite3.Error.OK, 'failed to close statement') 109 | self.stmt = nilptr 110 | end 111 | 112 | function SQLite3.Error.__tostring(errcode: SQLite3.Error): string 113 | return string.format('SQLite3 Error %d (%s)', errcode, sqlite3_errstr(errcode)) 114 | end 115 | 116 | --[[ 117 | Opens (or creates if it does not exist) an SQLite database with name `filename` 118 | and returns its handle. 119 | In case of an error, the function returns a closed database, an error message and an error code. 120 | ]] 121 | function SQLite3.open(filename: string, flags: facultative(int32)): (SQLite3, string, SQLite3.Error) 122 | ## if flags.type.is_niltype then 123 | local flags: int32 = SQLite3.OpenFlags.READWRITE | SQLite3.OpenFlags.CREATE 124 | ## end 125 | local db: *sqlite3 126 | local status: SQLite3.Error = sqlite3_open_v2(filename, &db, flags, nilptr) 127 | if status ~= SQLite3.Error.OK then 128 | local errmsg: string 129 | if db then 130 | errmsg = sqlite3_errmsg(db) 131 | sqlite3_close(db) 132 | else 133 | errmsg = sqlite3_errstr(status) 134 | end 135 | return SQLite3{}, errmsg, status 136 | end 137 | return SQLite3{db=db}, (@string){}, status 138 | end 139 | 140 | --[[ 141 | Closes a database. 142 | All SQL statements prepared using `db:prepare()` should have been finalized before this function is called. 143 | Returns true on success, otherwise false, plus and error message and an error code. 144 | ]] 145 | function SQLite3:close(): (boolean, string, SQLite3.Error) 146 | assert(self.db, 'attempt to use a closed sqlite database') 147 | local res: SQLite3.Error = sqlite3_close(self.db) 148 | if res ~= SQLite3.Error.OK then 149 | return false, sqlite3_errmsg(self.db), res 150 | end 151 | self.db = nilptr 152 | return true, (@string){}, res 153 | end 154 | 155 | function SQLite3:__close() 156 | if self and self.db then 157 | self:close() 158 | end 159 | end 160 | 161 | -- Returns true if database is open, false otherwise. 162 | function SQLite3:isopen(): boolean 163 | return self.db ~= nilptr 164 | end 165 | 166 | --[[ 167 | Compiles and executes the SQL statement(s) given in string `sql`. 168 | The statements are simply executed one after the other and not stored. 169 | The function true on success, otherwise false, plus and error message and an error code. 170 | 171 | If one or more of the SQL statements are queries, 172 | then the callback function specified in `func` is invoked once for each row of the query result 173 | (if func is nil, no callback is invoked). 174 | The callback receives four arguments: `udata` (the third parameter of the db:exec() call), 175 | the number of columns in the row, a table with the column values and another table with the column names. 176 | The callback function should return 0. 177 | If the callback returns a non-zero value then the query is aborted, 178 | all subsequent SQL statements are skipped and db:exec() returns sqlite3.ABORT. 179 | ]] 180 | function SQLite3:exec(sql: string, 181 | func: facultative(function(udata: pointer, ncols: cint, values: *[0]cstring, names: *[0]cstring): cint), 182 | udata: facultative(pointer)): (boolean, string, SQLite3.Error) 183 | assert(self.db, 'attempt to use a closed sqlite database') 184 | local cerrmsg: cstring 185 | ## if func.type.is_niltype then 186 | local res: SQLite3.Error = sqlite3_exec(self.db, sql, nilptr, nilptr, &cerrmsg) 187 | ## else 188 | local ExecCallback: type = @function(udata: pointer, ncols: cint, pvalues: *cstring, pnames: *cstring): cint 189 | ## if udata.type.is_niltype then 190 | local udata: pointer = nilptr 191 | ## end 192 | local res: SQLite3.Error = sqlite3_exec(self.db, sql, (@ExecCallback)(func), udata, &cerrmsg) 193 | ## end 194 | if res ~= SQLite3.Error.OK then 195 | local errmsg: string = string.copy(cerrmsg) 196 | sqlite3_free(cerrmsg) 197 | return false, errmsg, res 198 | end 199 | return true, (@string){}, res 200 | end 201 | 202 | -- Record used to store state of `urows`. 203 | local URowsState: type = @record{ 204 | db: *sqlite3, 205 | stmt: *sqlite3_stmt, 206 | } 207 | 208 | --[[ 209 | Creates an iterator that returns the successive rows selected by the SQL statement given in string `sql`. 210 | Each call to the iterator returns the values that correspond to the columns in the currently selected row. 211 | Types for each value to be returned by the iterator must be passed in `...`. 212 | ]] 213 | function SQLite3:urows(sql: string, ...: varargs): (auto, URowsState, auto, SQLite3.Stmt) 214 | local state: URowsState = {self.db} 215 | local res: SQLite3.Error = sqlite3_prepare_v2(state.db, sql, #sql, &state.stmt, nilptr) 216 | if res ~= SQLite3.Error.OK then 217 | error(sqlite3_errmsg(state.db)) 218 | end 219 | ##[[ 220 | local fortypes = {} 221 | for i=1,select('#', ...) do 222 | table.insert(fortypes, select(i, ...).attr.value) 223 | end 224 | static_assert(#fortypes > 0, 'missing value types in parameters') 225 | ]] 226 | local T: type = #[fortypes[1]]# 227 | assert(sqlite3_column_count(state.stmt) == #[#fortypes]#, 'results has incompatible number of columns') 228 | local function urows_next(state: URowsState, k: T) 229 | local res: SQLite3.Error = sqlite3_step(state.stmt) 230 | ## local fornodes = {} 231 | ## for i,fortype in ipairs(fortypes) do 232 | local #|'v'..i|#: #[fortype]# 233 | ## table.insert(fornodes, _ENV['v'..i]) 234 | ## end 235 | if res == SQLite3.Error.ROW then -- row available to read 236 | ## for i,fortype in ipairs(fortypes) do 237 | ## if fortype.is_string then 238 | #|'v'..i|# = string.copy((@cstring)(sqlite3_column_text(state.stmt, #[i-1]#))) 239 | ## elseif fortype.is_integral then 240 | #|'v'..i|# = sqlite3_column_int64(state.stmt, #[i-1]#) 241 | ## elseif fortype.is_float then 242 | #|'v'..i|# = sqlite3_column_double(state.stmt, #[i-1]#) 243 | ## else 244 | ## static_error("cannot handle type '%s'", fortype) 245 | ## end 246 | ## end 247 | return true, #[aster.unpack(fornodes)]# 248 | elseif res ~= SQLite3.Error.DONE then -- some unexpected error 249 | error(sqlite3_errmsg(state.db)) 250 | end 251 | -- finished reading all rows 252 | return false, #[aster.unpack(fornodes)]# 253 | end 254 | return urows_next, state, T(), SQLite3.Stmt{state.stmt} 255 | end 256 | 257 | -- Like `urows` but only for one row, returns `true` if a row is found plus its values. 258 | function SQLite3:urow(sql: string, ...: varargs) 259 | local urows_next, state, k, stmt = self:urows(sql, ...) 260 | return urows_next(state, k) 261 | end 262 | 263 | function SQLite3:cacheflush(): (boolean, string, SQLite3.Error) 264 | local res: SQLite3.Error = sqlite3_db_cacheflush(self.db) 265 | if res ~= SQLite3.Error.OK then 266 | return false, sqlite3_errmsg(self.db), res 267 | end 268 | return true, (@string){}, res 269 | end 270 | 271 | return SQLite3 272 | -------------------------------------------------------------------------------- /tests/Makefile: -------------------------------------------------------------------------------- 1 | NELUA=nelua 2 | NFLAGS=-L.. 3 | NELUARUN=$(NELUA) $(NFLAGS) 4 | 5 | test: \ 6 | algorithm_test \ 7 | bigint_test \ 8 | datetime_test \ 9 | ffi_test \ 10 | fs_test \ 11 | inspector_test \ 12 | json_test \ 13 | signal_test 14 | 15 | test-all: test test-test test-wrappers 16 | 17 | test-tester: \ 18 | nester_test \ 19 | 20 | test-wrappers: \ 21 | backtrace_test \ 22 | sqlite3_test 23 | 24 | ffi_test: 25 | $(NELUARUN) --shared-lib ffi_module.nelua -o ffi_module 26 | $(NELUARUN) ffi_test.nelua 27 | 28 | %_test: 29 | $(NELUARUN) $@.nelua 30 | -------------------------------------------------------------------------------- /tests/algorithm_test.nelua: -------------------------------------------------------------------------------- 1 | require 'nester' 2 | require 'algorithm' 3 | require 'vector' 4 | 5 | nester.describe("algorithm", function() 6 | nester.it("sort vector", function() 7 | local v: vector(integer) = {3,1,4,2,5,6} 8 | algorithm.sort(v) 9 | for i=1,<#v do 10 | expect.truthy(v[i-1] <= v[i]) 11 | end 12 | end) 13 | 14 | nester.it("sort array", function() 15 | local a: []integer = {3,1,4,2,5,6} 16 | algorithm.sort(a) 17 | for i=1,<#a do 18 | expect.truthy(a[i-1] <= a[i]) 19 | end 20 | end) 21 | 22 | nester.it("sort span", function() 23 | local a: []integer = {3,1,4,2,5,6} 24 | local s: span(integer) = &a 25 | algorithm.sort(s) 26 | for i=1,<#s do 27 | expect.truthy(s[i-1] <= s[i]) 28 | end 29 | end) 30 | 31 | nester.it("custom sort", function() 32 | local v: vector(integer) = {3,1,4,2,5,6} 33 | algorithm.sort(v, function(x: integer, y: integer) 34 | return x > y 35 | end) 36 | for i=1,<#v do 37 | expect.truthy(v[i-1] >= v[i]) 38 | end 39 | end) 40 | 41 | nester.it("heapsort", function() 42 | local v: vector(integer) = {3,1,4,2,5,6} 43 | algorithm.heapsort(v) 44 | for i=1,<#v do 45 | expect.truthy(v[i-1] <= v[i]) 46 | end 47 | end) 48 | 49 | nester.it("custom heapsort", function() 50 | local v: vector(integer) = {3,1,4,2,5,6} 51 | algorithm.heapsort(v, function(x: integer, y: integer) 52 | return x > y 53 | end) 54 | for i=1,<#v do 55 | expect.truthy(v[i-1] >= v[i]) 56 | end 57 | end) 58 | 59 | nester.it("unpack", function() 60 | local v: vector(integer) = {3,1,4,2,5,6} 61 | algorithm.unpackn(v, 0) 62 | local a = algorithm.unpackn(v, 1) 63 | assert(a == 3) 64 | local a, b = algorithm.unpackn(v, 2) 65 | assert(a == 3 and b == 1) 66 | local a, b, c = algorithm.unpackn(v, 3) 67 | assert(a == 3 and b == 1 and c == 4) 68 | end) 69 | end) 70 | -------------------------------------------------------------------------------- /tests/backtrace_test.nelua: -------------------------------------------------------------------------------- 1 | require 'nester' 2 | require 'backtrace' 3 | 4 | local function foo(): string 5 | return backtrace:traceback('hi') 6 | end 7 | 8 | local function boo(): string 9 | return foo() 10 | end 11 | 12 | local bt: string = boo() 13 | 14 | nester.describe("backtrace", function() 15 | nester.it("function traceback", function() 16 | expect.truthy(bt:find('hi') == 1) 17 | end) 18 | print(bt) 19 | end) 20 | -------------------------------------------------------------------------------- /tests/bigint_test.nelua: -------------------------------------------------------------------------------- 1 | local BigInt = require 'bigint' 2 | 3 | do -- empty 4 | local x: BigInt 5 | assert(#x == 0) 6 | assert(x:bitlen() == 0) 7 | assert(x:sign() == 0) 8 | assert(not x:isvalid()) 9 | end 10 | 11 | do -- new 12 | local x = BigInt.new() 13 | assert(x == 0) 14 | assert(#x == 0) 15 | assert(x:bitlen() == 0) 16 | assert(x:sign() == 0) 17 | assert(x:isvalid()) 18 | x:destroy() 19 | end 20 | 21 | do -- frominteger 22 | local x = BigInt.frominteger(0) assert(x:isvalid() and x == 0 and #x == 0) 23 | local x = BigInt.frominteger(2) assert(x:isvalid() and x == 2 and #x == 1) 24 | local x = BigInt.frominteger(0xfff) assert(x == 0xfff and #x == 2) 25 | end 26 | 27 | do -- fromstring 28 | local x = BigInt.fromstring('') assert(not x:isvalid()) 29 | local x = BigInt.fromstring('invalid') assert(not x:isvalid()) 30 | local x = BigInt.fromstring('0') assert(x:isvalid()) 31 | local x = BigInt.fromstring('2') assert(x:isvalid() and x == 2) 32 | local x = BigInt.fromstring('0000ffff', 16) assert(x == 0xffff) 33 | end 34 | 35 | do -- frombytes 36 | local x = BigInt.frombytes('') assert(x:isvalid() and x == 0) 37 | local x = BigInt.frombytes('\x13\x37') assert(x:isvalid() and x == 0x1337) 38 | local x = BigInt.frombytes('\x13\x37', true) assert(x == 0x3713) 39 | end 40 | 41 | do -- from 42 | local x = BigInt.from(1) assert(x == 1) 43 | local x = BigInt.from(0x7fffffff) assert(x == 0x7fffffff) 44 | local x = BigInt.from('0x7fffffff') assert(x == 0x7fffffff) 45 | local x = BigInt.from('2147483647') assert(x == 2147483647) 46 | end 47 | 48 | do -- clone 49 | local x = BigInt.from(0x1337) 50 | local y = x:clone() 51 | assert(x == y) 52 | end 53 | 54 | do -- copy 55 | local x = BigInt.from(0x1337) 56 | local y = BigInt.from(0x1338) 57 | x:copy_(y) 58 | assert(x == y) 59 | x:destroy() 60 | end 61 | 62 | do -- getbit 63 | local one = BigInt.from(0b10101) 64 | assert(one:getbit(0) == 1) 65 | assert(one:getbit(1) == 0) 66 | assert(one:getbit(2) == 1) 67 | assert(one:getbit(3) == 0) 68 | assert(one:getbit(4) == 1) 69 | assert(one:getbit(5) == 0) 70 | assert(one:getbit(6) == 0) 71 | end 72 | 73 | do -- sign 74 | local x = BigInt.from(2) assert(x:sign() == 1) 75 | local x = BigInt.from(-2) assert(x:sign() == -1) 76 | local x = BigInt.from(0) assert(x:sign() == 0) 77 | end 78 | do -- tointeger 79 | assert(BigInt.tointeger('0x7fffffff') == 0x7fffffff) 80 | assert(BigInt.tointeger('0x0123456789abcdef0123456789abcdef') & 0xfffffff == 0x9abcdef) 81 | end 82 | 83 | do -- tobytes 84 | local s = BigInt.tobytes('0x1337') assert(s == '\x13\x37') 85 | local s = BigInt.tobytes('0x1337', true) assert(s == '\x37\x13') 86 | local s = BigInt.tobytes('0x0137', false, 4) assert(s == '\x00\x00\x01\x37') 87 | local s = BigInt.tobytes('0x0137', true, 4) assert(s == '\x37\x01\x00\x00') 88 | end 89 | 90 | do -- tostring 91 | local b0 = BigInt.tostring(0) assert(b0 == '0') 92 | local b10 = BigInt.tostring(0x7fffffff) assert(b10 == '2147483647') 93 | local b16 = BigInt.tostring(0x7fffffff, 16) assert(b16 == '7fffffff') 94 | local b2 = BigInt.tostring(9, 2) assert(b2 == '1001') 95 | 96 | local b16 = BigInt.tostring(0xab, 16, 0) assert(b16 == 'ab') 97 | local b16 = BigInt.tostring(0xab, 16, 1) assert(b16 == 'ab') 98 | local b16 = BigInt.tostring(0xab, 16, 2) assert(b16 == 'ab') 99 | local b16 = BigInt.tostring(0xab, 16, 3) assert(b16 == '0ab') 100 | local b16 = BigInt.tostring(0xab, 16, 4) assert(b16 == '00ab') 101 | end 102 | 103 | do -- comparison 104 | local i1 = BigInt.from(1) 105 | local i2 = BigInt.from(2) 106 | assert(i1 == i1) assert(not (i1 == i2)) 107 | assert(i1 ~= i2) assert(not (i1 ~= i1)) 108 | assert(i1 <= i1) assert(i1 <= i2) assert(not (i1 >= i2)) 109 | assert(i1 >= i1) assert(i2 >= i1) assert(not (i2 <= i1)) 110 | assert(i1 < i2) assert(not (i1 < i1)) assert(not (i2 < i1)) 111 | assert(i2 > i1) assert(not (i1 > i1)) assert(not (i1 > i2)) 112 | end 113 | 114 | do -- arithmetic operations 115 | local i2 = BigInt.from(2) 116 | local n2 = BigInt.from(-2) 117 | local i3 = BigInt.from(3) 118 | local i5 = BigInt.from(5) 119 | local i18 = BigInt.from(18) 120 | local x = -i3 assert(x == -3) 121 | local x = i3 + i5 assert(x == 8) 122 | local x = i5 - i3 assert(x == 2) 123 | local x = i3 * i5 assert(x == 15) 124 | local x = i18 /// i5 assert(x == i3) 125 | local x = i18 %%% i5 assert(x == i3) 126 | local x = i2 ^ 1 assert(x == 2) 127 | local x = i2 ^ 2 assert(x == 4) 128 | local x = i2 ^ 3 assert(x == 8) 129 | local x = i2 ^ 5 assert(x == 32) 130 | local x = n2 ^ 2 assert(x == 4) 131 | local x = n2 ^ 3 assert(x == -8) 132 | end 133 | 134 | do -- bitwise shifts 135 | local i15 = BigInt.from(15) 136 | local x = i15 << 0 assert(x == 15) 137 | local x = i15 << 1 assert(x == 30) 138 | local x = i15 << 2 assert(x == 60) 139 | 140 | local x = i15 >> 0 assert(x == 15) 141 | local x = i15 >> 1 assert(x == 7) 142 | local x = i15 >> 2 assert(x == 3) 143 | 144 | local m15 = BigInt.from(-15) 145 | local x = m15 << 0 assert(x == -15) 146 | local x = m15 << 1 assert(x == -30) 147 | local x = m15 << 2 assert(x == -60) 148 | 149 | local n2 = BigInt.from(-2) 150 | local x = n2 >>> 1 assert(x == -1) 151 | end 152 | 153 | do -- bitwise operations 154 | local x = BigInt.bor(0b01010, 0b10010) assert(x == 0b11010) 155 | local x = BigInt.band(0b01010, 0b10010) assert(x == 0b00010) 156 | local x = BigInt.bxor(0b01010, 0b10011) assert(x == 0b11001) 157 | local x = BigInt.bnot(26) assert(x == -27) 158 | end 159 | 160 | do -- tdivrem 161 | local q , r = BigInt.tdivrem(17, 5) 162 | assert(q == 3) assert(r == 2) 163 | end 164 | 165 | do -- powmod 166 | local x = BigInt.powmod(11, 7, 3) assert(x == 2) 167 | end 168 | 169 | do -- invmod 170 | local x = BigInt.invmod(11, 3) assert(x == 2) 171 | end 172 | 173 | do -- gcd 174 | local x = BigInt.gcd(36, 20) assert(x == 4) 175 | end 176 | 177 | do -- lcm 178 | local x = BigInt.lcm(21, 6) assert(x == 42) 179 | end 180 | 181 | do -- abs 182 | local x = BigInt.abs(-1337) assert(x == 1337) 183 | local x = BigInt.abs(1337) assert(x == 1337) 184 | end 185 | 186 | do -- randombytes 187 | math.randomseed(0) 188 | local x: BigInt = BigInt.randombytes(32) 189 | assert(#x == 32) 190 | end 191 | 192 | do -- randomrange 193 | for i=1,10 do 194 | local x: BigInt = BigInt.random(2) 195 | assert(x >= 0 and x <= 1) 196 | end 197 | end 198 | 199 | do -- division and modulo operations 200 | local x = BigInt.tdiv(7, 3) assert(x == 2) 201 | local x = BigInt.tdiv(-7, 3) assert(x == -2) 202 | local x = BigInt.tdiv(7, -3) assert(x == -2) 203 | local x = BigInt.tdiv(-7, -3) assert(x == 2) 204 | local x = BigInt.tdiv(7, 4) assert(x == 1) 205 | local x = BigInt.tdiv(-7, 4) assert(x == -1) 206 | local x = BigInt.tdiv(7, -4) assert(x == -1) 207 | local x = BigInt.tdiv(-7, -4) assert(x == 1) 208 | 209 | local x = BigInt.fdiv(7, 3) assert(x == 2) 210 | local x = BigInt.fdiv(-7, 3) assert(x == -3) 211 | local x = BigInt.fdiv(7, -3) assert(x == -3) 212 | local x = BigInt.fdiv(-7, -3) assert(x == 2) 213 | local x = BigInt.fdiv(7, 4) assert(x == 1) 214 | local x = BigInt.fdiv(-7, 4) assert(x == -2) 215 | local x = BigInt.fdiv(7, -4) assert(x == -2) 216 | local x = BigInt.fdiv(-7, -4) assert(x == 1) 217 | 218 | local x = BigInt.cdiv(7, 3) assert(x == 3) 219 | local x = BigInt.cdiv(-7, 3) assert(x == -2) 220 | local x = BigInt.cdiv(7, -3) assert(x == -2) 221 | local x = BigInt.cdiv(-7, -3) assert(x == 3) 222 | local x = BigInt.cdiv(7, 4) assert(x == 2) 223 | local x = BigInt.cdiv(-7, 4) assert(x == -1) 224 | local x = BigInt.cdiv(7, -4) assert(x == -1) 225 | local x = BigInt.cdiv(-7, -4) assert(x == 2) 226 | 227 | local x = BigInt.rdiv(7, 3) assert(x == 2) 228 | local x = BigInt.rdiv(-7, 3) assert(x == -2) 229 | local x = BigInt.rdiv(7, -3) assert(x == -2) 230 | local x = BigInt.rdiv(-7, -3) assert(x == 2) 231 | local x = BigInt.rdiv(7, 4) assert(x == 2) 232 | local x = BigInt.rdiv(-7, 4) assert(x == -2) 233 | local x = BigInt.rdiv(7, -4) assert(x == -2) 234 | local x = BigInt.rdiv(-7, -4) assert(x == 2) 235 | 236 | local x = BigInt.mod(7, 3) assert(x == 1) 237 | local x = BigInt.mod(-7, 3) assert(x == 2) 238 | local x = BigInt.mod(7, -3) assert(x == 1) 239 | local x = BigInt.mod(-7, -3) assert(x == 2) 240 | 241 | local x = BigInt.tmod(7, 3) assert(x == 1) 242 | local x = BigInt.tmod(-7, 3) assert(x == -1) 243 | local x = BigInt.tmod(7, -3) assert(x == 1) 244 | local x = BigInt.tmod(-7, -3) assert(x == -1) 245 | 246 | local x = BigInt.fmod(7, 3) assert(x == 1) 247 | local x = BigInt.fmod(-7, 3) assert(x == 2) 248 | local x = BigInt.fmod(7, -3) assert(x == -2) 249 | local x = BigInt.fmod(-7, -3) assert(x == -1) 250 | end 251 | -------------------------------------------------------------------------------- /tests/datetime_test.nelua: -------------------------------------------------------------------------------- 1 | local os = require 'os' 2 | local datetime = require 'datetime' 3 | 4 | do print 'conversion' 5 | assert(datetime.iso8601_to_unixmillis'2021-10-31T23:26:18.580Z' == 1635722778580) 6 | assert(datetime.unixmillis_to_iso8601(1635722778580) == '2021-10-31T23:26:18.580Z') 7 | 8 | assert(datetime.iso8601_to_unixmillis'1970-01-01T00:00:00.000Z' == 0) 9 | assert(datetime.unixmillis_to_iso8601(0) == '1970-01-01T00:00:00.000Z') 10 | 11 | assert(datetime.iso8601_to_unixmillis'2000-02-29T23:59:59.999Z' == 951868799999) 12 | assert(datetime.unixmillis_to_iso8601(951868799999) == '2000-02-29T23:59:59.999Z') 13 | end 14 | 15 | do print 'roundtrip' 16 | assert(datetime.iso8601_to_unixmillis(datetime.unixmillis_to_iso8601(1635722778580)) == 1635722778580) 17 | assert(datetime.unixmillis_to_iso8601( 18 | datetime.iso8601_to_unixmillis('2021-10-31T23:26:18.580Z')) == 19 | '2021-10-31T23:26:18.580Z') 20 | assert(datetime.unixmillis_to_iso8601( 21 | datetime.iso8601_to_unixmillis('2021-11-02T11:43:45.850Z')) == 22 | '2021-11-02T11:43:45.850Z') 23 | end 24 | 25 | do print 'unixmillis' 26 | local t1 = os.time()*1000 27 | local t2 = datetime.unixmillis() 28 | assert(t2 - t1 <= 1000) 29 | end 30 | -------------------------------------------------------------------------------- /tests/ffi_module.nelua: -------------------------------------------------------------------------------- 1 | ## pragmas.noentrypoint = true 2 | 3 | local function f42(): cint 4 | return 42 5 | end 6 | -------------------------------------------------------------------------------- /tests/ffi_test.nelua: -------------------------------------------------------------------------------- 1 | require 'nester' 2 | require 'ffi' 3 | 4 | ## if ccinfo.is_windows then 5 | local libpath: cstring = "ffi_module.dll" 6 | ## else 7 | local libpath: cstring = "./ffi_module.so" 8 | ## end 9 | 10 | local lib: ffi 11 | nester.describe("ffi", function() 12 | nester.it("load", function() 13 | lib = ffi.load(libpath) 14 | expect.truthy(lib:isloaded()) 15 | end) 16 | 17 | nester.it("get", function() 18 | local f42 = (@function(): cuint)(lib:get("f42")) 19 | expect.not_equal(f42, nilptr) 20 | expect.equal(f42(), 42) 21 | end) 22 | 23 | nester.it("unload", function() 24 | assert(lib:unload() == true) 25 | assert(lib:unload() == false) 26 | end) 27 | end) 28 | -------------------------------------------------------------------------------- /tests/fs_test.nelua: -------------------------------------------------------------------------------- 1 | require 'nester' 2 | require 'fs' 3 | 4 | nester.describe("fs", function() 5 | -- remove temporary test files 6 | fs.rmdir('tempdir') 7 | fs.rmdir('tempdir2') 8 | fs.rmfile('tempfile') 9 | fs.rmfile('tempfile2') 10 | fs.rmfile('tempfile3') 11 | fs.rmfile('tempfile4') 12 | 13 | nester.it("cwdir, mkdir, isdir, move, rmdir", function() 14 | local pwd: string = fs.cwdir() 15 | local newdir: string = string.format('%s%s%s', pwd, fs.sep, 'tempdir') 16 | local newdir2: string = string.format('%s%s%s', pwd, fs.sep, 'tempdir2') 17 | expect.truthy(fs.chdir(pwd)) 18 | expect.truthy(fs.mkdir(newdir)) 19 | expect.truthy(fs.chdir(newdir)) 20 | local newpwd: string = fs.cwdir() 21 | expect.truthy(newpwd == newdir) 22 | expect.truthy(fs.chdir(pwd)) 23 | expect.truthy(fs.isdir(newdir)) 24 | expect.truthy(fs.move(newdir, newdir2)) 25 | expect.truthy(fs.rmdir(newdir2)) 26 | end) 27 | 28 | nester.it("mkfile, isfile, touch, move, rmfile", function() 29 | expect.truthy(fs.mkfile('tmpfile')) 30 | expect.truthy(fs.isfile('tmpfile')) 31 | expect.truthy(fs.touch('tmpfile')) 32 | expect.truthy(fs.move('tmpfile', 'tmpfile2')) 33 | expect.truthy(fs.rmfile('tmpfile2')) 34 | end) 35 | 36 | nester.it("mkfile, readfile", function() 37 | expect.truthy(fs.mkfile('tmpfile', 'hello!')) 38 | local contents: string = fs.readfile('tmpfile') 39 | expect.truthy(contents == 'hello!') 40 | end) 41 | 42 | nester.it("stat", function() 43 | local si: fs.StatInfo = fs.stat('tmpfile') 44 | expect.truthy(si.kind == fs.StatKind.FILE and si.size > 0) 45 | end) 46 | 47 | nester.it("symbolic link", function() 48 | expect.truthy(fs.mklink('tmpfile', 'tmpfile2')) 49 | expect.truthy(fs.stat('tmpfile2').kind == fs.StatKind.FILE) 50 | ## if not ccinfo.is_windows then 51 | expect.truthy(fs.stat('tmpfile2', true).kind == fs.StatKind.LINK) 52 | ## end 53 | local contents: string = fs.readfile('tmpfile2') 54 | expect.truthy(contents == 'hello!') 55 | local linktarget: string = fs.readlink('tmpfile2') 56 | ## if not ccinfo.is_windows then 57 | expect.truthy(linktarget == 'tmpfile') 58 | ## end 59 | expect.truthy(fs.rmfile('tmpfile2')) 60 | end) 61 | 62 | nester.it("hard link", function() 63 | expect.truthy(fs.mklink('tmpfile', 'tmpfile3', true)) 64 | local contents: string = fs.readfile('tmpfile3') 65 | expect.truthy(contents == 'hello!') 66 | expect.truthy(fs.stat('tmpfile3').kind == fs.StatKind.FILE) 67 | ## if not ccinfo.is_windows then 68 | expect.truthy(fs.stat('tmpfile3', true).kind == fs.StatKind.LINK) 69 | ## end 70 | expect.truthy(fs.rmfile('tmpfile3')) 71 | end) 72 | 73 | nester.it("chown, chmod", function() 74 | ## if not ccinfo.is_windows then 75 | local si: fs.StatInfo = fs.stat('tmpfile') 76 | expect.truthy(fs.chown('tmpfile', si.uid, si.gid)) 77 | expect.truthy(fs.chmod('tmpfile', si.mode)) 78 | ## end 79 | end) 80 | 81 | nester.it("cpfile", function() 82 | fs.cpfile('tmpfile', 'tmpfile4') 83 | local contents: string = fs.readfile('tmpfile4') 84 | expect.truthy(contents == 'hello!') 85 | expect.truthy(fs.rmfile('tmpfile4')) 86 | end) 87 | 88 | -- remove remaining temporary test files 89 | fs.rmfile('tmpfile') 90 | end) -------------------------------------------------------------------------------- /tests/inspector_test.nelua: -------------------------------------------------------------------------------- 1 | require 'string' 2 | require 'inspector' 3 | require 'nester' 4 | 5 | local function fn_hex_ptr(callback: auto) 6 | local cb_str = tostring(callback) 7 | return cb_str:gsub('function: ', '', 1) 8 | end 9 | 10 | nester.describe("inspector", function() 11 | nester.describe("simple", function() 12 | nester.it("booleans", function() 13 | local result = inspector(true) 14 | expect.equal(result, "true") 15 | 16 | local result = inspector(false) 17 | expect.equal(result, "false") 18 | end) 19 | 20 | nester.it("integers", function() 21 | local result = inspector(10) 22 | expect.equal(result, "10_i64") 23 | end) 24 | 25 | nester.it("numbers", function() 26 | local result = inspector(20.20) 27 | expect.equal(result, "20.2_f64") 28 | 29 | local result = inspector(30_number) 30 | expect.equal(result, "30.0_f64") 31 | end) 32 | 33 | nester.it("strings", function() 34 | local result = inspector("hello world!") 35 | expect.equal(result, '"hello world!"') 36 | 37 | local result = inspector("hello world!"_cstring) 38 | expect.equal(result, '"hello world!" --[[ @cstring ]]') 39 | end) 40 | end) 41 | 42 | nester.it("enums", function() 43 | local Weeks = @enum{ 44 | Sunday = 0, 45 | Monday, 46 | Tuesday, 47 | Wednesday, 48 | Thursday, 49 | Friday, 50 | Saturday 51 | } 52 | 53 | local ByteWeeks = @enum(byte){ 54 | Sunday = 0, 55 | Monday, 56 | Tuesday, 57 | Wednesday, 58 | Thursday, 59 | Friday, 60 | Saturday 61 | } 62 | 63 | local result = inspector(Weeks.Tuesday) 64 | expect.equal(result, "Weeks.Tuesday --[[ 2_i64 ]]") 65 | 66 | local result = inspector(ByteWeeks.Friday) 67 | expect.equal(result, "ByteWeeks.Friday --[[ 5_u8 ]]") 68 | end) 69 | 70 | nester.describe("arrays", function() 71 | nester.it("boolean arrays", function() 72 | local result = inspector((@array(boolean, 3)){true, false, false}) 73 | expect.equal(result, "(@array(boolean, 3)){ true, false, false }") 74 | 75 | -- using syntax sugar 76 | local result = inspector((@[2]boolean){false, true}) 77 | expect.equal(result, "(@array(boolean, 2)){ false, true }") 78 | end) 79 | 80 | nester.it("scalar arrays", function() 81 | local result = inspector((@array(uint16, 3)){3, 4, 5}) 82 | expect.equal(result, "(@array(uint16, 3)){ 3, 4, 5 }") 83 | 84 | -- using syntax sugar 85 | local result = inspector((@[2]integer){1, 2}) 86 | expect.equal(result, "(@array(int64, 2)){ 1, 2 }") 87 | 88 | -- inferred size 89 | local result = inspector((@[]number){-3, -2}) 90 | expect.equal(result, "(@array(float64, 2)){ -3.0, -2.0 }") 91 | end) 92 | end) 93 | 94 | nester.describe("records", function() 95 | nester.it("simple", function() 96 | local SimpleRec = @record{ 97 | a: integer, 98 | b: number 99 | } 100 | 101 | local result = inspector((@SimpleRec){ 10, 20.20 }) 102 | expect.equal(result, [==[ 103 | (@SimpleRec){ 104 | a = 10_i64, 105 | b = 20.2_f64, 106 | }]==]) 107 | 108 | local SimpleArrRec = @record{ 109 | a: [3]integer, 110 | } 111 | 112 | local result = inspector((@SimpleArrRec){ a = { 1, 2, 3 } }) 113 | expect.equal(result, [==[ 114 | (@SimpleArrRec){ 115 | a = (@array(int64, 3)){ 1, 2, 3 }, 116 | }]==]) 117 | end) 118 | 119 | nester.it("nested records", function() 120 | local InnerRecord = @record{ 121 | a: integer, 122 | } 123 | 124 | local OuterRecord = @record{ 125 | ir: InnerRecord, 126 | b: [2]integer 127 | } 128 | 129 | local result = inspector((@OuterRecord){ ir = { a = 10 }, b = {20, 30} }) 130 | expect.equal(result, [==[ 131 | (@OuterRecord){ 132 | ir = (@InnerRecord){ 133 | a = 10_i64, 134 | }, 135 | b = (@array(int64, 2)){ 20, 30 }, 136 | }]==]) 137 | end) 138 | 139 | nester.it("record arrays", function() 140 | local MyRec = @record{ 141 | b: byte, 142 | i: int16 143 | } 144 | 145 | local result = inspector((@[2]MyRec){ 146 | { b = 1, i = 10}, 147 | { b = 2, i = 20}, 148 | }) 149 | 150 | expect.equal(result, [==[ 151 | (@array(MyRec, 2)){ { 152 | b = 1_u8, 153 | i = 10_i16, 154 | }, { 155 | b = 2_u8, 156 | i = 20_i16, 157 | } }]==]) 158 | end) 159 | 160 | nester.it("pointers", function() 161 | local a: *int32 = (@*int32)(0x1234) 162 | 163 | local result = inspector(a) 164 | expect.equal(result, '0x1234 --[[ @pointer(int32) ]]') 165 | 166 | local result = inspector(nilptr) 167 | expect.equal(result, 'nilptr') 168 | 169 | local n: *int16 = nilptr 170 | local result = inspector(n) 171 | expect.equal(result, 'nilptr --[[ @pointer(int16) ]]') 172 | 173 | local RecOfPointers = @record{ 174 | a: *int32, 175 | b: string, 176 | c: *[2]integer, 177 | d: [2]*integer, 178 | } 179 | 180 | local value: RecOfPointers = { 181 | =a, 182 | b = 'hello world!', 183 | c = (@*[2]integer)(0x56789), 184 | d = { (@*integer)(0x1010), (@*integer)(0x2020) }, 185 | } 186 | 187 | local result = inspector(value) 188 | expect.equal(result, [==[ 189 | (@RecOfPointers){ 190 | a = 0x1234 --[[ @pointer(int32) ]], 191 | b = "hello world!", 192 | c = 0x56789 --[[ @pointer(array(int64, 2)) ]], 193 | d = (@array(pointer(int64), 2)){ 0x1010 --[[ @pointer(int64) ]], 0x2020 --[[ @pointer(int64) ]] }, 194 | }]==]) 195 | end) 196 | end) 197 | 198 | nester.describe("functions", function() 199 | local function foo(a: integer, b: integer): integer 200 | return a + b 201 | end 202 | 203 | local function bar(s: string) 204 | print(s) 205 | end 206 | 207 | local function tos(v: auto): string 208 | return tostring(v) 209 | end 210 | 211 | nester.it("simple", function() 212 | local result = inspector(foo) 213 | expect.equal(result, 'function(a: int64, b: int64): int64') 214 | end) 215 | 216 | nester.it("polymorphic", function() 217 | local result = inspector(tos) 218 | expect.equal(result, 'polyfunction(v: auto): string') 219 | end) 220 | 221 | nester.it("record of callbacks", function() 222 | local Callbacks = @record{ 223 | callback_add: function(a: integer, b: integer): integer, 224 | callback_print: function(string), 225 | } 226 | local cb: Callbacks = {} 227 | 228 | local result = inspector(cb) 229 | expect.equal(result, [==[ 230 | (@Callbacks){ 231 | callback_add = nilptr --[[ function(a: int64, b: int64): int64 ]], 232 | callback_print = nilptr --[[ function(string): void ]], 233 | }]==]) 234 | 235 | cb.callback_add = foo 236 | cb.callback_print = bar 237 | 238 | local a_str = fn_hex_ptr(cb.callback_add) 239 | local p_str = fn_hex_ptr(cb.callback_print) 240 | local expected_str = string.format([==[ 241 | (@Callbacks){ 242 | callback_add = %s --[[ function(a: int64, b: int64): int64 ]], 243 | callback_print = %s --[[ function(string): void ]], 244 | }]==], a_str, p_str) 245 | 246 | local result = inspector(cb) 247 | expect.equal(result, expected_str) 248 | end) 249 | 250 | nester.it("array of callbacks", function() 251 | local arr: [2]function(string) = { bar, bar } 252 | local bar_s = fn_hex_ptr(bar) 253 | 254 | local result = inspector(arr) 255 | local expected_str = string.format('(@array(function(string): void, 2)){ %s, %s }', bar_s, bar_s ) 256 | expect.equal(result, expected_str) 257 | end) 258 | end) 259 | 260 | nester.it("unions", function() 261 | local MyRec1 = @record{ a: integer } 262 | local MyRec2 = @record{ a: string } 263 | 264 | local MyUnion = @union{ 265 | v1: MyRec1, 266 | v2: MyRec2, 267 | } 268 | 269 | local value: MyUnion = {v1 = {1} } 270 | local result = inspector(value) 271 | expect.equal(result, '(@MyUnion)() --[[ @union{v1: MyRec1, v2: MyRec2} ]]') 272 | 273 | local result = inspector(value.v1) 274 | expect.equal(result, [==[ 275 | (@MyRec1){ 276 | a = 1_i64, 277 | }]==]) 278 | end) 279 | 280 | nester.describe("types (typedesc)", function() 281 | local MyRec = @record{ b: byte } 282 | local MyUnion = @union{ n: integer, m: MyRec } 283 | local MyEnum = @enum{ Foo = 0, Bar } 284 | local MyArr = @array(MyEnum, 2) 285 | 286 | nester.it('record', function() 287 | local result = inspector(MyRec) 288 | expect.equal(result, '@record{b: uint8}') 289 | end) 290 | 291 | nester.it('union', function() 292 | local result = inspector(MyUnion) 293 | expect.equal(result, '@union{n: int64, m: MyRec}') 294 | end) 295 | 296 | nester.it('enum', function() 297 | local result = inspector(MyEnum) 298 | expect.equal(result, '@enum(int64){Foo=0, Bar=1}') 299 | end) 300 | 301 | nester.it('array', function() 302 | local result = inspector(MyArr) 303 | expect.equal(result, '@array(MyEnum, 2)') 304 | end) 305 | end) 306 | 307 | nester.it("complex data-type", function() 308 | local n: uint8 = 5 309 | local E = @enum{ Foo = 0, Bar } 310 | local U = @union{ n: uint8, e: E } 311 | local function fn(i: integer): number return i + 1 end 312 | 313 | local ComplexRec = @record{ 314 | a: int16, 315 | b: record{ x: float32, y: boolean }, 316 | c: *uint8, 317 | d: [2]int32, 318 | e: E, 319 | cb: function(i: integer): number, 320 | u: U, 321 | } 322 | 323 | local value: [2]ComplexRec = { 324 | { 325 | a = 8, 326 | b = { 10.25, true }, 327 | c = &n, 328 | d = { 1, 2 }, 329 | e = E.Bar, 330 | cb = fn, 331 | u = { n = 1 } 332 | }, 333 | {} 334 | } 335 | 336 | local fn_hex = fn_hex_ptr(fn) 337 | 338 | local result = inspector(value) 339 | local expected_str = string.format([==[ 340 | (@array(ComplexRec, 2)){ { 341 | a = 8_i16, 342 | b = (@record{x: float32, y: boolean}){ 343 | x = 10.25_f32, 344 | y = true, 345 | }, 346 | c = %s --[[ @pointer(uint8) ]], 347 | d = (@array(int32, 2)){ 1, 2 }, 348 | e = E.Bar --[[ 1_i64 ]], 349 | cb = %s --[[ function(i: int64): float64 ]], 350 | u = (@U)() --[[ @union{n: uint8, e: E} ]], 351 | }, { 352 | a = 0_i16, 353 | b = (@record{x: float32, y: boolean}){ 354 | x = 0.0_f32, 355 | y = false, 356 | }, 357 | c = nilptr --[[ @pointer(uint8) ]], 358 | d = (@array(int32, 2)){ 0, 0 }, 359 | e = E.Foo --[[ 0_i64 ]], 360 | cb = nilptr --[[ function(i: int64): float64 ]], 361 | u = (@U)() --[[ @union{n: uint8, e: E} ]], 362 | } }]==], &n, fn_hex) 363 | expect.equal(result, expected_str) 364 | end) 365 | end) 366 | -------------------------------------------------------------------------------- /tests/json_test.nelua: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'vector' 3 | require 'hashmap' 4 | require 'nester' 5 | 6 | local Person = @record{name: string, age: integer} 7 | local String: type = @record{s: string} 8 | 9 | nester.describe("json", function() 10 | nester.it("primitives", function() 11 | local Foo: type = @record{ 12 | i: integer, 13 | inull: integer, 14 | n: number, 15 | s: string, 16 | snull: string, 17 | bt: boolean, 18 | bf: boolean, 19 | bmissing: boolean, 20 | } 21 | local primjson: string = [[{ 22 | "i": 1337, 23 | "inull": null, 24 | "n": 0.5, 25 | "s": "test", 26 | "snull": null, 27 | "bt": true, 28 | "bf": false, 29 | }]] 30 | local foo: Foo, err: string, len: usize = json.parse(primjson, @Foo) 31 | assert(#err == 0, err) 32 | expect.equal(foo.i, 1337) 33 | expect.equal(foo.inull, 0) 34 | expect.equal(foo.n, 0.5) 35 | expect.equal(foo.s, "test") 36 | expect.equal(foo.snull, "") 37 | expect.equal(foo.bt, true) 38 | expect.equal(foo.bf, false) 39 | expect.equal(len, #primjson) 40 | expect.equal(#err, 0) 41 | expect.equal(json.emit(foo), [[{"i":1337,"inull":0,"n":0.5,"s":"test","snull":"","bt":true,"bf":false,"bmissing":false}]]) 42 | end) 43 | 44 | nester.it("record", function() 45 | local p: Person, err: string, len: usize = json.parse("{}", @Person) 46 | expect.truthy(p.name == "" and p.age == 0 and #err == 0 and len == 2) 47 | expect.equal(json.emit(p), [[{"name":"","age":0}]]) 48 | 49 | p, err = json.parse([[{"name":"John","age":20}]], @Person) 50 | expect.truthy(p.name == 'John' and p.age == 20 and #err == 0) 51 | expect.equal(json.emit(p), [[{"name":"John","age":20}]]) 52 | 53 | p, err = json.parse([[{"name":"John"}]], @Person) 54 | expect.truthy(p.name == 'John' and p.age == 00 and #err == 0) 55 | expect.equal(json.emit(p), [[{"name":"John","age":0}]]) 56 | end) 57 | 58 | nester.it("vector", function() 59 | local v: vector(integer), err: string, len: usize = json.parse("[]", @vector(integer)) 60 | expect.truthy(#v == 0 and #err == 0 and len == 2) 61 | expect.equal(json.emit(v), "[]") 62 | 63 | v, err = json.parse("[1]", @vector(integer)) 64 | expect.truthy(#v == 1 and v[0] == 1 and #err == 0) 65 | expect.equal(json.emit(v), "[1]") 66 | 67 | v, err = json.parse("[1,2]", @vector(integer)) 68 | expect.truthy(#v == 2 and v[0] == 1 and v[1] == 2 and #err == 0) 69 | expect.equal(json.emit(v), "[1,2]") 70 | 71 | v, err = json.parse("[1,2,3,]", @vector(integer)) 72 | expect.truthy(#v == 3 and v[0] == 1 and v[1] == 2 and v[2] == 3 and #err == 0) 73 | expect.equal(json.emit(v), "[1,2,3]") 74 | end) 75 | 76 | nester.it("vector with record", function() 77 | local v: vector(Person) = expect.assert(json.parse([[ [ 78 | {"name": "John", "age": 20}, 79 | {"name": "Paul", "age": 21}, 80 | ] ]], @vector(Person))) 81 | expect.truthy(#v == 2) 82 | expect.truthy(v[0].name == 'John' and v[0].age == 20) 83 | expect.truthy(v[1].name == 'Paul' and v[1].age == 21) 84 | expect.equal(json.emit(v), '[{"name":"John","age":20},{"name":"Paul","age":21}]') 85 | end) 86 | 87 | nester.it("hashmap", function() 88 | local m: hashmap(string, string) = expect.assert(json.parse([[{ 89 | "key1": "value1", 90 | "key2": "value2", 91 | }]], @hashmap(string, string))) 92 | expect.truthy(#m == 2) 93 | expect.truthy(m["key1"] == "value1") 94 | expect.truthy(m["key2"] == "value2") 95 | expect.equal(json.emit(m), '{"key1":"value1","key2":"value2"}') 96 | end) 97 | 98 | nester.it("nested records", function() 99 | local Boo = @record{ 100 | x: integer 101 | } 102 | local NestedFoo = @record{ 103 | boo: Boo, 104 | vboos: vector(Boo), 105 | mboos: hashmap(string, Boo), 106 | } 107 | local foo: NestedFoo = expect.assert(json.parse([[{ 108 | "boo": {"x":1}, 109 | "vboos": [{"x":2}, {"x":3}], 110 | "mboos": {"first":{"x":4}, "second":{"x":3}} 111 | }]], @NestedFoo)) 112 | expect.truthy(foo.boo.x == 1) 113 | expect.truthy(foo.vboos[0].x == 2) 114 | expect.truthy(foo.vboos[1].x == 3) 115 | expect.equal(json.emit(foo), '{"boo":{"x":1},"vboos":[{"x":2},{"x":3}],"mboos":{"first":{"x":4},"second":{"x":3}}}') 116 | end) 117 | 118 | nester.it("escape sequence", function() 119 | expect.equal(expect.assert(json.parse([[{ 120 | "s": "\"\\\/\b\t\n\f\r", 121 | }]], String)).s, "\"\\/\b\t\n\f\r") 122 | end) 123 | 124 | nester.it("utf8 escape", function() 125 | expect.equal(expect.assert(json.parse([[{ 126 | "s": "\u0000\uFFFF\u007f\u00f8", 127 | }]], String)).s, "\u{0000}\u{FFFF}\u{007f}\u{00f8}") 128 | end) 129 | 130 | nester.it("inline record type", function() 131 | local p = expect.assert(json.parse([[{ 132 | "name": "John", 133 | "age": 20 134 | }]], @record{ 135 | name: string, 136 | age: integer 137 | })) 138 | assert(p.name == 'John' and p.age == 20) 139 | 140 | expect.equal(json.emit((@record{ 141 | name: string, 142 | age: integer 143 | }){ 144 | name = 'John', 145 | age = 20 146 | }), '{"name":"John","age":20}') 147 | end) 148 | end) 149 | -------------------------------------------------------------------------------- /tests/nester_test.nelua: -------------------------------------------------------------------------------- 1 | require 'nester' 2 | 3 | -- Customize nester configuration. 4 | nester.stop_on_fail = false 5 | 6 | nester.describe('nester', function() 7 | nester.before(function(name: string) 8 | -- This function is run before every test. 9 | end) 10 | 11 | nester.describe('module1', function() -- Describe blocks can be nested. 12 | nester.it('feature1', function() 13 | expect.equal('something', 'something') -- Pass. 14 | end) 15 | 16 | nester.it('feature2', function() 17 | expect.error('TEST FAILED, BUT THIS IS EXPECTED') -- Fail. 18 | end) 19 | end) 20 | end) 21 | 22 | nester.report() -- Print overall statistic of the tests run. 23 | -- nester.exit() -- Exit with success if all tests passed. 24 | -------------------------------------------------------------------------------- /tests/signal_test.nelua: -------------------------------------------------------------------------------- 1 | local signal = require 'signal' 2 | 3 | local got_signal: boolean 4 | 5 | do print 'setsignal/raise' 6 | assert(signal.setsignal(signal.SIGTERM, function(signum: cint) 7 | got_signal = true 8 | end)) 9 | assert(signal.raise(signal.SIGTERM)) 10 | assert(got_signal == true) 11 | end 12 | -------------------------------------------------------------------------------- /tests/sqlite3_test.nelua: -------------------------------------------------------------------------------- 1 | require 'nester' 2 | require 'os' 3 | 4 | local SQLite3 = require 'sqlite3' 5 | 6 | local db: SQLite3, errcode: SQLite3.Error, errmsg: string 7 | 8 | nester.describe("sqlite3", function() 9 | nester.it("new database", function() 10 | db, errmsg, errcode = SQLite3.open('testdb.sqlite3') 11 | expect.equal(errcode, SQLite3.Error.OK) 12 | expect.truthy(db:isopen()) 13 | expect.equal(db:close(), true) 14 | 15 | db, errmsg, errcode = SQLite3.open('testdb.sqlite3', SQLite3.OpenFlags.READWRITE) 16 | expect.equal(errcode, SQLite3.Error.OK) 17 | expect.equal(db:close(), true) 18 | end) 19 | 20 | nester.before(function(name: string) 21 | db, errmsg, errcode = SQLite3.open('testdb.sqlite3') 22 | assert(db:isopen(), errcode) 23 | end) 24 | 25 | nester.after(function(name: string) 26 | db:close() 27 | end) 28 | 29 | nester.it("exec", function() 30 | db:exec[[ 31 | DROP TABLE IF EXISTS names; 32 | CREATE TABLE IF NOT EXISTS names ( 33 | id INTEGER PRIMARY KEY, 34 | name TEXT NOT NULL 35 | ); 36 | INSERT INTO names (name) VALUES ('John'); 37 | INSERT INTO names (name) VALUES ('Paul'); 38 | ]] 39 | local count: integer = 0 40 | db:exec([[ 41 | SELECT * FROM names; 42 | ]], function(udata: pointer, ncols: cint, values: *[0]cstring, names: *[0]cstring): cint 43 | local count: *integer = (@*integer)(udata) 44 | $count = $count + 1 45 | if $count == 1 then 46 | expect.equal(values[0], '1') 47 | expect.equal(values[1], 'John') 48 | else 49 | expect.equal(values[0], '2') 50 | expect.equal(values[1], 'Paul') 51 | end 52 | expect.equal(names[0], 'id') 53 | expect.equal(names[1], 'name') 54 | return 0 55 | end, &count) 56 | expect.equal(count, 2) 57 | end) 58 | 59 | nester.it("urows", function() 60 | local count = 0 61 | for id, name in db:urows("SELECT id, name FROM names", @int64, @string) do 62 | if id == 1 then 63 | expect.equal(name, 'John') 64 | elseif id == 2 then 65 | expect.equal(name, 'Paul') 66 | end 67 | count = count + 1 68 | end 69 | expect.equal(count, 2) 70 | end) 71 | 72 | nester.it("urow", function() 73 | local ok: boolean, id: int64 = db:urow("SELECT id FROM names ORDER BY id LIMIT 1", @int64) 74 | expect.truthy(ok) 75 | expect.equal(id, 1) 76 | end) 77 | 78 | os.remove('testdb.sqlite3') 79 | end) 80 | -------------------------------------------------------------------------------- /tuple.nelua: -------------------------------------------------------------------------------- 1 | ## local function make_tupleT(...) 2 | local tupleT: type = @record{} 3 | ##[[ 4 | for i=1,select('#',...) do 5 | local fieldname = 'e'..(i-1) 6 | local fieldtype = select(i, ...) 7 | tupleT.value:add_field(fieldname, fieldtype) 8 | end 9 | ]] 10 | 11 | function tupleT.__atindex(self: *tupleT, index: integer ): auto 12 | return &self.#|'e'..index.value|#; 13 | end 14 | 15 | ## return tupleT 16 | ## end 17 | 18 | global tuple: type = #[generalize(make_tupleT)]# 19 | 20 | function tuple.pack(...: varargs): auto 21 | return (@tuple(#[aster.unpack(types.nodes_to_types{...})]#)){...} 22 | end 23 | 24 | function tuple.unpack(t: auto) 25 | ##[[ 26 | local rets = {} 27 | for _,field in ipairs(t.type.fields) do 28 | table.insert(rets, aster.DotIndex{field.name, aster.Id{'t'}}) 29 | end 30 | ]] 31 | return #[aster.unpack(rets)]# 32 | end 33 | 34 | return tuple 35 | --------------------------------------------------------------------------------