├── LICENSE ├── README.md ├── assert.lua ├── class.lua ├── cmdline.lua ├── coroutine.lua ├── ctypes.lua ├── debug.lua ├── detect_ffi.lua ├── detect_lfs.lua ├── detect_os.lua ├── distinfo ├── env.lua ├── ext.lua ├── ext.rockspec ├── fromlua.lua ├── gc.lua ├── gcmem.lua ├── io.lua ├── load.lua ├── math.lua ├── meta.lua ├── number.lua ├── op.lua ├── os.lua ├── path.lua ├── range.lua ├── reload.lua ├── require.lua ├── string.lua ├── table.lua ├── timer.lua ├── tolua.lua └── xpcall.lua /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-2025 Christopher E. Moore ( christopher.e.moore@gmail.com / http://thenumbernine.github.io ) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Lua Extension Library 2 | 3 | [![Donate via Stripe](https://img.shields.io/badge/Donate-Stripe-green.svg)](https://buy.stripe.com/00gbJZ0OdcNs9zi288)
4 | 5 | I found myself recreating so many extensions to the base lua classes over and over again. 6 | I thought I'd just put them in one place. 7 | I'm sure this will grow out of hand. 8 | 9 | Note to users: The structure of the source code doesn't exactly match the structure in the rock install destination. 10 | This is because I personally use a `LUA_PATH` pattern of "?/?.lua" in addition to the typical "?.lua". 11 | To work with compatability of everyone else who does not use this convention, I have the rockspec install `ext/ext.lua` into `ext.lua` and keep `ext/everything_else.lua` at `ext/everything_else.lua`. 12 | 13 | TLDR, how to set up your environment, choose one: 14 | - Install the rock and don't worry. 15 | - Move ext.lua into the parent directory. 16 | - Add "?/?.lua" to your `LUA_PATH`. 17 | 18 | 19 | Descriptions of the Files: 20 | 21 | ### assert.lua 22 | 23 | Returns an object that can be used as a typical `assert()` call, but also contains several functions that can be used for testing and outputting specific assert cases: 24 | 25 | `assert.eq(a, b, [msg, ...])` = If `a == b` returns all arguments. Otherwise error with message specifying `a`, `b`, and optionally `msg`. 26 | 27 | `assert.eqeps(a, b, [eps, msg, ...])` = If `|a-b|<=eps` returns all arguments. Otherwise error with message specifying `a`, `b`, `eps`, and optionally `msg`. Default `eps` is `1e-7`. 28 | 29 | `assert.eqepsnorm(a, b, [eps, norm, msg, ...])` = If `norm(a,b)<=eps` returns all arguments. Otherwise error with message specifying `a`, `b`, `eps`, and optionally `msg`. Default `eps` is `1e-7`, and default `norm(a,b)` is `math.abs(a - b)`. 30 | 31 | `assert.ne(a, b, [msg, ...])` = If `a ~= b` returns all arguments. Otherwise error. 32 | 33 | `assert.ge(a, b, [msg, ...])` = If `a >= b` returns all arguments. Otherwise error. 34 | 35 | `assert.gt(a, b, [msg, ...])` = If `a > b` returns all arguments. Otherwise error. 36 | 37 | `assert.le(a, b, [msg, ...])` = If `a <= b` returns all arguments. Otherwise error. 38 | 39 | `assert.lt(a, b, [msg, ...])` = If `a < b` returns all arguments. Otherwise error. 40 | 41 | `assert.len(t, n, [msg, ...])` = If `#t == n` returns all arguments. Otherwise error. 42 | 43 | `assert.type(x, t, [msg, ...])` = If `type(x) == t` returns all arguments. Otherwise error. 44 | 45 | `assert.types(msg, n, ...)` = Assumes `...` holds `n` types and then `n` variables. If all the `[n+1,2*n]` variables' types match the `[1,n]` types then it returns all the variables. Otherwise error. 46 | 47 | `assert.index(t, k, [msg, ...])` = If `t[k]` evaluates to true then returns `t[k], msg, ...`. Otherwise error. 48 | 49 | `assert.tableieq(t1, t2, [msg, ...])` = If `#t1 == #t2` and `t1[i] == t2[i]` then returns all arguments. Otherwise error. 50 | 51 | `assert.error(f, [msg, ...])` = If calling `f(...)` does not produce an error then this errors. If calling `f(...)` does error then this returns the error message. 52 | 53 | `assert.is(o, cl, [msg, ...])` = If object `o` is an instance of class `cl` then returns all arguments. Otherwise error. 54 | 55 | ### ext.lua 56 | 57 | `require`ing this file sets up global environment via ext.env and overrides metatables to all default Lua types via ext.meta. 58 | I would not recommend `require`ing this directly in any large scale projects, as it changes many default operations of Lua. 59 | It is much more useful for small-scale scripts. 60 | 61 | ### env.lua 62 | 63 | This file adds all tables to the specified Lua environment table (`_G` by default). 64 | It also sets _ to os.execute for shorthand shell scripting. 65 | 66 | ### class.lua: 67 | 68 | `class(parent1, parent2, ...)` = create a 'class' table, fill it with the union of the table 'parent1', 'parent2', etc. 69 | 70 | Class tables can instanciate object tables using the `\_\_call` operator: 71 | 72 | ``` lua 73 | Cl = class() 74 | obj = Cl(arg1, arg2, ...) 75 | ``` 76 | 77 | Upon construction of an object, the `Cl:init()` method is called, with arguments `arg1, arg2, ...` passed to it. 78 | 79 | Example: 80 | 81 | ``` lua 82 | Cl = class() 83 | function Cl:init(arg) 84 | self.x = arg 85 | end 86 | obj = Cl(42) 87 | assert(obj.x == 42) 88 | ``` 89 | 90 | Within this method, `self` is the object being constructed, so no values need to be returned. 91 | If `Cl:init()` does return any values then it will be returned by the constructor after the object. 92 | 93 | Example: 94 | 95 | ``` lua 96 | Cl = class() 97 | function Cl:init() 98 | return 123 99 | end 100 | obj, extra = Cl() 101 | assert(extra == 123) 102 | ``` 103 | 104 | Class tables have the following fields: 105 | 106 | - `Cl.super` = specifies whatever table was passed to parent1, nil if none was. 107 | - `Cl.supers` = specifies the table of all tables passed to parent1. 108 | 109 | Class tables have the following methods: 110 | 111 | - `Cl:isa(obj)` = returns `true` if obj is a table, and is an instance of class `Cl`. 112 | 113 | - `Cl:subclass(...)` = create a subclass of `Cl` and of any other tables specified in `...`. 114 | 115 | Notice that the object's metatable is the class table. This means that any metamethods defined in the class table are functional in the object table: 116 | 117 | ``` lua 118 | Cl = class() 119 | function Cl.__add(a,b) 120 | return 'abc' 121 | end 122 | obj1, obj2 = Cl(), Cl() 123 | assert(obj1 + obj2 == 'abc') 124 | ``` 125 | 126 | ### coroutine.lua 127 | 128 | `coroutine.assertresume(thread, ...)` = This is just like `coroutine.resume` except, upon failure, it automatically includes a stack trace in the error message. 129 | 130 | ### io.lua 131 | 132 | `io.readfile(path)` = Returns the contents of the file as a string. If an error exists, returns `false` and the error. 133 | 134 | `io.writefile(path, data)` = Writes the string in `data` to the file. If an error occurs, returns `false` and the error, otherwise returns true. 135 | 136 | `io.appendfile(path, data)` = Appends the string in `data` to the file. If an error occurs, returns `false` and the error, otherwise returns true. 137 | 138 | `io.readproc(command)` = Runs a process, reads the entirety of its output, returns the output. If an error occurs, returns `false` and the error. 139 | 140 | `local dirname, filename = io.getfiledir(path)` = Returns the directory and the file name of the file at the specified path. 141 | 142 | `local pathWithoutExtension, extension = io.getfileext(path)` = Returns the filename up to the extension (without the dot) and the extension. 143 | 144 | `file:lock()` = Shorthand for `lfs.lock(filehandle)`. 145 | 146 | `file:unlock()` = Shorthand for `lfs.unlock(filehandle)`. 147 | 148 | ### math.lua 149 | 150 | `math.nan` = not-a-number. `math.nan ~= math.nan`. 151 | 152 | `math.e` = Euler's natural base. 153 | 154 | `math.atan2` = for Lua version compatability, if this isn't defined then it will be mapped to `math.atan`. 155 | 156 | `math.clamp(x, min, max)` = Returns min if x is less than min, max if x is less than max, or x if x is between the two. 157 | 158 | `math.sign(x)` = Returns the sign of x. Note that this is using the convention that `math.sign(0) = 0`. 159 | 160 | `math.trunc(x)` = Truncates x, rounding it towards zero. 161 | 162 | `math.round(x)` = Rounds x towards the nearest integer. 163 | 164 | `math.isnan(x)` = Returns true if x is not-a-number. 165 | 166 | `math.isinf(x)` 167 | `math.isinfinite(x)` = Returns true if x is infinite. 168 | 169 | `math.isprime(x)` = Returns true if x is a prime number. 170 | 171 | `math.factors(x)` = Returns a table of the distinct factors of x. 172 | 173 | `math.primeFactorization(x)` = Returns a table of the prime factorization of x. 174 | 175 | `math.cbrt(x)` = Returns the cube root of x. 176 | 177 | ### os.lua 178 | 179 | `os.execute(...)` = Attempts to fix the compatability of `os.execute` for Lua 5.1 to match that of Lua 5.2+. 180 | 181 | `os.exec(...)` = Prints the command executed, then performs `os.execute`. 182 | 183 | `os.fileexists(path)` = Returns true/false whether the associated file exists. 184 | 185 | `os.isdir(path)` = Returns true if the file at `path` is a directory. 186 | 187 | `os.listdir(path)` = Return an iterator that iterates through all files in a directory. 188 | 189 | Example: 190 | ``` lua 191 | for fn in os.listdir('.') do 192 | print(fn) 193 | end 194 | ``` 195 | 196 | `os.rlistdir(path, callback)` = Returns an iterator that recursively iterates through all files in a directory tree. If `callback` is specified then it is called for each file, and if the callback returns false then the file is not returned, or the sub-directory is not traversed. 197 | 198 | `os.mkdir(path[, createParents])` = Create directory. Set createParents to `true` to create parents as well. 199 | 200 | `os.rmdir(path)` = Removes directory. 201 | 202 | `os.move(src, dst)` = Move file from src to dst. 203 | 204 | `os.home()` = Returns the home directory path. Queries environment variable HOME or USERPROFILE. 205 | 206 | ### string.lua 207 | 208 | Don't forget that - just as with vanilla Lua - all of these are operable via Lua string metamethods: `("a b c"):split(" ")` gives you `{'a', 'b', 'c'}`. 209 | 210 | `string.concat(...)` = `tostring`'s and then `concat`'s all the arguments. 211 | 212 | `string.split(str, sep)` = Splits a string, returning a table of the pieces separated by `sep`. If `sep` is not provided then the string is split into individual characters. 213 | 214 | `string.trim(str)` = Returns the string with the whitespace at the beginning and end removed. 215 | 216 | `string.bytes(s)` = Returns a table containing the numeric byte values of the characters of the string. 217 | 218 | `string.load(str)` = Shorthand for `load` or `loadstring`. Returns a function of the compiled Lua code, or false and any errors. 219 | 220 | `string.csub(str, start, size)` = Returns a substring, where `start` is 0-based and `size` is the length of the substring. 221 | 222 | `string.hexdump(str, columnLength, hexWordSize, spaceEveryNColumns)` = Returns a hex-dump of the string. 223 | - `str` = The string to be hex-dumped. 224 | - `columnLength` = How many columns wide. Default 32. 225 | - `hexWordSize` = How many bytes per word in the hex dump. Default 1. 226 | - `spaceEveryNColumns` = How often to insert an extra 1-character space. Default every 8 bytes. 227 | 228 | Example of the output: 229 | 230 | ``` lua 231 | > io.readfile'8x8-24bpp-solidwhite.bmp':hexdump() 232 | 00000000 42 4d 3a 01 00 00 00 00 00 00 7a 00 00 00 6c 00 00 00 08 00 00 00 08 00 00 00 01 00 18 00 00 00 BM:.......z...l................. 233 | 00000020 00 00 c0 00 00 00 23 2e 00 00 23 2e 00 00 00 00 00 00 00 00 00 00 42 47 52 73 00 00 00 00 00 00 ..�...#...#...........BGRs...... 234 | 00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................................ 235 | 00000060 00 00 00 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ff ff ff ff ff ff ..........................������ 236 | 00000080 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff �������������������������������� 237 | 000000a0 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff �������������������������������� 238 | 000000c0 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff �������������������������������� 239 | 000000e0 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff �������������������������������� 240 | 00000100 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff �������������������������������� 241 | 00000120 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff �������������������������� 242 | ``` 243 | 244 | ### table.lua: extensions to the Lua builtin tables 245 | 246 | `table.new([table1, table2, ...])` 247 | `table([table1, table2, ...])` = Returns a new table with `table` as its metatable. If any tables are passed as arguments then this performs a shallow-union of them into the resulting table. 248 | 249 | Notice that tables created with `table()` / `table.new()`, i.e. tables with `table` as a metatable, also have `table` as their metatable's `\_\_index` and can use infix notation to call any `table` methods. 250 | 251 | `table.unpack(t[, start[, end]])` is assigned to `unpack` for compatability with Lua <5.2. 252 | 253 | `table.pack(...)` is made compatible for Lua <5.2. 254 | 255 | `table.maxn(t)` is brought back. Returns the maximum of all number keys of a table. 256 | 257 | `table.sort(t, comparator)` is modified to return itself, for ease of chaining infix operations. 258 | 259 | `table.append(t, [t1, t2, ...])` = For each additional table provided in the argument (t1, t2, ...), inserts its numeric keys on the end of the first table `t`. 260 | 261 | `table.removeKeys(t, k1, k2, ...)` = Remove the specified keys from the table. 262 | 263 | `table.map(t, callback)` = Creates and returns a new table. The new table is formed by iterating through the elements of the table, calling `callback(value, key, newtable)`. If callback returns zero or one values then this is assumed to be the new value for the associated key in the new table. If callback returns two values then this is assumed to be the new value and key in the new table. 264 | 265 | `table.mapi(t, callback)` = Same as `table.map`, but only iterates through the `ipairs` keys. 266 | 267 | `table.filter(t, callback)` = Returns a new table formed by iterating through all keys of table `t` and calling the callback. If the callback returns false then the key/value is excluded from the new table, otherwise it is included. If the key is numeric then it is inserted using `table.insert`, otherwise it is directly assigned to the new table. 268 | 269 | `table.keys(t)` = Returns a new table of the enumerated keys of `t` in the order that `pairs` provides them. 270 | 271 | `table.values(t)` = Returns a new table of the enumerated values of `t` in the order that `pairs` provides them. 272 | 273 | `table.kvpairs(t)` = Returns a new table containing a list of `{[key] = value}` for each key/value pair within `t`. Intended for use with `next()`. 274 | 275 | `table.find(t, value, callback)` = Returns the key and value of the results of finding a value in a table `t`. If `callback` is not provided then the table is searched for `value`, and matching is tested with ==. If `callback` is provided then it is called for each i'th value in the table as `callback(ithValue, value)`. If it returns true then this element is considered to be matching. 276 | 277 | `table.insertUnique(t, value, callback)` = Inserts a value into a vable only if it does not already exist in the table (using `table.find`). If `callback` is provided then it is used as the callback in `table.find`. 278 | 279 | `table.removeObject(t, value, callback)` = Removes the specified value from the table, using `table.find`. Returns a list of the keys that were removed. 280 | 281 | `table.sup(t, comparator)` = Returns the maximum value in the table. `comparator(a,b)` is `a > b` by default. 282 | 283 | `table.inf(t, comparator)` = Returns the minimum value in the table. `comparator(a,b)` is `a < b` by default. 284 | 285 | `table.combine(t, callback)` = Combines elements in a table. The accumulated value is initialized to the first value, and all subsequent values are combined using `callback(accumulatedValue, newValue)`. 286 | 287 | `table.sum(t)` = Returns the sum of all values in the table. 288 | 289 | `table.product(t)` = Returns the product of all values in the table. 290 | 291 | `table.last(t)` = Shorthand for `t[#t]`. 292 | 293 | `table.sub(t, start, finish)` = Returns a subset of the table with elements from start to finish. Uses negative indexes and inclusive ranges, just like `string.sub`. 294 | 295 | `table.reverse(t)` = Returns the integer elements of table `t` reversed. 296 | 297 | `table.rep(t, n)` = Returns a table of `t` repeated `n` times, just like `string.rep`. 298 | 299 | `table.shuffle(t)` = Returns a new table with the integer keys of t shuffled randomly. 300 | 301 | `table.pickRandom(t)` = Return a random value in an indexed table. 302 | 303 | `table.pickWeighted(t)` = Asssumes collection to pick from are in keys and probabilities are in values. Returns a random key based. 304 | 305 | `table.wrapfor(f, s, var)` = Iterates over a Lua iterator, collecting results in a table. 306 | 307 | `table.permutations(t)` = Return an iterator across all permutations of indexed table values. 308 | 309 | `table.setmetatable(t)` = `setmetatable` 310 | 311 | ### number.lua: holds some extra number metatable functionality 312 | 313 | This is an extension off of the `ext/math.lua` file, with additional modifications for use as a replacement for the Lua number metatable. 314 | 315 | By requiring `ext.number` it will not set up the metatable, don't worry, that all only takes place via `ext.meta`. 316 | 317 | The major contribution of this file is `number.tostring`: 318 | 319 | `number.tostring(n, base, maxDigits)` = Converts a number to a string, where `base` is the specified base (default 10, non-integers are valid) and `maxDigits` is the maximum number of digits (default 50). 320 | `number.base` = A number value specifying the default base. This is initialized to 10. 321 | `number.maxdigits` = A number value specifying the default max digts. This is initialized to 50. 322 | 323 | This library doesn't assign `tostring` to `\_\_tostring` by default, but you can accomplish this using the following code: 324 | ``` lua 325 | -- assign number to the Lua number metatable 326 | require 'ext.meta' 327 | 328 | -- assign number's tostring to __tostring 329 | (function(m) m.__tostring = m.tostring end)(debug.getmetatable(0)) 330 | ``` 331 | 332 | `number.charfor(digit)` = Returns a character for the specified digit. This function assumes the digit is within the range of its desired base. 333 | 334 | `number.todigit(char)` = Returns the numerical value associated with the character. This is the opposite of `number.charfor`. 335 | 336 | `number.alphabets` = A table of tables containing the start and ending of ranges of unicode characters to use for the number to string conversion. 337 | 338 | `number.char` = Shorthand assigned to `string.char` for easy infix notation. Example, when `number` is set to the number metatable, `(97):char()` produces `'a'`. 339 | 340 | Example with `number` as Lua number metatables: 341 | ``` lua 342 | > require 'ext.meta' 343 | > (10):tostring(3) 344 | 101.00000000000000000000000000000000211012220021010120 345 | > (10):tostring(2) 346 | 1010. 347 | > (10):tostring(math.pi) 348 | 100.01022122221121122001111210201211021100300011111010 349 | ``` 350 | 351 | Yes, go ahead and sum up those digits scaled by their respective powers of pi, see that it does come out to be 10. 352 | 353 | Example with `number`, changing the default 354 | ``` lua 355 | > require 'ext.meta' 356 | > debug.getmetatable(0).base = 3 357 | > print(10) 358 | 10 -- nothing new 359 | > print(tostring(10)) 360 | 10 -- also nothing new. tostring is not assigned to __tostring by default. 361 | > print((10):tostring()) 362 | 101.00000000000000000000000000000000211012220021010120 -- subject to roundoff error 363 | ``` 364 | 365 | ### meta.lua: extends off of builtin metatables for all types. 366 | 367 | This file modifies most primitive Lua value metatables to add more functionality. 368 | 369 | I would not recommend using this file unless it is for small-scale scripts. The modifications it does are pretty overarching in Lua's behavior, and therefore could have unforeseen side-effects on librarys (though fwiw I haven't seen any yet). 370 | More importantly, it will definitely have implications on the interoperability of any Lua code that you write that is dependent on this behavior. 371 | 372 | Metatable changes: 373 | 374 | `nil` metatable now supports concat that converts values `tostring` before concatenating them. 375 | 376 | `boolean` metatable also now supports concatenation. Some infix functions are added to represent primitive boolean operations: 377 | 378 | - `and_` = `and`, such that `(true):and_(false)` produces `false`. 379 | - `or_` = `or` 380 | - `not_` = `not` 381 | - `xor` = logical XOR, equivalent to `a ~= b` for booleans a and b. 382 | - `implies` = logical 'implies', i.e. `not a or b`. 383 | 384 | `number` metatable is assigned to the table in `ext.number` (which is an extension of `ext.math` with some string-serialization additions). 385 | 386 | `string` metatable is assigned to the table in `ext.string` and given `tostring` concatenation. 387 | 388 | `coroutine` metatable is assigned to the table in `ext.coroutine`. 389 | 390 | `function` metatable is given the following operations: 391 | 392 | Default `tostring` concatenation. 393 | 394 | The following binary operators will now work on functions: + - * / % ^ 395 | Example: 396 | 397 | ``` lua 398 | > function f(x) return 2*x end 399 | > function g(x) return x+1 end 400 | > (f+g)(3) -- produces (function(x) return 2*x + x+1 end) 401 | 10 -- ...and calls it 402 | ``` 403 | 404 | The following unary operators will now work on functions: - # 405 | 406 | ``` lua 407 | > function f() return 2 end 408 | > (-f)() 409 | -2 410 | > function f() return 'foo' end 411 | > (#f)() 412 | 3 413 | ``` 414 | 415 | The following infix methods are added to functions: 416 | 417 | `f:dump()` = Infix shorthand for `string.dump`. 418 | 419 | `f:wrap()` = Infix shorthand for `corountine.wrap`. 420 | 421 | `f:co()` = Infix shorthand for `coroutine.create`. 422 | 423 | `f:index(key)` = Returns a function `g` such that `g(...) == f(...)[k]`. 424 | 425 | `f:assign(key, value)` = Returns a function `g` such that `g(...)` executes `f(...)[k] = v`. 426 | 427 | `f:compose(g1[, g2, ..., gN])` 428 | `f:o(g1[, g2, ..., gN])` = Returns a function `h` such that `h(x1, x2, ...)` executes `f(g1(g2(...gN( x1, x2, ... ))))`. 429 | 430 | `f:compose_n(n, g1[, g2, ..., gM])` 431 | `f:o_n(n, g1[, g2, ..., gM])` = Returns a function 'h' that only replaces the n'th argument with a concatenation of subsequent functions g1...gN. 432 | 433 | `f:bind(arg1[, arg2, ..., argN])` = Function curry. Returns a function 'g' that already has arguments arg1 ... argN bound to the first 'n' arguments of 'f'. 434 | 435 | `f:bind_n(n, arg1[, arg2, ..., argN])` = Same as above, but start the binding at argument index 'n'. 436 | 437 | `f:uncurry(n)` = Uncurry's a function 'n' functions deep. (a1 -> (a2 -> ... (an -> b))) -> (a1, a2, ..., an -> b). 438 | 439 | `f:nargs(n)` = Returns a function that is a duplicate of 'f' but only accepts 'n' arguments. Very useful with passing builtin functions as callbacks to `table.map` when you don't want extra return values to mess up your resulting table's keys.. 440 | 441 | `f:swap()` = Returns a new function with the first two parameters swapped. 442 | 443 | ### range.lua: 444 | 445 | `range = require 'ext.range'` 446 | `range(a[, b[, c]])` = A very simple function for creating tables of numeric for-loops. 447 | 448 | ### reload.lua: 449 | 450 | `reload = require 'ext.reload'` 451 | `reload(packagename)` = Removes the package from package.loaded, re-requires it, and returns its result. Useful for live testing of newly developed features. 452 | 453 | ### tolua.lua: 454 | 455 | `tolua = require 'ext.tolua'` 456 | `tolua(obj[, args])` = Serialization from any Lua value to a string. 457 | 458 | args can be any of the following: 459 | - `indent` = Default to 'true', set to 'false' to make results concise. 460 | - `pairs` = The `pairs()` operator to use when iterating over tables. This defaults to a form of pairs() which iterates over all fields using next(). 461 | Set this to your own custom pairs function, or 'pairs' if you would like serialization to respect the `_ _ pairs` metatable (which it does not by default). 462 | - `serializeForType` = A table with keys of lua types and values of callbacks for serializing those types. 463 | - `serializeMetatables` = Set to 'true' to include serialization of metatables. 464 | - `serializeMetatableFunc` = Function to override the default serialization of metatables. 465 | - `skipRecursiveReferences` = Default to 'false', set this to 'true' to not include serialization of recursive references. 466 | 467 | ### fromlua.lua: 468 | 469 | `fromlua = require 'ext.fromlua'` 470 | `fromlua(str)` = De-Serialization from a string to any Lua value. This is just a small wrapper using `load`. 471 | 472 | ### cmdline.lua: 473 | 474 | `getCmdline = require 'cmdline'` 475 | `cmdline = getCmdline(...)` = builds the table `cmdline` from all command-line arguments. Here are the rules it follows: 476 | - `cmdline[i]` is the `i`th command-line argument. 477 | - If a command-line argument `k` has no equals sign then `cmdline[k]` is assigned to `true`. 478 | - If a command-line argument has an equals sign present, i.e. is of the form `k=v`, then `cmdline[k]` is assigned the value of `v` if it was evaluated in Lua. 479 | - If evaluating it in Lua produces an error or nil then `cmdline[k]` is assigned the literal string of `v`. 480 | - Don't forget to wrap your complicated assignments in quotations marks. 481 | 482 | Notice that, calling `require 'ext'` will also call `getCmdline` on `arg`, producing the global `cmdline`. 483 | 484 | ### path.lua: path wrapper 485 | 486 | `path = require 'ext.path'` = represents an object representing the cwd. 487 | 488 | `p = path(pathstr)` returns a new `path` object representing the path at `pathstr`. Relative paths are appended from the previous `path` object's path. 489 | 490 | 491 | `p:open(...)` is an alias of `io.open(p.path, ...)`. 492 | 493 | `p:read(...)` is an alias of `io.readfile(p.path, ...)`. 494 | 495 | `p:write(...)` is an alias of `io.writefile(p.path, ...)`. 496 | 497 | `p:append(...)` is an alias of `io.appendfile(p.path, ...)`. 498 | 499 | `p:getdir(...)` is an alias of `io.getfiledir(p.path, ...)`, except that it wraps the arguments in a `Path` object. 500 | 501 | `p:getext(...)` is an alias of `io.getfileext(p.path, ...)`, except that it wraps the 1st argument in a `Path` object. 502 | 503 | `p:setext(newext)` = Returns a path matching `p`s path but with the last extension replaced with `newext`. If `newext` is `nil` then the last extension is removed. 504 | 505 | 506 | `p:remove(...)` is an alias of `os.remove(p.path, ...)`. 507 | 508 | `p:mkdir(...)` is an alias of `os.mkdir(p.path, ...)`. 509 | 510 | `p:dir(...)` is an alias of `os.listdir(p.path, ...)`, except that it wraps the arguments in a `Path` object. 511 | 512 | `p:exists(...)` is an alias of `os.fileexists(p.path, ...)`. 513 | 514 | `p:isdir(...)` is an alias of `os.isdir(p.path, ...)`. 515 | 516 | `p:rdir(...)` is an alias of `os.rlistdir(p.path, ...)`, except that it wraps the arguments in a `Path` object. 517 | 518 | 519 | `p:attr(...)` is an alias of `lfs.attributes(p.path, ...)`. 520 | 521 | `p:symattr(...)` is an alias of `lfs.symlinkattributes(p.path, ...)`. 522 | 523 | `p:cd(...)` is an alias of `lfs.chdir(p.path, ...)`. 524 | 525 | `p:link(...)` is an alias of `lfs.link(p.path, ...)`. 526 | 527 | `p:setmode(...)` is an alias of `lfs.setmode(p.path, ...)`. 528 | 529 | `p:touch(...)` is an alias of `lfs.touch(p.path, ...)`. 530 | 531 | `p:lockdir(...)` is an alias of `lfs.lock_dir(p.path, ...)`. 532 | 533 | 534 | `p:cwd()` returns the absolute cwd path, as a Path object. This is an alias of `lfs.currendir()` if available, or `io.readproc'pwd'` on Linux or `io.readproc'cd'` on Windows. 535 | 536 | `p:abs()` returns the absolute form of the path, as a Path object. 537 | 538 | ### debug.lua 539 | 540 | use this like so: 541 | 542 | Add debug code to your file like so: 543 | 544 | `--DEBUG:print"This only runs in debug"` 545 | 546 | then run your code like so: 547 | 548 | `lua -lext.debug ...` 549 | 550 | or 551 | 552 | `lua -e "require'ext.debug'" ...` 553 | 554 | ...and any single-line comments in any code that start with `--DEBUG:` will be uncommented. 555 | 556 | You can also add specific runtime tags: 557 | 558 | `--DEBUG(mytag):print"This only runs with debug'mytag'` 559 | 560 | `lua -e "require'ext.debug''tag==[[mytag]]'` 561 | 562 | ... or log-levels (the default level is 1): 563 | 564 | `--DEBUG(@2):print"This only runs with debug'level<=2'` 565 | 566 | `lua -e "require'ext.debug''level<=2'` 567 | 568 | ... or both: 569 | 570 | `--DEBUG(mytag@3):print"This only runs with debug'tag=="mytag" and level<=3'` 571 | 572 | `lua -e "require'ext.debug''tag==[[mytag]] and level<=3'` 573 | 574 | Debug queries can be any Lua expression using the variables `source`, `line`, `tag`, and `log`. 575 | 576 | ### gcmem.lua: 577 | 578 | Provides FFI-based functions for manually or automatically allocating and freeing memory. 579 | 580 | WIP due to crashing in LuaJIT when you run `ptr = ffi.cast('T*', ptr)` and haven't bound `ptr` anywhere else. 581 | 582 | # NOTICE: 583 | - path.lua will optionally use luafilesystem, if available. My own luafilesystem fork is preferred: https://github.com/thenumbernine/luafilesystem since it maintains a single copy of ffi cdefs within my lua-ffi-bindings library. 584 | - gcmem.lua depends on ffi, particularly some ffi headers of stdio found in my lua-ffi-bindings project: https://github.com/thenumbernine/lua-ffi-bindings 585 | -------------------------------------------------------------------------------- /assert.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | the original assert() asserts that the first arg is true, and returns all args, therefore we can assert that the first returned value will also always coerce to true 3 | 1) should asserts always return a true value? 4 | 2) or should asserts always return the forwarded value? 5 | I'm voting for #2 so assert can be used for wrapping args and not changing behvaior. when would you need to assert the first arg is true afer the assert has already bene carried out anyways? 6 | ... except for certain specified operations that cannot return their first argument, like assertindex() 7 | --]] 8 | 9 | -- cheap 'tolua' 10 | local function tostr(x) 11 | --[[ just tostring 12 | return tostring(x) 13 | --]] 14 | --[[ also quotes to help distinguish strings-of-numbers from numbers 15 | if type(x) == 'string' then return ('%q'):format(x) end 16 | return tostring(x) 17 | --]] 18 | --[[ full-on lua serialization ... might have trouble with cdata, especially cdata-primitives 19 | return require 'ext.tolua'(x) 20 | --]] 21 | -- [[ lua type and value 22 | return type(x)..'('..tostring(x)..')' 23 | --]] 24 | end 25 | 26 | local function prependmsg(msg, str) 27 | if type(msg) == 'number' then 28 | msg = tostring(msg) 29 | end 30 | if type(msg) == 'nil' then 31 | return str 32 | end 33 | if type(msg) == 'string' then 34 | return msg..': '..str 35 | end 36 | -- not implicitly converted to string -- return as is without message 37 | return msg 38 | end 39 | 40 | local function asserttype(x, t, msg, ...) 41 | local xt = type(x) 42 | if xt ~= t then 43 | error(prependmsg(msg, "expected "..tostring(t).." found "..tostring(xt))) 44 | end 45 | return x, t, msg, ... 46 | end 47 | 48 | local function assertis(obj, cl, msg, ...) 49 | if not cl.isa then 50 | error(prependmsg(msg, "assertis expected 2nd arg to be a class")) 51 | end 52 | if not cl:isa(obj) then 53 | error(prependmsg(msg, "object "..tostring(obj).." is not of class "..tostring(cl))) 54 | end 55 | return obj, cl, msg, ... 56 | end 57 | 58 | -- how to specify varargs... 59 | -- for now: (msg, N, type1, ..., typeN, arg1, ..., argN) 60 | local function asserttypes(msg, n, ...) 61 | asserttype(n, 'number', prependmsg(msg, "asserttypes number of args")) 62 | for i=1,n do 63 | asserttype(select(n+i, ...), select(i, ...), prependmsg(msg, "asserttypes arg "..i)) 64 | end 65 | return select(n+1, ...) 66 | end 67 | 68 | local function asserteq(a, b, msg, ...) 69 | if not (a == b) then 70 | error(prependmsg(msg, "expected "..tostr(a).." == "..tostr(b))) 71 | end 72 | return a, b, msg, ... 73 | end 74 | 75 | local function asserteqeps(a, b, eps, msg, ...) 76 | eps = eps or 1e-7 77 | local normval = math.abs(a - b) 78 | if normval > eps then 79 | error((msg and msg..': ' or '').."expected |"..tostr(a).." - "..tostr(b).."| <= "..eps..' but found norm to be '..tostr(normval)) 80 | end 81 | return a, b, eps, msg, ... 82 | end 83 | 84 | local function absdiff(a,b) return math.abs(a - b) end 85 | local function asserteqepsnorm(a, b, eps, norm, msg, ...) 86 | eps = eps or 1e-7 87 | norm = norm or absdiff 88 | local normval = norm(a, b) 89 | if normval > eps then 90 | error((msg and msg..': ' or '').."expected |"..tostr(a)..", "..tostr(b).."| <= "..eps..' but found norm to be '..tostr(normval)) 91 | end 92 | return a, b, eps, norm, msg, ... 93 | end 94 | 95 | local function assertne(a, b, msg, ...) 96 | if not (a ~= b) then 97 | error(prependmsg(msg, "expected "..tostr(a).." ~= "..tostr(b))) 98 | end 99 | return a, b, msg, ... 100 | end 101 | 102 | local function assertlt(a, b, msg, ...) 103 | if not (a < b) then 104 | error(prependmsg(msg, "expected "..tostr(a).." < "..tostr(b))) 105 | end 106 | return a, b, msg, ... 107 | end 108 | 109 | local function assertle(a, b, msg, ...) 110 | if not (a <= b) then 111 | error(prependmsg(msg, "expected "..tostr(a).." <= "..tostr(b))) 112 | end 113 | return a, b, msg, ... 114 | end 115 | 116 | local function assertgt(a, b, msg, ...) 117 | if not (a > b) then 118 | error(prependmsg(msg, "expected "..tostr(a).." > "..tostr(b))) 119 | end 120 | return a, b, msg, ... 121 | end 122 | 123 | local function assertge(a, b, msg, ...) 124 | if not (a >= b) then 125 | error(prependmsg(msg, "expected "..tostr(a).." >= "..tostr(b))) 126 | end 127 | return a, b, msg, ... 128 | end 129 | 130 | -- this is a t[k] operation + assert 131 | local function assertindex(t, k, msg, ...) 132 | if not t then 133 | error(prependmsg(msg, "object is nil")) 134 | end 135 | local v = t[k] 136 | assert(v, prependmsg(msg, "expected "..tostr(t).."["..tostr(k).." ]")) 137 | return v, msg, ... 138 | end 139 | 140 | -- assert integer indexes 1 to len, and len of tables matches 141 | -- maybe I'll use ipairs... maybe 142 | local function asserttableieq(t1, t2, msg, ...) 143 | asserteq(#t1, #t2, msg) 144 | for i=1,#t1 do 145 | asserteq(t1[i], t2[i], msg) 146 | end 147 | return t1, t2, msg, ... 148 | end 149 | 150 | -- for when you want to assert a table's length but still want to return the table 151 | -- TODO should this be like assertindex() where it performs the operation and returns the operator value, i.e. returns the length instead of the table? 152 | -- or would that be less usable than asserting the length and returning the table? 153 | local function assertlen(t, n, msg, ...) 154 | asserteq(#t, n, msg) 155 | return t, n, msg, ... 156 | end 157 | 158 | local function asserterror(f, msg, ...) 159 | local result, errmsg = pcall(f, ...) 160 | asserteq(result, false, prependmsg(msg, errmsg)) 161 | -- I'd like to forward all arguments like every assert above 162 | --return f, msg, ... 163 | -- but by its nature, "asserterror" means "we expect a discontinuity in execution from this code" 164 | -- and the calling code wants to see the resulting error information 165 | -- and since I already error'd if no error was found, 166 | -- then we already know the pcall's result at this point is false 167 | -- so I'll do this: 168 | return errmsg 169 | end 170 | 171 | local origassert = _G.assert 172 | return setmetatable({ 173 | type = asserttype, 174 | types = asserttypes, 175 | is = assertis, 176 | eq = asserteq, 177 | ne = assertne, 178 | lt = assertlt, 179 | le = assertle, 180 | gt = assertgt, 181 | ge = assertge, 182 | index = assertindex, 183 | eqeps = asserteqeps, 184 | eqepsnorm = asserteqepsnorm, 185 | tableieq = asserttableieq, 186 | len = assertlen, 187 | error = asserterror, 188 | }, { 189 | -- default `assert = require 'ext.assert'` works, as well as `assertle = assert.le` 190 | __call = function(t, ...) 191 | return origassert(...) 192 | end, 193 | }) 194 | -------------------------------------------------------------------------------- /class.lua: -------------------------------------------------------------------------------- 1 | local table = require 'ext.table' 2 | 3 | -- classes 4 | 5 | local function newmember(class, ...) 6 | local obj = setmetatable({}, class) 7 | if obj.init then return obj, obj:init(...) end 8 | return obj 9 | end 10 | 11 | local classmeta = { 12 | __call = function(self, ...) 13 | -- [[ normally: 14 | return self:new(...) 15 | --]] 16 | --[[ if you want to keep track of all instances 17 | local results = table.pack(self:new(...)) 18 | local obj = results[1] 19 | self.instances[obj] = true 20 | return results:unpack() 21 | --]] 22 | end, 23 | } 24 | 25 | -- usage: class:isa(obj) 26 | -- so it's not really a member method, since the object doesn't come first, but this way we can use it as Class:isa(obj) and not worry about nils or local closures 27 | local function isa(cl, obj) 28 | assert(cl, "isa: argument 1 is nil, should be the class object") -- isa(nil, anything) errors, because it should always have a class in the 1st arg 29 | if type(obj) ~= 'table' then return false end -- class:isa(not a table) will return false 30 | if not obj.isaSet then return false end -- not an object generated by class(), so it doesn't have a set of all classes that it "is-a" 31 | return obj.isaSet[cl] or false -- returns true if the 'isaSet' of the object's metatable (its class) holds the calling class 32 | end 33 | 34 | local function class(...) 35 | local cl = table(...) 36 | cl.class = cl 37 | 38 | cl.super = ... -- .super only stores the first. the rest can be accessed by iterating .isaSet's keys 39 | 40 | -- I was thinking of calling this '.superSet', but it is used for 'isa' which is true for its own class, so this is 'isaSet' 41 | cl.isaSet = {[cl] = true} 42 | for i=1,select('#', ...) do 43 | local parent = select(i, ...) 44 | if parent ~= nil then 45 | cl.isaSet[parent] = true 46 | if parent.isaSet then 47 | for grandparent,_ in pairs(parent.isaSet) do 48 | cl.isaSet[grandparent] = true 49 | end 50 | end 51 | end 52 | end 53 | 54 | -- store 'descendantSet' as well that gets appended when we call class() on this obj? 55 | for ancestor,_ in pairs(cl.isaSet) do 56 | ancestor.descendantSet = ancestor.descendantSet or {} 57 | ancestor.descendantSet[cl] = true 58 | end 59 | 60 | cl.__index = cl 61 | cl.new = newmember 62 | cl.isa = isa -- usage: Class:isa(obj) 63 | cl.subclass = class -- such that cl:subclass() or cl:subclass{...} will return a subclass of 'cl' 64 | 65 | --[[ if you want to keep track of all instances 66 | cl.instances = setmetatable({}, {__mode = 'k'}) 67 | --]] 68 | 69 | setmetatable(cl, classmeta) 70 | return cl 71 | end 72 | 73 | return class 74 | -------------------------------------------------------------------------------- /cmdline.lua: -------------------------------------------------------------------------------- 1 | -- auto serialize all cmdline params and store them in the global 'cmdline' 2 | -- well, don't store them here, instead return the table from the require() 3 | -- have ext.env store them in the global 4 | 5 | local fromlua = require 'ext.fromlua' 6 | local assert = require 'ext.assert' 7 | local table = require 'ext.table' 8 | local string = require 'ext.string' 9 | local tolua = require 'ext.tolua' 10 | 11 | 12 | local function getCmdline(...) 13 | -- let cmdline[1...] work as well 14 | local cmdline = {...} 15 | 16 | for _,w in ipairs{...} do 17 | local k,v = w:match'^(.-)=(.*)$' 18 | if k then 19 | pcall(function() 20 | cmdline[k] = fromlua(v) 21 | end) 22 | if cmdline[k] == nil then cmdline[k] = v end 23 | else 24 | cmdline[w] = true 25 | end 26 | end 27 | 28 | return cmdline 29 | end 30 | 31 | 32 | 33 | -- this might have officially gotten out of hand... 34 | local function showHelp(cmdValue, cmdKey, cmdline, desc) 35 | print'specify commands via `command` or `command=value`' 36 | print() 37 | print'commands:' 38 | for _,k in ipairs(table.keys(desc):sort()) do 39 | local descValue = desc[k] 40 | if descValue.desc then 41 | print('\t'..descValue.name..' = '..string.trim(descValue.desc):gsub('\n', '\n\t\t')) 42 | else 43 | print('\t'..descValue.name) 44 | end 45 | print() 46 | end 47 | end 48 | -- .validate() signature: 49 | local function showHelpAndQuit(...) 50 | showHelp(...) 51 | -- 'and quit' means willingly , vs showing help if we fail, so ... 52 | -- TODO brief help (if a cmd goes bad, show brief help) 53 | -- vs full help (i.e. --help etc) 54 | os.exit(0) 55 | end 56 | 57 | --[[ 58 | ok now for some stern support 59 | errors if it does't get a validated argument 60 | TODO how to support key'd, indexed, or both cmds ... 61 | for now just validate key'd commands. 62 | TODO equivalent processing for integer indexes, so 'a=1 b=2 c=3' is the same as 'a 1 b 2 c 3' 63 | 64 | desc = 65 | [cmdline key] = 66 | true = use anything 67 | string = Lua type of the argument 68 | or 'number' will cast strings to numbers for you 69 | or 'file' = string + validate the file exists 70 | table = 71 | .type = type of the argument \_ ... use one of these 72 | .validate = input validation / 73 | .must = error if the argument isn't present 74 | .desc = description of the argument (to print for help?) 75 | function 76 | = function for validating the argument 77 | 78 | --]] 79 | local function validate(desc) 80 | -- here transform desc into the same format 81 | for _,name in ipairs(table.keys(desc)) do 82 | local descValue = desc[name] 83 | 84 | -- use desc[name] to handle the cmdline 85 | if descValue == true then 86 | descValue = {} 87 | elseif type(descValue) == 'string' then 88 | descValue = { 89 | type = descValue, 90 | } 91 | elseif type(descValue) == 'function' then 92 | descValue = { 93 | validate = descValue, 94 | } 95 | elseif type(descValue) == 'table' then 96 | -- fallthru and handle next 97 | else 98 | error('idk how to handle this cmdline description '..tolua(descValue)) 99 | end 100 | 101 | if not descValue.type 102 | and not descValue.validate then 103 | -- no type/validate is provided? use always 104 | descValue.validate = function() end 105 | end 106 | 107 | -- convert desc's with .type into .validate 108 | if descValue.type then 109 | assert(not descValue.validate, "you should provide either a .type or a .validate, but not both") 110 | local descType = descValue.type 111 | descValue.validate = function(cmdValue, key, cmdline) 112 | -- special-case casting numbers etc? 113 | if descType == 'number' then 114 | cmdline[name] = assert(tonumber(cmdValue)) 115 | elseif descType == 'file' then 116 | assert.type(cmdValue, 'string') 117 | assert(require 'ext.path'(cmdValue):exists(), "failed to find file "..tolua(cmdValue)) 118 | else 119 | assert.type(cmdValue, descType) 120 | end 121 | end 122 | --desc.type = nil -- still useful? 123 | end 124 | 125 | descValue.name = name 126 | 127 | desc[name] = descValue 128 | end 129 | 130 | -- now build our object that we're going to return to the caller 131 | 132 | local cmdlineValidation = {} 133 | -- cmdline.validate(descs...):fromTable(getCmdline(cmdline...)) is equivalent to cmdline.validate(descs)(cmdline...) 134 | -- or can be used to handle already-produced cmdline objects 135 | cmdlineValidation.fromTable = function(self, cmdline) 136 | -- make sure all cmdline keys are in the description 137 | -- TODO this is going to assume the int key'd cmdline are from ... 138 | -- and the string key'd cmdline are from 'k' or 'k=v' 139 | -- so if someon does '[1]=true', then yes, it will overwrite the int-key'd cmdline, and that should probably be prevented in 'getCmdline' 140 | for _,k in ipairs(table.keys(cmdline)) do 141 | local cmdValue = cmdline[k] 142 | if type(k) == 'number' then 143 | -- assume its part of the ... sequence 144 | elseif type(k) == 'string' then 145 | local descValue = desc[k] 146 | if not descValue then 147 | error("got an unknown command "..tolua(k)) 148 | else 149 | -- assert validation, sometimes overwriting cmdline[k] as we go 150 | descValue.validate(cmdValue, k, cmdline, desc) 151 | end 152 | else 153 | error("got a cmdline with an unknown key type: "..tolua(k)) 154 | end 155 | end 156 | 157 | -- make sure all must-be keys are in the command-line 158 | for k,v in pairs(desc) do 159 | if v.must then 160 | if not cmdline[k] then 161 | error("expected to find key "..k) 162 | end 163 | end 164 | end 165 | 166 | return cmdline 167 | end 168 | -- cmdline.validate(descs ...)(cmdline...) operates on the cmdline 169 | setmetatable(cmdlineValidation, { 170 | __call = function(self, ...) 171 | return self:fromTable(getCmdline(...)) 172 | end, 173 | }) 174 | return cmdlineValidation 175 | end 176 | 177 | return setmetatable({ 178 | getCmdline = getCmdline, 179 | validate = validate, 180 | showHelp = showHelp, 181 | showHelpAndQuit = showHelpAndQuit, 182 | }, { 183 | __call = function(t,...) 184 | return getCmdline(...) 185 | end, 186 | }) 187 | -------------------------------------------------------------------------------- /coroutine.lua: -------------------------------------------------------------------------------- 1 | local coroutine = {} 2 | for k,v in pairs(require 'coroutine') do coroutine[k] = v end 3 | 4 | local function safehandle(thread, res, ...) 5 | if not res then 6 | local err = tostring(...)..'\n'..debug.traceback(thread) 7 | --[[ 8 | reminder to myself: 9 | this is here because where else should it go? 10 | I don't want assertresume to error, but I do want it to print a stacktrace 11 | it is especially used with threadmanager, which calls assertresume, then gets the status to determine if the thread is running or not 12 | I could put an assert() around that, but assert(assertresume()) seems strange, and I'd still have to only print the exceptions when the status ~= dead 13 | So things seem to work best with threadmanager if I put this here. 14 | Maybe I shouldn't call it 'assertresume' but instead something else like 'resumeAndPrintErrorIfThereIsOne' ? 15 | --]] 16 | io.stderr:write(err..'\n') 17 | io.stderr:flush() 18 | return false, err 19 | end 20 | return true, ... 21 | end 22 | 23 | -- resumes thread 24 | -- if the thread is dead, return false 25 | -- if the thread is alive and resume failed due to error, prints the stack trace of the thread upon error 26 | -- as opposed to assert(coroutine.resume(thread)), which only prints the stack trace of the resume statement 27 | -- if the thread is alive and resume succeeded, returns true 28 | function coroutine.assertresume(thread, ...) 29 | if coroutine.status(thread) == 'dead' then return false, 'dead' end 30 | return safehandle(thread, coroutine.resume(thread, ...)) 31 | end 32 | 33 | return coroutine 34 | -------------------------------------------------------------------------------- /ctypes.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | put all the ctypes in scope for more c++-ish programming 3 | 4 | I'm not sure if this belongs in ext or ffi or anywhere else ... 5 | I probably won't add it to git 6 | Maybe I'll never use this, since it primarily operates on the global namespace 7 | --]] 8 | 9 | local ffi = require 'ffi' 10 | 11 | for _,t in ipairs{ 12 | 'char', 13 | --'byte', = uint8_t right? 14 | 'short', 15 | 'int', 16 | 'long', 17 | 'int8_t', 18 | 'uint8_t', 19 | 'int16_t', 20 | 'uint16_t', 21 | 'int32_t', 22 | 'uint32_t', 23 | 'int64_t', 24 | 'uint64_t', 25 | 'float', 26 | 'double', 27 | --'float32' = float right 28 | --'float64' = double right? 29 | --'float80' = long double ... in most cases ... ? or is long double sometimes float128? 30 | } do 31 | _G[t] = ffi.typeof(t) 32 | end 33 | 34 | _G.byte = ffi.typeof'uint8_t' 35 | _G.float32 = ffi.typeof'float' 36 | _G.float64 = ffi.typeof'double' 37 | _G.float80 = ffi.typeof'long double' 38 | 39 | _G.ffi = ffi 40 | _G.sizeof = ffi.sizeof 41 | _G.typeof = ffi.typeof 42 | -------------------------------------------------------------------------------- /debug.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Here's a quick hack for debugging 3 | It's not meta-lua, not dependent on my parser lib, nothing like that 4 | It does use my new load() shim layer ext.load 5 | Just this: when you require() a file, if the debug tag is set, then grep all --DEBUG: lines to remove the comment 6 | Usage: `lua -lext.debug ...` 7 | or if you want to query specific dags: `lua -e "require 'ext.debug' 'tag1,tag2,tag3'" ...` 8 | 9 | 10 | But now I'm tired of always tagging files, so how about a proper query script. 11 | Something that looks like this: 12 | lua -e "require 'ext.debug' 'source:match'sdl*' and level>3 and func:match'SDLAPP' and tag:match'testing''" 13 | 14 | That's great but those are a lot of variables that I haven't got yet. 15 | 16 | What kind of variables would we like: 17 | source = string of the current loaded source 18 | level = log-level, needs to be specified in the DEBUG tag 19 | func = current function ... how to pick out, aside from making this use the parser (that means moving it out of ext and into its own library...) 20 | tag = same as before 21 | 22 | How to specify things like tag and level ... 23 | 24 | DEBUG(tag@level): ? 25 | DEBUG(@1): 26 | 27 | What should default log level be? 1. 28 | --]] 29 | 30 | function string_split(s, exp) 31 | exp = exp or '' 32 | s = tostring(s) 33 | local t = {} 34 | -- handle the exp='' case 35 | if exp == '' then 36 | for i=1,#s do 37 | table.insert(t, s:sub(i,i)) 38 | end 39 | else 40 | local searchpos = 1 41 | local start, fin = s:find(exp, searchpos) 42 | while start do 43 | table.insert(t, s:sub(searchpos, start-1)) 44 | searchpos = fin+1 45 | start, fin = s:find(exp, searchpos) 46 | end 47 | table.insert(t, s:sub(searchpos)) 48 | end 49 | return t 50 | end 51 | 52 | local oldload = load or loadstring 53 | local logcond 54 | 55 | --[[ 56 | Strip out DEBUG: and DEBUG(...): tags based on what tags were requested via `require 'ext.debug'(tag1,tag2,...)` 57 | 58 | d = data 59 | source = for require'd files will be an absolute file path, from ext.load, from package.searchers[2] 60 | 61 | Tags will look like: 62 | `--DEBUG(tag):` to specify a tag. 63 | `--DEBUG(@level):` to specify a level. 64 | `--DEBUG(tag@level):` to specify both. 65 | --]] 66 | table.insert(require 'ext.load'().xforms, function(d, source) 67 | local result = {} 68 | local ls = string_split(d, '\n') 69 | for lineno=1,#ls do 70 | local l = ls[lineno] 71 | local found 72 | repeat 73 | found = false 74 | 75 | local start, fin = l:find'%-%-DEBUG:' 76 | if start then 77 | if logcond(source, lineno, 1) then 78 | l = l:sub(1, start-1)..l:sub(fin+1) 79 | ls[lineno] = l 80 | found = true 81 | end 82 | end 83 | 84 | local start, fin = l:find'%-%-DEBUG%b():' 85 | if start then 86 | local inside = l:sub(start+8, fin-2) 87 | local tag, level = table.unpack(string_split(inside, '@')) 88 | level = tonumber(level) 89 | if logcond(source, lineno, level, tag) then 90 | l = l:sub(1, start-1)..l:sub(fin+1) 91 | ls[lineno] = l 92 | found = true 93 | end 94 | end 95 | until not found 96 | end 97 | d = table.concat(ls, '\n') 98 | return d 99 | end) 100 | 101 | --[[ 102 | Use this with: `lua -e "require 'ext.debug' 'source:match'sdl*' or level>3 and tag:match'testing''"` 103 | 104 | Conditions can use the variables: source, line, level, tag 105 | --]] 106 | local function setCond(condstr) 107 | local code = 'local source, line, level, tag = ... return '..condstr 108 | logcond = assert(oldload(code)) 109 | end 110 | 111 | setCond'level == 1' 112 | 113 | return setCond 114 | -------------------------------------------------------------------------------- /detect_ffi.lua: -------------------------------------------------------------------------------- 1 | -- this runs just once and then gets cached in package.path 2 | -- but return the function, not the value, because the value could be false, which (I don't believe) package.path will cache 3 | local ffi 4 | local function detect_ffi() 5 | if ffi == nil then 6 | local result 7 | result, ffi = pcall(require, 'ffi') 8 | ffi = result and ffi 9 | end 10 | return ffi 11 | end 12 | return detect_ffi 13 | -------------------------------------------------------------------------------- /detect_lfs.lua: -------------------------------------------------------------------------------- 1 | local lfs 2 | local function detect_lfs() 3 | if lfs == nil then 4 | -- ok in a naive world luajit => lfs_ffi and lua => lfs 5 | -- esp in a windows world where giving your lua and luajit scripts the same package.path/cpath's will cause crashes 6 | -- but on openresty we're using luajit but using lfs so ... 7 | -- don't mix up your package.path/cpath's 8 | -- and i'm gonna try both 9 | for _,try in ipairs{'lfs', 'lfs_ffi'} do 10 | local result 11 | result, lfs = pcall(require, try) 12 | lfs = result and lfs 13 | if lfs then break end 14 | end 15 | end 16 | return lfs 17 | end 18 | return detect_lfs 19 | -------------------------------------------------------------------------------- /detect_os.lua: -------------------------------------------------------------------------------- 1 | local detect_ffi = require 'ext.detect_ffi' 2 | local result 3 | local function detect_os() 4 | if result ~= nil then return result end 5 | local ffi = detect_ffi() 6 | if ffi then 7 | result = ffi.os == 'Windows' 8 | else 9 | -- TODO what if uname doesn't exist? then this will output to stderr. does it exist in Windows? 10 | -- to get around that on Windows I can pipe to > NUL 11 | -- TODO what if it's not Windows? then this will create a NUL file ... 12 | -- honestly I could just use the existence of piping to NUL vs /dev/null to determine Windows vs Unix ... 13 | result = ({ 14 | msys = true, 15 | ming = true, 16 | --})[(io.popen'uname 2> NUL':read'*a'):sub(1,4):lower()] 17 | })[(io.popen'uname':read'*a'):sub(1,4):lower()] or false 18 | end 19 | 20 | -- right now just true/value for windows/not 21 | return result 22 | end 23 | return detect_os 24 | -------------------------------------------------------------------------------- /distinfo: -------------------------------------------------------------------------------- 1 | name = "ext" 2 | files = { 3 | ["LICENSE"] = "ext/LICENSE", 4 | ["README.md"] = "ext/README.md", 5 | ["assert.lua"] = "ext/assert.lua", 6 | ["class.lua"] = "ext/class.lua", 7 | ["cmdline.lua"] = "ext/cmdline.lua", 8 | ["coroutine.lua"] = "ext/coroutine.lua", 9 | ["ctypes.lua"] = "ext/ctypes.lua", 10 | ["debug.lua"] = "ext/debug.lua", 11 | ["detect_ffi.lua"] = "ext/detect_ffi.lua", 12 | ["detect_lfs.lua"] = "ext/detect_lfs.lua", 13 | ["detect_os.lua"] = "ext/detect_os.lua", 14 | ["env.lua"] = "ext/env.lua", 15 | ["ext.lua"] = "ext/ext.lua", 16 | ["ext.rockspec"] = "ext/ext.rockspec", 17 | ["fromlua.lua"] = "ext/fromlua.lua", 18 | ["gc.lua"] = "ext/gc.lua", 19 | ["gcmem.lua"] = "ext/gcmem.lua", 20 | ["io.lua"] = "ext/io.lua", 21 | ["load.lua"] = "ext/load.lua", 22 | ["math.lua"] = "ext/math.lua", 23 | ["meta.lua"] = "ext/meta.lua", 24 | ["number.lua"] = "ext/number.lua", 25 | ["op.lua"] = "ext/op.lua", 26 | ["os.lua"] = "ext/os.lua", 27 | ["path.lua"] = "ext/path.lua", 28 | ["range.lua"] = "ext/range.lua", 29 | ["reload.lua"] = "ext/reload.lua", 30 | ["require.lua"] = "ext/require.lua", 31 | ["string.lua"] = "ext/string.lua", 32 | ["table.lua"] = "ext/table.lua", 33 | ["timer.lua"] = "ext/timer.lua", 34 | ["tolua.lua"] = "ext/tolua.lua", 35 | ["xpcall.lua"] = "ext/xpcall.lua", 36 | } 37 | deps = { 38 | "ffi", 39 | --"lfs", -- for vanilla lua 40 | "lfs_ffi", -- for luajit 41 | } 42 | -------------------------------------------------------------------------------- /env.lua: -------------------------------------------------------------------------------- 1 | require 'ext.gc' 2 | local table = require 'ext.table' 3 | return function(env) 4 | env = env or _G 5 | require 'ext.xpcall'(env) 6 | require 'ext.require'(env) 7 | require 'ext.load'(env) 8 | env.math = require 'ext.math' 9 | env.table = table 10 | env.string = require 'ext.string' 11 | env.coroutine = require 'ext.coroutine' 12 | env.io = require 'ext.io' 13 | env.os = require 'ext.os' 14 | env.path = require 'ext.path' 15 | env.tolua = require 'ext.tolua' 16 | env.fromlua = require 'ext.fromlua' 17 | env.class = require 'ext.class' 18 | env.reload = require 'ext.reload' 19 | env.range = require 'ext.range' 20 | env.timer = require 'ext.timer' 21 | env.op = require 'ext.op' 22 | env.getCmdline = require 'ext.cmdline' 23 | env.cmdline = env.getCmdline(table.unpack(arg or {})) 24 | env._ = os.execute 25 | -- requires ffi 26 | --env.gcnew = require 'ext.gcmem'.new 27 | --env.gcfree = require 'ext.gcmem'.free 28 | env.assert = require 'ext.assert' 29 | -- TODO deprecate this and switch to assert.le assert.ge etc 30 | for k,v in pairs(env.assert) do 31 | env['assert'..k] = v 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /ext.lua: -------------------------------------------------------------------------------- 1 | require 'ext.meta' 2 | require 'ext.env'() 3 | -------------------------------------------------------------------------------- /ext.rockspec: -------------------------------------------------------------------------------- 1 | package = "ext" 2 | version = "dev-1" 3 | source = { 4 | url = "git+https://github.com/thenumbernine/lua-ext.git" 5 | } 6 | description = { 7 | summary = "Commonly used extensions to the Lua default libraries.", 8 | detailed = [[ 9 | Commonly used extensions to the Lua default libraries. 10 | Note to users: The structure of the source code doesn"t exactly match the structure in the rock install destination. 11 | This is because I personally use a `LUA_PATH` pattern of "?/?.lua" in addition to the typical "?.lua". 12 | To work with compatability of everyone else who does not use this convention, I have the rockspec install `ext/ext.lua` into `ext.lua` and keep `ext/everything_else.lua` at `ext/everything_else.lua`. 13 | ]], 14 | homepage = "https://github.com/thenumbernine/lua-ext", 15 | license = "MIT" 16 | } 17 | dependencies = { 18 | "lua ~> 5.1" 19 | } 20 | build = { 21 | type = "builtin", 22 | modules = { 23 | ["ext.assert"] = "assert.lua", 24 | ["ext.class"] = "class.lua", 25 | ["ext.cmdline"] = "cmdline.lua", 26 | ["ext.coroutine"] = "coroutine.lua", 27 | ["ext.ctypes"] = "ctypes.lua", 28 | ["ext.debug"] = "debug.lua", 29 | ["ext.detect_ffi"] = "detect_ffi.lua", 30 | ["ext.detect_lfs"] = "detect_lfs.lua", 31 | ["ext.detect_os"] = "detect_os.lua", 32 | ["ext.env"] = "env.lua", 33 | ["ext"] = "ext.lua", 34 | ["ext.fromlua"] = "fromlua.lua", 35 | ["ext.gc"] = "gc.lua", 36 | ["ext.gcmem"] = "gcmem.lua", 37 | ["ext.io"] = "io.lua", 38 | ["ext.load"] = "load.lua", 39 | ["ext.math"] = "math.lua", 40 | ["ext.meta"] = "meta.lua", 41 | ["ext.number"] = "number.lua", 42 | ["ext.op"] = "op.lua", 43 | ["ext.os"] = "os.lua", 44 | ["ext.path"] = "path.lua", 45 | ["ext.range"] = "range.lua", 46 | ["ext.reload"] = "reload.lua", 47 | ["ext.require"] = "require.lua", 48 | ["ext.string"] = "string.lua", 49 | ["ext.table"] = "table.lua", 50 | ["ext.timer"] = "timer.lua", 51 | ["ext.tolua"] = "tolua.lua", 52 | ["ext.xpcall"] = "xpcall.lua" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /fromlua.lua: -------------------------------------------------------------------------------- 1 | return function(str, ...) 2 | return assert(load('return '..str, ...))() 3 | end 4 | -------------------------------------------------------------------------------- /gc.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | lua 5.2+ supports __gc for tables and userdata 3 | lua 5.1 supports __gc for userdata only 4 | luajit (5.1+...?) supports __gc for userdata and cdata ... but not tables 5 | 6 | but luckily 5.1 and luajit have this 'newproxy' function 7 | 8 | To use this, just call "require 'ext.gc'" once to override the 'setmetatable' function to a new one that uses 'newproxy' to provide table metatables 9 | If this doesn't find 'newproxy' (not in Lua 5.2+) then it doesn't override anything. 10 | --]] 11 | 12 | if not newproxy then return end 13 | 14 | -- from https://stackoverflow.com/a/77702023/2714073 15 | -- TODO accept an 'env' param like so many other of my override functions ... maybe ... tho I don't do this with all, do I? 16 | local gcProxies = setmetatable({}, {__mode='k'}) 17 | local oldsetmetatable = setmetatable 18 | function setmetatable(t, mt) 19 | local oldp = gcProxies[t] 20 | if oldp then 21 | getmetatable(oldp).__gc = nil 22 | --oldsetmetatable(oldp, nil) 23 | end 24 | 25 | if mt and mt.__gc then 26 | local p = newproxy(true) 27 | gcProxies[t] = p 28 | getmetatable(p).__gc = function() 29 | if type(mt.__gc) == 'function' then 30 | mt.__gc(t) 31 | end 32 | end 33 | end 34 | 35 | return oldsetmetatable(t, mt) 36 | end 37 | -------------------------------------------------------------------------------- /gcmem.lua: -------------------------------------------------------------------------------- 1 | local ffi = require 'ffi' 2 | require 'ffi.req' 'c.stdlib' 3 | 4 | --[[ 5 | uses C malloc paired with ffi-based garbage collection 6 | typecasts correctly 7 | and retains the ability to manually free 8 | (so you don't have to manually free it) 9 | NOTICE casting *AFTER* wrapping will crash, probably due to the gc thinking the old pointer is gone 10 | also ffi.gc retains type, so no worries about casting before 11 | ... 12 | that was true, but now it's always losing the ptr and crashing, so I'm going to fall back on ffi.new 13 | --]] 14 | local function gcnew(T, n) 15 | -- [[ 16 | local ptr = ffi.C.malloc(n * ffi.sizeof(T)) 17 | ptr = ffi.cast(T..'*', ptr) 18 | ptr = ffi.gc(ptr, ffi.C.free) 19 | --]] 20 | --[[ 21 | local ptr = ffi.new(T..'['..n..']') 22 | --]] 23 | return ptr 24 | end 25 | 26 | --[[ 27 | manual free of a pointer 28 | frees the ptr and removes it from the gc 29 | (just in case you want to manually free a pointer) 30 | --]] 31 | local function gcfree(ptr) 32 | ffi.C.free(ffi.gc(ptr, nil)) 33 | end 34 | 35 | return { 36 | new = gcnew, 37 | free = gcfree, 38 | } 39 | -------------------------------------------------------------------------------- /io.lua: -------------------------------------------------------------------------------- 1 | local io = {} 2 | for k,v in pairs(require 'io') do io[k] = v end 3 | 4 | -- io or os? io since it is shorthand for io.open():read() 5 | function io.readfile(fn) 6 | local f, err = io.open(fn, 'rb') 7 | if not f then return false, err end 8 | 9 | -- file.read compat (tested on Windows) 10 | -- *a a *l l 11 | -- lua-5.3.5: yes yes yes yes jit == nil and _VERSION == 'Lua 5.3' 12 | -- lua-5.2.4: yes no yes no jit == nil and _VERSION == 'Lua 5.2' 13 | -- lua-5.1.5: yes no yes no jit == nil and _VERSION == 'Lua 5.1' 14 | -- luajit-2.1.0-beta3: yes yes yes yes (jit.version == 'LuaJIT 2.1.0-beta3' / jit.version_num == 20100) 15 | -- luajit-2.0.5 yes no yes no (jit.version == 'LuaJIT 2.0.5' / jit.version_num == 20005) 16 | local d = f:read('*a') 17 | f:close() 18 | return d 19 | end 20 | 21 | function io.writefile(fn, d) 22 | local f, err = io.open(fn, 'wb') 23 | if not f then return false, err end 24 | if d then f:write(d) end 25 | f:close() 26 | return true 27 | end 28 | 29 | function io.appendfile(fn, d) 30 | local f, err = io.open(fn, 'ab') 31 | if not f then return false, err end 32 | if d then f:write(d) end 33 | f:close() 34 | return true 35 | end 36 | 37 | function io.readproc(cmd) 38 | local f, err = io.popen(cmd) 39 | if not f then return false, err end 40 | local d = f:read('*a') 41 | f:close() 42 | return d 43 | end 44 | 45 | function io.getfiledir(fn) 46 | local dir, name = fn:match'^(.*)/([^/]-)$' 47 | if dir == '' then 48 | -- "/" => "/", "/" 49 | if name == '' then return '/', '/' end 50 | -- "/x" => "/", "x" 51 | return '/', name 52 | elseif not dir then 53 | return '.', fn 54 | end 55 | return dir, name 56 | end 57 | 58 | -- this should really return the extension first. 59 | -- that is the function name, after all. 60 | function io.getfileext(fn) 61 | local front, ext = fn:match('^(.*)%.([^%./]-)$') 62 | if front then 63 | return front, ext 64 | end 65 | -- no ext? then leave that field nil - just return the base filename 66 | return fn, nil 67 | end 68 | 69 | -- in Lua 5.3.5 at least: 70 | -- (for file = getmetatable(io.open(something))) 71 | -- io.read ~= file.read 72 | -- file.__index == file 73 | -- within meta.lua, simply modifying the file metatable 74 | -- but if someone requires ext/io.lua and not lua then io.open and all subsequently created files will need to be modified 75 | --[[ TODO FIXME 76 | if jit or (not jit and _VERSION < 'Lua 5.2') then 77 | 78 | local function fixfilereadargs(...) 79 | print(...) 80 | if select('#', ...) == 0 then return ... end 81 | local fmt = select(1, ...) 82 | if fmt == 'a' then fmt = '*a' 83 | elseif fmt == 'l' then fmt = '*l' 84 | elseif fmt == 'n' then fmt = '*n' 85 | end 86 | return fmt, fixfilereadargs(select(2, ...)) 87 | end 88 | 89 | -- even though io.read is basically the same as file.read, they are still different functions 90 | -- so file.read will still have to be separately overridden 91 | local oldfileread 92 | local function newfileread(...) 93 | return oldfileread(fixfilereadargs(...)) 94 | end 95 | io.read = function(...) 96 | return newfileread(io.stdout, ...) 97 | end 98 | 99 | local oldfilemeta = debug.getmetatable(io.stdout) 100 | local newfilemeta = {} 101 | for k,v in pairs(oldfilemeta) do 102 | newfilemeta[k] = v 103 | end 104 | 105 | -- override file:read 106 | oldfileread = oldfilemeta.read 107 | newfilemeta.read = newfileread 108 | 109 | -- should these be overridden in this case, or only when running ext/meta.lua? 110 | debug.setmetatable(io.stdin, newfilemeta) 111 | debug.setmetatable(io.stdout, newfilemeta) 112 | debug.setmetatable(io.stderr, newfilemeta) 113 | 114 | local function fixfilemeta(...) 115 | if select('#', ...) > 0 then 116 | local f = select(1, ...) 117 | if f then 118 | debug.setmetatable(f, newfilemeta) 119 | end 120 | end 121 | return ... 122 | end 123 | 124 | local oldioopen = io.open 125 | function io.open(...) 126 | return fixfilemeta(oldioopen(...)) 127 | end 128 | end 129 | --]] 130 | 131 | -- [[ add lfs lock/unlock to files 132 | do 133 | local detect_lfs = require 'ext.detect_lfs' 134 | local lfs = detect_lfs() 135 | if lfs then 136 | -- can I do this? yes on Lua 5.3. Yes on LuaJIT 2.1.0-beta3 137 | local filemeta = debug.getmetatable(io.stdout) 138 | filemeta.lock = lfs.lock 139 | filemeta.unlock = lfs.unlock 140 | end 141 | end 142 | --]] 143 | 144 | return io 145 | -------------------------------------------------------------------------------- /load.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | shim load() and loadstring() and loadfile() layer 3 | comes with callback handler for various AST or string modifications 4 | 5 | How to make this more modular so that it could be applied to specific environments and not just _G ? 6 | require 'ext.load'(_G) -- modify _G so load, loadstring, loadfile, require all use new load() function with xform shim layer 7 | but if it's returning a function instead of caching result in package.laded after a one-time global modification, then how to store it and associate it per-env, so we don't re-add the layer every time we require it? 8 | How about caching the results per-input-env table? 9 | Of course if anyone else replaces _G, or passes in multiple _ENV's, then we're gonna re-apply this a few times possibly ... 10 | 11 | local state = require 'ext.load'(env) 12 | state has the following: 13 | xforms = table of transforms for the load() function 14 | oldload = old load function 15 | load = new load function that is now also assigned to env.load 16 | oldloadfile = old loadfile function 17 | loadfile = new loadfile function that is now also assigned to env.loadfile 18 | olddofile = old dofile function 19 | dofile = new dofile function that is now also assigned to env.dofile 20 | oldloadstring = (if env.loadstring exists) old loadstring function 21 | loadstring = (if env.loadstring exists) new loadstring function that is now also assigned to env.loadstring 22 | oldsearchfile = old package.searchers[2] or package.loaders[2] 23 | searchfile = new package.searchers[2] or package.loaders[2] 24 | --]] 25 | 26 | local stateForEnv = {} 27 | 28 | return function(env) 29 | env = env or _G 30 | 31 | local state = stateForEnv[env] 32 | if state then return state end 33 | state = {} 34 | 35 | require 'ext.xpcall'(env) 36 | require 'ext.require'(env) 37 | 38 | local package = env.package or _G.package 39 | 40 | -- package.searchpath is missing in 5.1 (but not luajit) ... I need a package.searchpath 41 | -- Put in its own file? does anyone else need this? or not since it pairs with my require-override, which is in this file too. 42 | local searchpath = package.searchpath 43 | if not searchpath then 44 | function searchpath(name, path, sep, rep) 45 | sep = sep or ';' 46 | rep = rep or '/' -- or \ for windows ... TODO meh? 47 | local namerep = name:gsub('%.', rep) 48 | local attempted = {} 49 | for w in path:gmatch('[^'..sep..']*') do -- TODO escape sep? meh? 50 | local fn = w 51 | if fn == '' then -- search default locations ... which are ... builtin ... or not? 52 | else 53 | fn = fn:gsub('%?', namerep) 54 | -- path(fn):exists() implementation: 55 | local exists = io.open(fn,'rb') 56 | if exists then 57 | exists:close() 58 | return fn 59 | end 60 | table.insert(attempted, "\n\tno file '"..fn.."'") 61 | end 62 | end 63 | return nil, table.concat(attempted) 64 | end 65 | end 66 | 67 | state.xforms = setmetatable({}, {__index=table}) 68 | 69 | -- TODO proper test? like if load'string' fails? 70 | local loadUsesFunctions = (_VERSION == 'Lua 5.1' and not env.jit) 71 | state.oldload = loadUsesFunctions and (env.loadstring or _G.loadstring) or (env.load or _G.load) 72 | 73 | -- ok here's my modified load behavior 74 | -- it's going to parse the lua 5.4 code and spit out the luajit code 75 | state.load = function(data, ...) 76 | if type(data) == 'function' then 77 | local s = {} 78 | repeat 79 | -- "A return of an empty string, nil, or no value signals the end of the chunk." 80 | local chunk = data() 81 | if chunk == '' or chunk == nil then break end 82 | table.insert(s, chunk) 83 | until false 84 | data = table.concat(s) 85 | end 86 | -- 5.1 behavior: load(func, name) versus loadstring(data, name) 87 | -- 5.2..5.4 behavior: load(chunk, name, mode, env) 88 | -- TODO mind you the formatting on re-converting it will be off ... 89 | -- errors won't match up ... 90 | -- so I'll re-insert the generated code 91 | -- TODO would be nice to save whitespace and re-insert that ... hmm maybe long into the future ... 92 | -- TODO xpcall behavior testing for when we are allowed to forward the args ... maybe that compat behavior belongs in ext ? 93 | local source = ... or ('['..data:sub(1,10)..'...]') 94 | for i,xform in ipairs(state.xforms) do 95 | local reason 96 | data, reason = xform(data, source) 97 | if not data then 98 | return false, "ext.load.xform["..i.."]: "..(reason and tostring(reason) or '') 99 | end 100 | end 101 | return state.oldload(data, ...) 102 | end 103 | 104 | -- override global load() function, and maybe loadfile() if it's present too 105 | -- (maybe loadstring() too ?) 106 | if env.loadstring ~= nil or _G.loadstring ~= nil then 107 | state.oldloadstring = env.loadstring or _G.loadstring 108 | state.loadstring = state.load 109 | env.loadstring = state.loadstring 110 | end 111 | -- TODO if we're in luajit (_VERSION=Lua 5.1) then load() will handle strings, but if we're in lua 5.1 then it will only handle functions (according to docs?) right? 112 | env.load = state.load 113 | 114 | state.oldloadfile = env.loadfile or _G.loadfile 115 | -- NOTICE when specifying args (filename, mode, env) explicitly, and forwarding them explicitly, the CLI had some trouble with _ENV var in lua 5.4 ... 116 | -- so for vanilla lua cli the number of args matters for some reason 117 | state.loadfile = function(...) 118 | local filename = ... 119 | local data, err 120 | if filename then 121 | local f 122 | f, err = io.open(filename, 'rb') 123 | if not f then return nil, err end 124 | data, err = f:read'*a' 125 | f:close() 126 | else 127 | data, err = io.read'*a' 128 | end 129 | if err then return nil, err end 130 | 131 | -- luajit loadfile/dofile will skip the first # just fine 132 | -- but lua 5.4 will not ... lua 5.4 seems to only skip the leading # if you run it directly 133 | -- so here's some luajit compat ... 134 | if data then 135 | data = data:match'^#[^\n]*\n(.*)$' or data 136 | end 137 | 138 | return state.load(data, ...) 139 | end 140 | env.loadfile = state.loadfile 141 | 142 | state.olddofile = env.dofile or _G.dofile 143 | state.dofile = function(filename) 144 | return assert(state.loadfile(filename))() 145 | end 146 | env.dofile = state.dofile 147 | 148 | -- next TODO here , same as ext.debug (consider making modular) 149 | -- ... wedge in new package.seachers[2]/package.loaders[2] behavior to use my modified load() 150 | -- replace the package.loaders[2] / package.searchers[2] table entry 151 | -- make it to replace file contents before loading 152 | local searchers = assert(package.searchers or package.loaders, "couldn't find searchers") 153 | state.oldsearchfile = searchers[2] 154 | state.searchfile = function(req, ...) 155 | local filename, err = searchpath(req, package.path) 156 | if not filename then return err end 157 | local f, err = state.loadfile(filename) 158 | return f or err 159 | end 160 | searchers[2] = state.searchfile 161 | 162 | stateForEnv[env] = state 163 | return state 164 | end 165 | -------------------------------------------------------------------------------- /math.lua: -------------------------------------------------------------------------------- 1 | local math = {} 2 | for k,v in pairs(require 'math') do math[k] = v end 3 | 4 | math.nan = 0/0 5 | 6 | math.e = math.exp(1) 7 | 8 | -- luajit and lua 5.1 compat ... 9 | if not math.atan2 then math.atan2 = math.atan end 10 | -- also note, code that uses math.atan(y,x) in luajit will instead just call math.atan(y) ... 11 | 12 | -- some luas don't have hyperbolic trigonometric functions 13 | 14 | if not math.sinh then 15 | function math.sinh(x) 16 | local ex = math.exp(x) 17 | return .5 * (ex - 1/ex) 18 | end 19 | end 20 | 21 | if not math.cosh then 22 | function math.cosh(x) 23 | local ex = math.exp(x) 24 | return .5 * (ex + 1/ex) 25 | end 26 | end 27 | 28 | if not math.tanh then 29 | function math.tanh(x) 30 | --[[ this isn't so stable. 31 | local ex = math.exp(x) 32 | return (ex - 1/ex) / (ex + 1/ex) 33 | --]] 34 | -- [[ instead... 35 | -- if e^-2x < smallest float epsilon 36 | -- then consider (e^x - e^-x) ~ e^x .. well, it turns out to be 1 37 | -- and if e^2x < smallest float epsilon then -1 38 | if x < 0 then 39 | local e2x = math.exp(2*x) 40 | return (e2x - 1) / (e2x + 1) 41 | else 42 | local em2x = math.exp(-2*x) 43 | return (1 - em2x) / (1 + em2x) 44 | end 45 | --]] 46 | end 47 | end 48 | 49 | function math.asinh(x) 50 | return math.log(x + math.sqrt(x*x + 1)) 51 | end 52 | 53 | function math.acosh(x) 54 | return math.log(x + math.sqrt(x*x - 1)) 55 | end 56 | 57 | function math.atanh(x) 58 | return .5 * math.log((1 + x) / (1 - x)) 59 | end 60 | 61 | function math.cbrt(x) 62 | return math.sign(x) * math.abs(x)^(1/3) 63 | end 64 | 65 | function math.clamp(v,min,max) 66 | return math.max(math.min(v, max), min) 67 | end 68 | 69 | function math.sign(x) 70 | if x < 0 then return -1 end 71 | if x > 0 then return 1 end 72 | return 0 73 | end 74 | 75 | function math.trunc(x) 76 | if x < 0 then return math.ceil(x) else return math.floor(x) end 77 | end 78 | 79 | function math.round(x) 80 | return math.floor(x+.5) 81 | end 82 | 83 | function math.isnan(x) return x ~= x end 84 | function math.isinf(x) return x == math.huge or x == -math.huge end 85 | function math.isfinite(x) return tonumber(x) and not math.isnan(x) and not math.isinf(x) end 86 | 87 | function math.isprime(n) 88 | if n < 2 then return false end -- 1 isnt prime 89 | for i=2,math.floor(math.sqrt(n)) do 90 | if n%i == 0 then 91 | return false 92 | end 93 | end 94 | return true 95 | end 96 | 97 | -- assumes n is a non-negative integer. this isn't the Gamma function 98 | function math.factorial(n) 99 | local prod = 1 100 | for i=1,n do 101 | prod = prod * i 102 | end 103 | return prod 104 | end 105 | 106 | function math.factors(n) 107 | local table = require 'ext.table' 108 | local f = table() 109 | for i=1,n do 110 | if n%i == 0 then 111 | f:insert(i) 112 | end 113 | end 114 | return f 115 | end 116 | 117 | -- returns a table containing the prime factorization of the number 118 | function math.primeFactorization(n) 119 | local table = require 'ext.table' 120 | n = math.floor(n) 121 | local f = table() 122 | while n > 1 do 123 | local found = false 124 | for i=2,math.floor(math.sqrt(n)) do 125 | if n%i == 0 then 126 | n = math.floor(n/i) 127 | f:insert(i) 128 | found = true 129 | break 130 | end 131 | end 132 | if not found then 133 | f:insert(n) 134 | break 135 | end 136 | end 137 | return f 138 | end 139 | 140 | function math.gcd(a,b) 141 | return b == 0 and a or math.gcd(b, a % b) 142 | end 143 | 144 | -- if this math lib gets too big ... 145 | function math.mix(a,b,s) 146 | return a * (1 - s) + b * s 147 | end 148 | 149 | return math 150 | -------------------------------------------------------------------------------- /meta.lua: -------------------------------------------------------------------------------- 1 | local assert = require 'ext.assert' 2 | local table = require 'ext.table' 3 | local string = require 'ext.string' 4 | local coroutine = require 'ext.coroutine' 5 | local number = require 'ext.number' 6 | local op = require 'ext.op' 7 | 8 | -- fix up lua type metatables 9 | 10 | -- nil 11 | debug.setmetatable(nil, {__concat = string.concat}) 12 | 13 | -- booleans 14 | debug.setmetatable(true, { 15 | __concat = string.concat, 16 | __index = { 17 | and_ = op.land, 18 | or_ = op.lor, 19 | not_ = op.lnot, 20 | xor = function(a,b) return a ~= b end, 21 | implies = function(a,b) return not a or b end, 22 | } 23 | }) 24 | 25 | debug.setmetatable(0, number) 26 | 27 | -- strings 28 | getmetatable('').__concat = string.concat 29 | getmetatable('').__index = string 30 | 31 | -- It'd be fun if I could apply the operator to all return values, and not just the first ... 32 | -- like (function() return 1,2 end + function() return 3,4 end)() returns 4,6 33 | local function combineFunctionsWithBinaryOperator(f, g, opfunc) 34 | if type(f) == 'function' and type(g) == 'function' then 35 | return function(...) 36 | return opfunc(f(...), g(...)) 37 | end 38 | elseif type(f) == 'function' then 39 | return function(...) 40 | return opfunc(f(...), g) 41 | end 42 | elseif type(g) == 'function' then 43 | return function(...) 44 | return opfunc(f, g(...)) 45 | end 46 | else 47 | -- shouldn't get called unless __add is called explicitly 48 | return function() 49 | return opfunc(f, g) 50 | end 51 | end 52 | end 53 | 54 | -- primitive functions. should these be public? or put in a single table? 55 | 56 | -- function operators generate functions 57 | -- f(x) = y, g(x) = z, (f+g)(x) = y+z 58 | local functionMeta = { 59 | -- I could make this a function composition like the rest of the meta operations, 60 | -- but instead I'm going to have it follow the default __concat convention I have with other primitive types 61 | __concat = string.concat, 62 | dump = function(f) return string.dump(f) end, 63 | __add = function(f, g) return combineFunctionsWithBinaryOperator(f, g, op.add) end, 64 | __sub = function(f, g) return combineFunctionsWithBinaryOperator(f, g, op.sub) end, 65 | __mul = function(f, g) return combineFunctionsWithBinaryOperator(f, g, op.mul) end, 66 | __div = function(f, g) return combineFunctionsWithBinaryOperator(f, g, op.div) end, 67 | __mod = function(f, g) return combineFunctionsWithBinaryOperator(f, g, op.mod) end, 68 | __pow = function(f, g) return combineFunctionsWithBinaryOperator(f, g, op.pow) end, 69 | __unm = function(f) return function(...) return -f(...) end end, 70 | __len = function(f) return function(...) return #f(...) end end, 71 | -- boolean operations aren't overloaded just yet. should they be? 72 | --__call doesn't work anyways 73 | -- TODO comparison operations too? probably not equivalence for compatability with sort() and anything else 74 | -- TODO boolean operations? anything else? 75 | --[[ here's one option for allowing any function object dereference to be mapped to a new function 76 | __index = function(f, k) return function(...) return f(...)[k] end end, 77 | __newindex = function(f, k, v) return function(...) f(...)[k] = v end end, 78 | --]] 79 | -- [[ ... but that prevents us from overloading our own methods. 80 | -- so here's "index" to be used in its place 81 | -- while we can provide more of our own methods as we desire 82 | __index = { 83 | -- takes a function that returns an object 84 | -- returns a function that returns that object's __index to the key argument 85 | -- so if f() = {a=1} then f:index'a'() == 1 86 | index = function(f, k) 87 | return function(...) 88 | return f(...)[k] 89 | end 90 | end, 91 | -- takes a function that returns an object 92 | -- returns a function that applies that object's __newindex to the key and value arguments 93 | -- so if t={} and f()==t then f:assign('a',1)() assigns t.a==1 94 | assign = function(f, k, v) 95 | return function(...) 96 | f(...)[k] = v 97 | end 98 | end, 99 | 100 | -- f:compose(g1, ...) returns a function that evaluates to f(g1(...(gn(args)))) 101 | compose = function(...) 102 | local funcs = table.pack(...) 103 | for i=1,funcs.n do 104 | assert.type(funcs[i], 'function') 105 | end 106 | return function(...) 107 | local args = table.pack(...) 108 | for i=funcs.n,1,-1 do 109 | args = table.pack(funcs[i](table.unpack(args,1,args.n))) 110 | end 111 | return table.unpack(args,1,args.n) 112 | end 113 | end, 114 | 115 | -- f:compose_n(n, g) returns a function that evaluates to f(arg[1], ... arg[j-1], g(arg[j]), arg[j+1], ..., arg[n]) 116 | compose_n = function(f, n, ...) 117 | local funcs = table.pack(...) 118 | return function(...) 119 | local args = table.pack(...) 120 | 121 | local ntharg = {args[n]} 122 | ntharg.n = n <= args.n and 1 or 0 123 | for i=funcs.n,1,-1 do 124 | ntharg = table.pack(funcs[i](table.unpack(ntharg,1,ntharg.n))) 125 | end 126 | 127 | args[n] = ntharg[1] 128 | args.n = math.max(args.n, n) 129 | 130 | return f(table.unpack(args, 1, args.n)) 131 | end 132 | end, 133 | 134 | -- bind / partial apply -- currying first args, and allowing vararg rest of args 135 | bind = function(f, ...) 136 | local args = table.pack(...) 137 | return function(...) 138 | local n = args.n 139 | local callargs = {table.unpack(args, 1, n)} 140 | for i=1,select('#', ...) do 141 | n=n+1 142 | callargs[n] = select(i, ...) 143 | end 144 | return f(table.unpack(callargs, 1, n)) 145 | end 146 | end, 147 | 148 | -- bind argument n, n+1, n+2, ... to the values provided 149 | bind_n = function(f, n, ...) 150 | local nargs = table.pack(...) 151 | return function(...) 152 | local args = table.pack(...) 153 | for i=1,nargs.n do 154 | args[n+i-1] = nargs[i] 155 | end 156 | args.n = math.max(args.n, n+nargs.n-1) 157 | return f(table.unpack(args, 1, args.n)) 158 | end 159 | end, 160 | 161 | -- Takes a function and a number of arguments, 162 | -- returns a function that applies them individually, 163 | -- first to the function, then to each function returned 164 | -- (a1 -> (a2 -> ... (an -> b))) -> (a1, a2, ..., an -> b) 165 | uncurry = function(f, n) 166 | return function(...) 167 | local s = f 168 | for i=1,n do 169 | s = s(select(i, ...)) 170 | end 171 | return s 172 | end 173 | end, 174 | -- grows/shrinks the number of args passed. pads with nil. 175 | nargs = function(f, n) 176 | return function(...) 177 | local t = {} 178 | for i=1,n do 179 | t[i] = select(i, ...) 180 | end 181 | return f(table.unpack(t, 1, n)) 182 | end 183 | end, 184 | -- swaps the next two arguments 185 | swap = function(f) 186 | return function(a, b, ...) 187 | return f(b, a, ...) 188 | end 189 | end, 190 | dump = string.dump, 191 | -- coroutine access 192 | wrap = coroutine.wrap, 193 | co = coroutine.create, 194 | } 195 | --]] 196 | } 197 | -- shorthand 198 | functionMeta.__index._ = functionMeta.__index.index 199 | functionMeta.__index.o = functionMeta.__index.compose 200 | functionMeta.__index.o_n = functionMeta.__index.compose_n 201 | debug.setmetatable(function() end, functionMeta) 202 | 203 | -- coroutines 204 | debug.setmetatable(coroutine.create(function() end), {__index = coroutine}) 205 | 206 | -- TODO lightuserdata, if you can create it within lua somehow ... 207 | -------------------------------------------------------------------------------- /number.lua: -------------------------------------------------------------------------------- 1 | -- number metatable 2 | local math = require 'ext.math' 3 | 4 | -- flatten ext.math into this just like we flatten lua's string into ext.string 5 | 6 | local hasutf8, utf8 = pcall(require, 'utf8') 7 | 8 | local number = {} 9 | number.__index = number 10 | 11 | for k,v in pairs(math) do number[k] = math[k] end 12 | 13 | -- [[ tostring machine precision of arbitrary base 14 | number.alphabets = { 15 | {('0'):byte(), ('9'):byte()}, -- latin numbers 16 | {('a'):byte(), ('z'):byte()}, -- english 17 | {0x3b1, 0x3c9}, -- greek 18 | {0x430, 0x45f}, -- cyrillic 19 | {0x561, 0x586}, -- armenian 20 | {0x905, 0x939}, -- devanagari 21 | {0x2f00, 0x2fd5}, -- kangxi 22 | {0x3041, 0x3096}, -- hiragana 23 | {0x30a1, 0x30fa}, -- katakana 24 | {0x4e00, 0x9fd0}, -- chinese, japanese, korean characters 25 | } 26 | 27 | function number.charfor(digit) 28 | local table = require 'ext.table' 29 | for _,alphabet in ipairs(number.alphabets) do 30 | local start,fin = table.unpack(alphabet) 31 | if digit <= fin - start then 32 | digit = digit + start 33 | if hasutf8 then 34 | return utf8.char(digit) 35 | else 36 | -- TODO this will fail with utf8 chars beyond ascii 37 | return string.char(digit) 38 | end 39 | end 40 | digit = digit - (fin - start + 1) 41 | end 42 | error 'you need more alphabets to represent that many digits' 43 | end 44 | 45 | -- TODO rename above function to 'tochar' ? 46 | function number.todigit(ch) 47 | local table = require 'ext.table' 48 | local indexInAlphabet 49 | if hasutf8 then 50 | indexInAlphabet = utf8.codepoint(ch) 51 | else 52 | -- TODO this will fail with utf8 chars beyond ascii 53 | indexInAlphabet = string.byte(ch) 54 | end 55 | local lastTotalIndex = 0 56 | for _,alphabet in ipairs(number.alphabets) do 57 | local start,fin = table.unpack(alphabet) 58 | if indexInAlphabet >= start and indexInAlphabet <= fin then 59 | return lastTotalIndex + (indexInAlphabet - start) 60 | end 61 | lastTotalIndex = lastTotalIndex + (fin - start + 1) 62 | end 63 | error"couldn't find the character in all the alphabets" 64 | end 65 | 66 | number.base = 10 67 | number.maxdigits = 50 68 | -- I'm not going to set this as __tostring by default, but I will leave it as part of the meta 69 | -- feel free to use it with a line something like (function(m)m.__tostring=m.tostring end)(debug.getmetatable(0)) 70 | number.tostring = function(t, base, maxdigits) 71 | local s = {} 72 | if t < 0 then 73 | t = -t 74 | table.insert(s, '-') 75 | end 76 | if t == 0 then 77 | table.insert(s, '0.') 78 | else 79 | --print('t',t) 80 | if not base then base = number.base end 81 | if not maxdigits then maxdigits = number.maxdigits end 82 | --print('base',base) 83 | local i = math.floor(math.log(t,base))+1 84 | if i == math.huge then error'infinite number of digits' end 85 | --print('i',i) 86 | t = t / base^i 87 | --print('t',t) 88 | local dot 89 | while true do 90 | if i < 1 then 91 | if not dot then 92 | dot = true 93 | table.insert(s, '.') 94 | table.insert(s, ('0'):rep(-i)) 95 | end 96 | if t == 0 then break end 97 | if i <= -maxdigits then break end 98 | end 99 | t = t * base 100 | local digit = math.floor(t) 101 | t = t - digit 102 | -- at this point 'digit' holds an integer value in [0,base) 103 | -- there's two ways we can go about it: 104 | -- 1) traditionally, where each digit represents an integer, times some base^i for some i 105 | -- in this case, for fractional bases, the last (fraction) digit is only considered up to the fraction 106 | -- i.e. in base 2.5, from 0b..1b we have the span of 1, partitioned by digits 0.1b at 2/5ths the distance, 0.2b at 4/5ths the distance, and 1.0 and 5/5ths the distance 107 | -- from 1b..2b we have the same span, 108 | -- and from 2b..10b we have half that span, from 2 to 2.5 109 | -- therefore the value 2.2b would represent 2.8, which is also represetned by 10.0112210002002002... 110 | -- so as long as the span between digits can represent fractions of base^i rather than whole base^i's 111 | -- we can have multiple representations of numbers 112 | -- 2) stretched. in this case a fractional span, such as from 2b to 10b in base 2.5, would be stretched 113 | -- this is harder to convey when descibing the number system 114 | table.insert(s, number.charfor(digit)) 115 | i = i - 1 116 | --print('t',t,'i',i,'digit',digit) 117 | end 118 | end 119 | return table.concat(s) 120 | end 121 | --]] 122 | 123 | -- ('a'):byte():char() == 'a' 124 | number.char = string.char 125 | 126 | -- so the lookup goes: primitive number -> number -> math 127 | return number 128 | -------------------------------------------------------------------------------- /op.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | make lua functions for each operator. 3 | it looks like i'm mapping 1-1 between metamethods and fields in this table. 4 | useful for using Lua as a functional language. 5 | 6 | TODO rename to 'ops'? 7 | --]] 8 | 9 | --local load = require 'string'.load -- string.load = loadstring or load 10 | local load = loadstring or load 11 | 12 | -- test if we hae lua 5.3 bitwise operators 13 | -- orrr I could just try each op and bail out on error 14 | -- and honestly I should be defaulting to the 'bit' library anyways, esp in the case of luajit where it is translated to an asm opcode 15 | local lua53 = _VERSION >= 'Lua 5.3' 16 | 17 | local symbolscode = [[ 18 | 19 | -- which fields are unary operators 20 | local unary = { 21 | unm = true, 22 | bnot = true, 23 | len = true, 24 | lnot = true, 25 | } 26 | 27 | local symbols = { 28 | add = '+', 29 | sub = '-', 30 | mul = '*', 31 | div = '/', 32 | mod = '%', 33 | pow = '^', 34 | unm = '-', -- unary 35 | concat = '..', 36 | eq = '==', 37 | ne = '~=', 38 | lt = '<', 39 | le = '<=', 40 | gt = '>', 41 | ge = '>=', 42 | land = 'and', -- non-overloadable 43 | lor = 'or', -- non-overloadable 44 | len = '#', -- unary 45 | lnot = 'not', -- non-overloadable, unary 46 | ]] 47 | if lua53 then 48 | symbolscode = symbolscode .. [[ 49 | idiv = '//', -- 5.3 50 | band = '&', -- 5.3 51 | bor = '|', -- 5.3 52 | bxor = '~', -- 5.3 53 | shl = '<<', -- 5.3 54 | shr = '>>', -- 5.3 55 | bnot = '~', -- 5.3, unary 56 | ]] 57 | --[[ alternatively, luajit 'bit' library: 58 | I should probably include all of these instead 59 | would there be a perf hit from directly assigning these functions to my own table, 60 | as there is a perf hit for assigning from ffi.C func ptrs to other variables? probably. 61 | how about as a tail call / vararg forwarding? 62 | I wonder if luajit adds extra metamethods 63 | 64 | luajit 2.0 lua 5.2 lua 5.3 65 | band band & 66 | bnot bnot ~ 67 | bor bor | 68 | bxor bxor ~ 69 | lshift lshift << 70 | rshift rshift >> 71 | arshift arshift 72 | rol lrotate 73 | ror rrotate 74 | bswap (reverses 32-bit integer endian-ness of bytes) 75 | tobit (converts from lua number to its signed 32-bit value) 76 | tohex (string conversion) 77 | btest (does some bitflag stuff) 78 | extract (same) 79 | replace (same) 80 | --]] 81 | end 82 | symbolscode = symbolscode .. [[ 83 | } 84 | ]] 85 | 86 | local symbols, unary = assert(load(symbolscode..' return symbols, unary'))() 87 | 88 | local code = symbolscode .. [[ 89 | -- functions for operators 90 | local ops 91 | ops = { 92 | ]] 93 | for name,symbol in pairs(symbols) do 94 | if unary[name] then 95 | code = code .. [[ 96 | ]]..name..[[ = function(a) return ]]..symbol..[[ a end, 97 | ]] 98 | else 99 | code = code .. [[ 100 | ]]..name..[[ = function(a,b) return a ]]..symbol..[[ b end, 101 | ]] 102 | end 103 | end 104 | code = code .. [[ 105 | index = function(t, k) return t[k] end, 106 | newindex = function(t, k, v) 107 | t[k] = v 108 | return t, k, v -- ? should it return anything ? 109 | end, 110 | call = function(f, ...) return f(...) end, 111 | 112 | symbols = symbols, 113 | 114 | -- special pcall wrapping index, thanks luajit. thanks. 115 | -- while i'm here, multiple indexing, so it bails out nil early, so it's a chained .? operator 116 | safeindex = function(t, ...) 117 | if select('#', ...) == 0 then return t end 118 | local res, v = pcall(ops.index, t, ...) 119 | if not res then return nil, v end 120 | return ops.safeindex(v, select(2, ...)) 121 | end, 122 | } 123 | return ops 124 | ]] 125 | return assert(load(code))() 126 | -------------------------------------------------------------------------------- /os.lua: -------------------------------------------------------------------------------- 1 | local os = {} 2 | for k,v in pairs(require 'os') do os[k] = v end 3 | 4 | -- for io.readproc 5 | -- don't require os from inside io ... 6 | local io = require 'ext.io' 7 | 8 | -- table.pack 9 | local table = require 'ext.table' 10 | 11 | -- string.trim 12 | local string = require 'ext.string' 13 | local assert = require 'ext.assert' 14 | local detect_lfs = require 'ext.detect_lfs' 15 | local detect_os = require 'ext.detect_os' 16 | 17 | os.sep = detect_os() and '\\' or '/' 18 | 19 | -- TODO this vs path.fixpathsep ... 20 | -- should I just move everything over to 'path' ... 21 | function os.path(str) 22 | assert.type(str, 'string') 23 | return (str:gsub('/', os.sep)) 24 | end 25 | 26 | -- 5.2 os.execute compat 27 | -- TODO if 5.1 was built with 5.2-compat then we don't have to do this ... 28 | -- how to test? 29 | if _VERSION == 'Lua 5.1' then 30 | local execute = os.execute 31 | function os.execute(cmd) 32 | local results = table.pack(execute(cmd)) 33 | if #results > 1 then return results:unpack() end -- >5.1 API 34 | local errcode = results[1] 35 | local reason = ({ 36 | [0] = 'exit', 37 | })[errcode] or 'unknown' 38 | return errcode == 0 and true or nil, reason, errcode 39 | end 40 | end 41 | 42 | -- too common not to put here 43 | -- this does execute but first prints the command to stdout 44 | function os.exec(cmd) 45 | print('>'..cmd) 46 | return os.execute(cmd) 47 | end 48 | 49 | -- TODO should this fail if the dir already exists? or should it succeed? 50 | -- should it fail if a file is presently there? probably. 51 | -- should makeParents be set by default? it's on by default in Windows. 52 | function os.mkdir(dir, makeParents) 53 | local lfs = detect_lfs() 54 | if lfs then 55 | if not makeParents then 56 | -- no parents - just mkdir 57 | return lfs.mkdir(dir) 58 | else 59 | -- if then split up path into /'s and make sure each part exists or make it 60 | local parts = string.split(dir, '/') 61 | for i=(parts[1] == '' and 2 or 1),#parts do 62 | local parent = parts:sub(1, i):concat'/' 63 | if not os.fileexists(parent) then 64 | local results = table.pack(lfs.mkdir(parent)) 65 | if not results[1] then return results:unpack() end 66 | else 67 | -- TODO if it exists and it's not a dir then we're going to be in trouble. 68 | -- what does shell mkdir -p do in that case? 69 | end 70 | end 71 | return true 72 | end 73 | else 74 | -- fallback on shell 75 | local tonull 76 | if detect_os() then 77 | dir = os.path(dir) 78 | tonull = ' 2> nul' 79 | makeParents = nil -- mkdir in Windows always makes parents, and doesn't need a switch 80 | else 81 | tonull = ' 2> /dev/null' 82 | end 83 | local cmd = 'mkdir'..(makeParents and ' -p' or '')..' '..('%q'):format(dir)..tonull 84 | return os.execute(cmd) 85 | end 86 | end 87 | 88 | function os.rmdir(dir) 89 | local lfs = detect_lfs() 90 | if lfs then 91 | -- lfs 92 | return lfs.rmdir(dir) 93 | else 94 | -- shell 95 | local cmd = 'rmdir "'..os.path(dir)..'"' 96 | return os.execute(cmd) 97 | end 98 | end 99 | 100 | function os.move(from, to) 101 | -- WHY isn't this a part of luafilesystem? https://lunarmodules.github.io/luafilesystem/manual.html 102 | local detect_ffi = require 'ext.detect_ffi' 103 | local ffi = detect_ffi() 104 | if ffi then 105 | -- if we have ffi then we can use rename() 106 | local stdio = require 'ffi.req' 'c.stdio' 107 | local errno = require 'ffi.req' 'c.errno' 108 | if stdio.rename(from, to) == 0 then return true end 109 | return nil, errno.str() 110 | else 111 | -- [[ shell 112 | -- alternatively I could write this as readfile/writefile and os.remove 113 | from = os.path(from) 114 | to = os.path(to) 115 | local cmd = (detect_os() and 'move' or 'mv') .. ' "'..from..'" "'..to..'"' 116 | return os.execute(cmd) 117 | --]] 118 | --[[ worst case, rewrite it. 119 | local d = path(from):read() 120 | path(from):remove() -- remove first in case to and from match 121 | path(to):write(d) 122 | --]] 123 | end 124 | end 125 | 126 | function os.isdir(fn) 127 | local lfs = detect_lfs() 128 | if lfs then 129 | local attr = lfs.attributes(fn) 130 | if not attr then return false end 131 | return attr.mode == 'directory' 132 | else 133 | if detect_os() then 134 | return 'yes' == 135 | string.trim(io.readproc( 136 | 'if exist "' 137 | ..os.path(fn) 138 | ..'\\*" (echo yes) else (echo no)' 139 | )) 140 | else 141 | -- for OSX: 142 | -- TODO you could work around this for directories: 143 | -- f:read(1) for 5.1,jit,5.2,5.3 returns nil, 'Is a directory', 21 144 | local f = io.open(fn,'rb') 145 | if not f then return false end 146 | local result, reason, errcode = f:read(1) 147 | f:close() 148 | if result == nil 149 | and reason == 'Is a directory' 150 | and errcode == 21 151 | then 152 | return true 153 | end 154 | return false 155 | end 156 | end 157 | end 158 | 159 | function os.listdir(path) 160 | local lfs = detect_lfs() 161 | if not lfs then 162 | -- no lfs? use a fallback of shell ls or dir (based on OS) 163 | local fns 164 | -- all I'm using ffi for is reading the OS ... 165 | -- local detect_ffi = require 'ext.detect_ffi' 166 | -- local ffi = detect_ffi() -- no lfs? are you using luajit? 167 | -- if not ffi then 168 | -- if 'dir' exists ... 169 | -- local filestr = io.readproc('dir "'..path..'"') 170 | -- error('you are here: '..filestr) 171 | -- if 'ls' exists ... 172 | 173 | local cmd 174 | if detect_os() then 175 | cmd = 'dir /b "'..os.path(path)..'"' 176 | else 177 | cmd = 'ls -a '..path:gsub('[|&;<>`\"\' \t\r\n#~=%$%(%)%%%[%*%?]', [[\%0]]) 178 | end 179 | local filestr = io.readproc(cmd) 180 | fns = string.split(filestr, '\n') 181 | assert.eq(fns:remove(), '') 182 | if fns[1] == '.' then fns:remove(1) end 183 | if fns[1] == '..' then fns:remove(1) end 184 | --[[ 185 | else 186 | -- do a directory listing 187 | -- TODO escape? 188 | if ffi.os == 'Windows' then 189 | -- put your stupid FindFirstFile/FindNextFile code here 190 | error('windows sucks...') 191 | else 192 | fns = {} 193 | require 'ffi.req' 'c.dirent' 194 | -- https://stackoverflow.com/questions/10678522/how-can-i-get-this-readdir-code-sample-to-search-other-directories 195 | local dirp = ffi.C.opendir(path) 196 | if dirp == nil then 197 | error('failed to open dir '..path) 198 | end 199 | repeat 200 | local dp = ffi.C.readdir(dirp) 201 | if dp == nil then break end 202 | local name = ffi.string(dp[0].d_name) 203 | if name ~= '.' and name ~= '..' then 204 | table.insert(fns, name) 205 | end 206 | until false 207 | ffi.C.closedir(dirp) 208 | end 209 | end 210 | --]] 211 | return coroutine.wrap(function() 212 | for _,k in ipairs(fns) do 213 | --local fn = k:sub(1,1) == '/' and k or (path..'/'..k) 214 | coroutine.yield(k) 215 | end 216 | end) 217 | else 218 | return coroutine.wrap(function() 219 | for k in lfs.dir(path) do 220 | if k ~= '.' and k ~= '..' then 221 | --local fn = k:sub(1,1) == '/' and k or (path..'/'..k) 222 | -- I shouldn't have io.readfile for performance 223 | -- but for convenience it is so handy... 224 | coroutine.yield(k)--, io.readfile(fn)) 225 | end 226 | end 227 | end) 228 | end 229 | end 230 | 231 | --[[ recurse directory 232 | args: 233 | dir = directory to search from 234 | callback(filename, isdir) = optional callback to filter each file 235 | 236 | should this be in io or os? 237 | --]] 238 | function os.rlistdir(dir, callback) 239 | return coroutine.wrap(function() 240 | for f in os.listdir(dir) do 241 | local path = dir..'/'..f 242 | if os.isdir(path) then 243 | if not callback or callback(path, true) then 244 | for f in os.rlistdir(path, callback) do 245 | coroutine.yield(f) 246 | end 247 | end 248 | else 249 | if not callback or callback(path, false) then 250 | local fn = path 251 | if #fn > 2 and fn:sub(1,2) == './' then fn = fn:sub(3) end 252 | coroutine.yield(fn) 253 | end 254 | end 255 | end 256 | end) 257 | end 258 | 259 | function os.fileexists(fn) 260 | assert(fn, "expected filename") 261 | local lfs = detect_lfs() 262 | if lfs then 263 | return lfs.attributes(fn) ~= nil 264 | else 265 | if detect_os() then 266 | -- Windows reports 'false' to io.open for directories, so I can't use that ... 267 | return 'yes' == string.trim(io.readproc('if exist "'..os.path(fn)..'" (echo yes) else (echo no)')) 268 | else 269 | -- here's a version that works for OSX ... 270 | local f, err = io.open(fn, 'r') 271 | if not f then return false, err end 272 | f:close() 273 | return true 274 | end 275 | end 276 | end 277 | 278 | -- to complement os.getenv 279 | function os.home() 280 | local home = os.getenv'HOME' or os.getenv'USERPROFILE' 281 | if not home then return false, "failed to find environment variable HOME or USERPROFILE" end 282 | return home 283 | end 284 | 285 | return os 286 | -------------------------------------------------------------------------------- /path.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | path(pathtofile):open(mode) - to get a file handle 3 | path(pathtofile):read() - to read a file in entirety 4 | path(pathtofile):write() - to write to a file 5 | path(pathtofile):dir() - to iterate through a directory listing 6 | path(pathtofile):attr() - to get file attributes 7 | 8 | 9 | TODO 10 | 11 | - `path:cwd()` returns the *absolute* cwd, but `path` returns the directory `.` ... 12 | ... maybe path:cwd() should return `path'.'` and `path:abs()` should return the absolute path (using lfs.currentdir() for evaluation of '.') 13 | 14 | - maybe `path` shoudl be the class, so I can use `path:isa` instead of `path.class:isa` ? 15 | 16 | - right now path(a)(b)(c) is the same as path(a)/b/c 17 | ... maybe just use /'s and use call for something else? or don't use call at all? 18 | --]] 19 | 20 | 21 | -- [[ TODO - this block is also in ext/os.lua and ext/file.lua 22 | local detect_os = require 'ext.detect_os' 23 | local detect_lfs = require 'ext.detect_lfs' 24 | local assert = require 'ext.assert' 25 | 26 | local io = require 'ext.io' 27 | local os = require 'ext.os' 28 | local string = require 'ext.string' 29 | local class = require 'ext.class' 30 | 31 | 32 | -- TODO this goes in file or os or somewhere in ext 33 | local function simplifypath(s) 34 | local p = string.split(s, '/') 35 | for i=#p-1,1,-1 do 36 | -- convert a//b's to a/b 37 | if i > 1 then -- don't remove empty '' as the first entry - this signifies a root path 38 | while p[i] == '' do p:remove(i) end 39 | end 40 | -- convert a/./b's to a/b 41 | while p[i] == '.' do p:remove(i) end 42 | -- convert a/b/../c's to a/c 43 | if p[i+1] == '..' 44 | and p[i] ~= '..' 45 | then 46 | if i == 1 and p[1] == '' then 47 | error("/.. absolute + previous doesn't make sense: "..tostring(s)) -- don't allow /../ to remove the base / ... btw this is invalid anyways ... 48 | end 49 | p:remove(i) 50 | p:remove(i) 51 | end 52 | end 53 | -- remove trailing '.''s except the first 54 | while #p > 1 and p[#p] == '.' do 55 | p:remove() 56 | end 57 | return p:concat'/' 58 | end 59 | 60 | 61 | -- PREPEND the path if fn is relative, otherwise use fn 62 | -- I should reverse these arguments 63 | -- but this function is really specific to the Path path state variable 64 | local function appendPath(...) 65 | local fn, p = assert.types('appendPath', 2, 'string', 'string', ...) 66 | --[[ dont change to path sep ... always use / internally 67 | if detect_os() then 68 | fn = os.path(fn) 69 | if not ( 70 | fn:sub(1,1) == '\\' 71 | or fn:match'^[A-Z,a-z]:\\' 72 | ) then 73 | fn = p .. '\\' .. fn 74 | end 75 | else 76 | --]] 77 | if fn:sub(1,1) ~= '/' then 78 | fn = p .. '/' .. fn 79 | end 80 | --end 81 | fn = fn:gsub('/%./', '/') 82 | fn = fn:gsub('/+', '/') 83 | if #fn > 2 and fn:sub(1,2) == './' then 84 | fn = fn:sub(3) 85 | end 86 | return fn 87 | end 88 | 89 | 90 | local Path = class() 91 | 92 | --Path.sep = os.sep -- TOO redundant? 93 | 94 | function Path:init(args) 95 | self.path = assert.type( 96 | assert.type( 97 | args, 98 | 'table', 99 | 'Path:init args' 100 | ).path, 101 | 'string', 102 | 'Path:init args.path' 103 | ) 104 | assert.ne(self.path, nil) 105 | end 106 | 107 | -- wrappers 108 | local mappings = { 109 | [io] = { 110 | lines = 'lines', 111 | open = 'open', 112 | read = 'readfile', 113 | write = 'writefile', 114 | append = 'appendfile', 115 | --getdir = 'getfiledir', -- defined later, wrapped in Path 116 | --getext = 'getfileext', -- defined later, wrapped in Path 117 | }, 118 | [os] = { 119 | -- vanilla 120 | remove = 'remove', 121 | -- ext 122 | mkdir = 'mkdir', -- using os.mkdir instead of lfs.mkdir becasuse of fallbacks ... and 'makeParents' flag 123 | rmdir = 'rmdir', 124 | --move = 'move', -- defined later for picking out path from arg 125 | exists = 'fileexists', 126 | isdir = 'isdir', 127 | --dir = 'listdir', -- wrapping in path 128 | --rdir = 'rlistdir', 129 | 130 | -- TODO rename to 'fixpath'? 'fixsep'? 131 | fixpathsep = 'path', 132 | }, 133 | } 134 | local lfs = detect_lfs() 135 | if lfs then 136 | mappings[lfs] = { 137 | attr = 'attributes', 138 | symattr = 'symlinkattributes', 139 | cd = 'chdir', 140 | link = 'link', 141 | setmode = 'setmode', 142 | touch = 'touch', 143 | --cwd = 'currentdir', -- TODO how about some kind of cwd or something ... default 'file' obj path is '.', so how about relating this to the default. path storage? 144 | --mkdir = 'mkdir', -- in 'ext.os' 145 | --rmdir = 'rmdir', -- in 'ext.os' 146 | --lock = 'lock', -- in 'file' objects via ext.io.open 147 | --unlock = 'unlock', -- in 'file' objects via ext.io.open 148 | lockdir = 'lock_dir', -- can this be combined with lock() nah since lock() needs an open file handle. 149 | } 150 | end 151 | 152 | for obj,mapping in pairs(mappings) do 153 | for k,v in pairs(mapping) do 154 | Path[k] = function(self, ...) 155 | return obj[v](self.path, ...) 156 | end 157 | end 158 | end 159 | 160 | -- Path wrapping function, but return wraps in Path 161 | function Path:getdir(...) 162 | local dir, name = io.getfiledir(self.path, ...) 163 | return Path{path=dir}, Path{path=name} 164 | end 165 | 166 | -- Path wrapping 167 | function Path:getext(...) 168 | local base, ext = io.getfileext(self.path) 169 | return Path{path=base}, ext 170 | end 171 | 172 | -- [[ same as above but with non-lfs options. 173 | -- TODO put them in io or os like I am doing to abstract non-lfs stuff elsewhere? 174 | 175 | function Path:cwd() 176 | if lfs then 177 | return Path{path=lfs.currentdir()} 178 | else 179 | --[=[ TODO should I even bother with the non-lfs fallback? 180 | -- if so then use this: 181 | require 'ffi.req' 'c.stdlib' 182 | local dirp = unistd.getcwd(nil, 0) 183 | local dir = ffi.string(dirp) 184 | ffi.C.free(dirp) 185 | return dir 186 | --]=] 187 | if detect_os() then 188 | return Path{path=string.trim(io.readproc'cd')} 189 | else 190 | return Path{path=string.trim(io.readproc'pwd')} 191 | end 192 | end 193 | end 194 | --]] 195 | 196 | -- convert relative to absolute paths 197 | function Path:abs() 198 | if self.path:sub(1,1) == '/' then 199 | return self 200 | end 201 | return Path:cwd()/self 202 | end 203 | 204 | -- os.listdir wrapper 205 | 206 | function Path:move(to) 207 | if Path:isa(to) then to = to.path end 208 | return os.move(self.path, to) 209 | end 210 | 211 | function Path:dir() 212 | if not os.isdir(self.path) then 213 | error("can't dir() a non-directory: "..tostring(self.path)) 214 | end 215 | return coroutine.wrap(function() 216 | for fn in os.listdir(self.path) do 217 | coroutine.yield(Path{path=fn}) 218 | end 219 | end) 220 | end 221 | 222 | function Path:rdir(callback) 223 | if not os.isdir(self.path) then 224 | error("can't rdir() a non-directory: "..tostring(self.path)) 225 | end 226 | return coroutine.wrap(function() 227 | for fn in os.rlistdir(self.path, callback) do 228 | coroutine.yield(Path{path=fn}) 229 | end 230 | end) 231 | end 232 | 233 | 234 | -- shorthand for splitting off ext and replacing it 235 | -- Path:getext() splits off the last '.' and returns the letters after it 236 | -- but for files with no ext it returns nil afterwards 237 | -- so a file with only a single '.' at the end will produce a '' for an ext 238 | -- and a file with no '.' will produce ext nil 239 | -- so for compat, handle it likewise 240 | -- for newext == nil, remove the last .ext from the filename 241 | -- for newext == "", replace the last .ext with just a . 242 | function Path:setext(newext) 243 | local base = self:getext().path 244 | if newext then 245 | base = base .. '.' .. newext 246 | end 247 | return Path{path=base} 248 | end 249 | 250 | -- iirc setting __index and __newindex outside :init() is tough, since so much writing is still going on 251 | --[[ 252 | TODO how to do the new interface? 253 | specifically reading vs writing? 254 | 255 | instead of __newindex for writing new files, how about path(path):write() 256 | --]] 257 | function Path:__call(k) 258 | assert.ne(self.path, nil) 259 | if k == nil then return self end 260 | if Path:isa(k) then k = k.path end 261 | local fn = assert.type( 262 | appendPath(k, self.path), 263 | 'string', 264 | "Path:__call appendPath(k, self.path)") 265 | -- is this safe? 266 | fn = simplifypath(fn) 267 | 268 | -- one option is checking existence and returning nil if no file 269 | -- but then how about file writing? 270 | 271 | return Path{ 272 | path = assert.type(fn, 'string', "Path:__call simplifypath"), 273 | } 274 | end 275 | 276 | -- clever stl idea: path(a)/path(b) = path(a..'/'..b) 277 | Path.__div = Path.__call 278 | 279 | -- return the path but for whatever OS we're using 280 | function Path:__tostring() 281 | return self:fixpathsep() 282 | end 283 | 284 | -- This is intended for shell / cmdline use 285 | -- TODO it doesn't need to quote if there are no special characters present 286 | -- also TODO make sure its escaping matches up with whatever OS is being used 287 | function Path:escape() 288 | return('%q'):format(self:fixpathsep()) 289 | end 290 | 291 | Path.__concat = string.concat 292 | 293 | -- don't coerce strings just yet 294 | -- don't coerce abs path either 295 | function Path.__eq(a,b) return a.path == b.path end 296 | function Path.__lt(a,b) return a.path < b.path end 297 | function Path.__le(a,b) return a.path <= b.path end 298 | 299 | local pathSys = Path{path='.'} 300 | 301 | return pathSys 302 | -------------------------------------------------------------------------------- /range.lua: -------------------------------------------------------------------------------- 1 | local table = require 'ext.table' 2 | 3 | -- for-loop to generate a table 4 | -- TODO just use lua-fun and coroutines and iterators. don't do this. 5 | local function range(a,b,c) 6 | local t = table() 7 | if c then 8 | for x=a,b,c do 9 | t:insert(x) 10 | end 11 | elseif b then 12 | for x=a,b do 13 | t:insert(x) 14 | end 15 | else 16 | for x=1,a do 17 | t:insert(x) 18 | end 19 | end 20 | return t 21 | end 22 | 23 | return range 24 | -------------------------------------------------------------------------------- /reload.lua: -------------------------------------------------------------------------------- 1 | -- this goes wherever packages and the likes would go 2 | -- it would be awesome to track loaded dependencies and auto-reload them too 3 | -- should be easy to do by overriding 'require' 4 | local function reload(n) 5 | package.loaded[n] = nil 6 | return require(n) 7 | end 8 | 9 | return reload 10 | -------------------------------------------------------------------------------- /require.lua: -------------------------------------------------------------------------------- 1 | -- pure-lua require since the luajit one has C-call boundary yield problems (maybe? 10yo bug? https://github.com/openresty/lua-nginx-module/issues/376#issuecomment-46104284 ) 2 | return function(env) 3 | env = env or _G 4 | --local oldrequire = env.require 5 | local package = env.package 6 | env.require = function(modname) 7 | local mod = package.loaded[modname] 8 | if mod ~= nil then return mod end 9 | local errs = {"module '"..tostring(modname).."' not found:"} 10 | local searchers = assert(package.searchers or package.loaders, "couldn't find searchers") 11 | for _,search in ipairs(searchers) do 12 | local result = search(modname) 13 | local resulttype = type(result) 14 | if resulttype == 'function' then 15 | local mod = result() or true 16 | package.loaded[modname] = mod 17 | return mod 18 | elseif resulttype == 'string' or resulttype == 'nil' then 19 | table.insert(errs, result) -- if I get a nil, should I convert it into an error? 20 | else 21 | error("package.searchers result got an unknown type: "..resulttype) 22 | end 23 | end 24 | error(table.concat(errs)) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /string.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | notice that, 3 | while this does override the 'string' and add some extra stuff, 4 | it does not explicitly replace the default string metatable __index 5 | to do that, require 'ext.meta' (or do it yourself) 6 | --]] 7 | local string = {} 8 | for k,v in pairs(require 'string') do string[k] = v end 9 | 10 | local table = require 'ext.table' 11 | 12 | -- table.concat(string.split(a,b),b) == a 13 | function string.split(s, exp) 14 | exp = exp or '' 15 | s = tostring(s) 16 | local t = table() 17 | -- handle the exp='' case 18 | if exp == '' then 19 | for i=1,#s do 20 | t:insert(s:sub(i,i)) 21 | end 22 | else 23 | local searchpos = 1 24 | local start, fin = s:find(exp, searchpos) 25 | while start do 26 | t:insert(s:sub(searchpos, start-1)) 27 | searchpos = fin+1 28 | start, fin = s:find(exp, searchpos) 29 | end 30 | t:insert(s:sub(searchpos)) 31 | end 32 | return t 33 | end 34 | 35 | function string.trim(s) 36 | return s:match('^%s*(.-)%s*$') 37 | end 38 | 39 | -- should this wrap in a table? 40 | function string.bytes(s) 41 | return table{s:byte(1,#s)} 42 | end 43 | 44 | string.load = load or loadstring 45 | 46 | --[[ 47 | -- drifting further from standards... 48 | -- this string-converts everything concat'd (no more errors, no more print(a,b,c)'s) 49 | getmetatable('').__concat = function(a,b) 50 | return tostring(a)..tostring(b) 51 | end 52 | --]] 53 | 54 | -- a C++-ized accessor to subsets 55 | -- indexes are zero-based inclusive 56 | -- sizes are zero-based-exclusive (or one-based-inclusive depending on how you think about it) 57 | -- parameters are (index, size) rather than (start index, end index) 58 | function string.csub(d, start, size) 59 | if not size then return string.sub(d, start + 1) end -- til-the-end 60 | return string.sub(d, start + 1, start + size) 61 | end 62 | 63 | --d = string data 64 | --l = length of a column. default 32 65 | --w = hex word size. default 1 66 | --c = extra column space. default 8 67 | function string.hexdump(d, l, w, c) 68 | d = tostring(d) 69 | l = tonumber(l) 70 | w = tonumber(w) 71 | c = tonumber(c) 72 | if not l or l < 1 then l = 32 end 73 | if not w or w < 1 then w = 1 end 74 | if not c or c < 1 then c = 8 end 75 | local s = table() 76 | local rhs = table() 77 | local col = 0 78 | for i=1,#d,w do 79 | if i % l == 1 then 80 | s:insert(string.format('%.8x ', (i-1))) 81 | rhs = table() 82 | col = 1 83 | end 84 | s:insert' ' 85 | for j=w,1,-1 do 86 | local e = i+j-1 87 | local sub = d:sub(e,e) 88 | if #sub > 0 then 89 | local b = string.byte(sub) 90 | s:insert(string.format('%.2x', b)) 91 | rhs:insert(b >= 32 and sub or '.') 92 | end 93 | end 94 | if col % c == 0 then 95 | s:insert' ' 96 | end 97 | if (i + w - 1) % l == 0 or i+w>#d then 98 | s:insert' ' 99 | s:insert(rhs:concat()) 100 | end 101 | if (i + w - 1) % l == 0 then 102 | s:insert'\n' 103 | end 104 | col = col + 1 105 | end 106 | return s:concat() 107 | end 108 | 109 | -- escape for pattern matching 110 | local escapeFind = '[' .. ([[^$()%.[]*+-?]]):gsub('.', '%%%1') .. ']' 111 | function string.patescape(s) 112 | return (s:gsub(escapeFind, '%%%1')) 113 | end 114 | 115 | -- this is a common function, especially as a __concat metamethod 116 | -- it is nearly table.concat, except table.concat errors upon non-string/number instead of calling tostring() automatically 117 | -- (should I change table.concat's default behavior and use that instead? nah, because why require a table creation.) 118 | -- tempted to make this ext.op.concat ... but that's specifically a binary op ... and that shouldn't call tostring() while this should ... 119 | -- maybe I should move this to ext.op as 'tostringconcat' or something? 120 | function string.concat(...) 121 | local n = select('#', ...) 122 | if n == 0 then return end -- base-case nil or "" ? 123 | local s = tostring((...)) 124 | if n == 1 then return s end 125 | return s .. string.concat(select(2, ...)) 126 | end 127 | 128 | -- another common __tostring metamethod 129 | -- since luajit doesn't support __name metafield 130 | function string:nametostring() 131 | -- NOTICE this will break for anything that overrides its __metatable metafield 132 | local mt = getmetatable(self) 133 | 134 | -- invoke a 'rawtostring' call / get the builtin 'tostring' result 135 | setmetatable(self, nil) 136 | local s = tostring(self) 137 | setmetatable(self, mt) 138 | 139 | local name = mt.__name 140 | return name and tostring(name)..s:sub(6) or s 141 | end 142 | 143 | return string 144 | -------------------------------------------------------------------------------- /table.lua: -------------------------------------------------------------------------------- 1 | local table = {} 2 | for k,v in pairs(require 'table') do table[k] = v end 3 | 4 | table.__index = table 5 | 6 | function table.new(...) 7 | return setmetatable({}, table):union(...) 8 | end 9 | 10 | setmetatable(table, { 11 | __call = function(t, ...) 12 | return table.new(...) 13 | end 14 | }) 15 | 16 | -- 5.2 or 5.3 compatible 17 | table.unpack = table.unpack or unpack 18 | 19 | -- [[ how about table.unpack(t) defaults to table.unpack(t, 1, t.n) if t.n is present? 20 | -- for cohesion with table.pack? 21 | -- already table.unpack's default is #t, but this doesn't account for nils 22 | -- this might break compatability somewhere ... 23 | local origTableUnpack = table.unpack 24 | function table.unpack(...) 25 | local nargs = select('#', ...) 26 | local t, i, j = ... 27 | if nargs < 3 and t.n ~= nil then 28 | return origTableUnpack(t, i or 1, t.n) 29 | end 30 | return origTableUnpack(...) 31 | end 32 | --]] 33 | 34 | -- 5.1 compatible 35 | if not table.pack then 36 | function table.pack(...) 37 | local t = {...} 38 | t.n = select('#', ...) 39 | return setmetatable(t, table) 40 | end 41 | else 42 | local oldpack = table.pack 43 | function table.pack(...) 44 | return setmetatable(oldpack(...), table) 45 | end 46 | end 47 | 48 | -- non-5.1 compat: 49 | if not table.maxn then 50 | function table.maxn(t) 51 | local max = 0 52 | for k,v in pairs(t) do 53 | if type(k) == 'number' then 54 | max = math.max(max, k) 55 | end 56 | end 57 | return max 58 | end 59 | end 60 | 61 | -- applies to the 'self' table 62 | -- same behavior as new 63 | function table:union(...) 64 | for i=1,select('#', ...) do 65 | local o = select(i, ...) 66 | if o then 67 | for k,v in pairs(o) do 68 | self[k] = v 69 | end 70 | end 71 | end 72 | return self 73 | end 74 | 75 | -- something to consider: 76 | -- mapvalue() returns a new table 77 | -- but append() modifies the current table 78 | -- for consistency shouldn't append() create a new one as well? 79 | function table:append(...) 80 | for i=1,select('#', ...) do 81 | local u = select(i, ...) 82 | if u then 83 | for _,v in ipairs(u) do 84 | table.insert(self, v) 85 | end 86 | end 87 | end 88 | return self 89 | end 90 | 91 | function table:removeKeys(...) 92 | for i=1,select('#', ...) do 93 | local v = select(i, ...) 94 | self[v] = nil 95 | end 96 | end 97 | 98 | -- cb(value, key, newtable) returns newvalue[, newkey] 99 | -- nil newkey means use the old key 100 | function table:map(cb) 101 | local t = table() 102 | for k,v in pairs(self) do 103 | local nv, nk = cb(v,k,t) 104 | if nk == nil then nk = k end 105 | t[nk] = nv 106 | end 107 | return t 108 | end 109 | 110 | -- cb(value, key, newtable) returns newvalue[, newkey] 111 | -- nil newkey means use the old key 112 | function table:mapi(cb) 113 | local t = table() 114 | for k=1,#self do 115 | local v = self[k] 116 | local nv, nk = cb(v,k,t) 117 | if nk == nil then nk = k end 118 | t[nk] = nv 119 | end 120 | return t 121 | end 122 | 123 | -- this excludes keys that don't pass the callback function 124 | -- if the key is an ineteger then it is table.remove'd 125 | -- currently the handling of integer keys is the only difference between this 126 | -- and calling table.map and returning nil kills on filtered items 127 | function table:filter(f) 128 | local t = table() 129 | if type(f) == 'function' then 130 | for k,v in pairs(self) do 131 | if f(v,k) then 132 | -- TODO instead of this at runtime, how about filter vs filteri like map vs mapi 133 | -- but most the times filter is used it is for integers already 134 | -- how about filterk? or probably filteri and change everything 135 | if type(k) == 'string' then 136 | t[k] = v 137 | else 138 | t:insert(v) 139 | end 140 | end 141 | end 142 | else 143 | -- I kind of want to do arrays ... but should we be indexing the keys or values? 144 | -- or separate functions for each? 145 | error('table.filter second arg must be a function') 146 | end 147 | return t 148 | end 149 | 150 | function table:keys() 151 | local t = table() 152 | for k,_ in pairs(self) do 153 | t:insert(k) 154 | end 155 | return t 156 | end 157 | 158 | function table:values() 159 | local t = table() 160 | for _,v in pairs(self) do 161 | t:insert(v) 162 | end 163 | return t 164 | end 165 | 166 | -- should we have separate finds for pairs and ipairs? 167 | -- should we also return value, key to match map, sup, and inf? 168 | -- that seems redundant if it's find-by-value ... 169 | function table:find(value, eq) 170 | if eq then 171 | for k,v in pairs(self) do 172 | if eq(v, value) then return k, v end 173 | end 174 | else 175 | for k,v in pairs(self) do 176 | if v == value then return k, v end 177 | end 178 | end 179 | end 180 | 181 | -- should insertUnique only operate on the pairs() ? 182 | -- especially when insert() itself is an ipairs() operation 183 | function table:insertUnique(value, eq) 184 | if not table.find(self, value, eq) then table.insert(self, value) end 185 | end 186 | 187 | function table:removeObject(...) 188 | local removedKeys = table() 189 | local len = #self 190 | local k = table.find(self, ...) 191 | while k ~= nil do 192 | if type(k) == 'number' and tonumber(k) <= len then 193 | table.remove(self, k) 194 | else 195 | self[k] = nil 196 | end 197 | removedKeys:insert(k) 198 | k = table.find(self, ...) 199 | end 200 | return table.unpack(removedKeys) 201 | end 202 | 203 | function table:kvpairs() 204 | local t = table() 205 | for k,v in pairs(self) do 206 | table.insert(t, {[k]=v}) 207 | end 208 | return t 209 | end 210 | 211 | -- TODO - math instead of table? 212 | -- TODO - have cmp default to operator> just like inf and sort? 213 | function table:sup(cmp) 214 | local bestk, bestv 215 | if cmp then 216 | for k,v in pairs(self) do 217 | if bestv == nil or cmp(v, bestv) then bestk, bestv = k, v end 218 | end 219 | else 220 | for k,v in pairs(self) do 221 | if bestv == nil or v > bestv then bestk, bestv = k, v end 222 | end 223 | end 224 | return bestv, bestk 225 | end 226 | 227 | -- TODO - math instead of table? 228 | function table:inf(cmp) 229 | local bestk, bestv 230 | if cmp then 231 | for k,v in pairs(self) do 232 | if bestv == nil or cmp(v, bestv) then bestk, bestv = k, v end 233 | end 234 | else 235 | for k,v in pairs(self) do 236 | if bestv == nil or v < bestv then bestk, bestv = k, v end 237 | end 238 | end 239 | return bestv, bestk 240 | end 241 | 242 | -- combine elements of 243 | function table:combine(callback) 244 | local s 245 | for _,v in pairs(self) do 246 | if s == nil then 247 | s = v 248 | else 249 | s = callback(s, v) 250 | end 251 | end 252 | return s 253 | end 254 | 255 | local op = require 'ext.op' 256 | 257 | function table:sum() 258 | return table.combine(self, op.add) 259 | end 260 | 261 | function table:product() 262 | return table.combine(self, op.mul) 263 | end 264 | 265 | function table:last() 266 | return self[#self] 267 | end 268 | 269 | -- just like string subset 270 | function table.sub(t,i,j) 271 | if i < 0 then i = math.max(1, #t + i + 1) end 272 | --if i < 0 then i = math.max(1, #t + i + 1) else i = math.max(1, i) end -- TODO this is affecting symmath edge cases somewhere ... 273 | j = j or #t 274 | j = math.min(j, #t) 275 | if j < 0 then j = math.min(#t, #t + j + 1) end 276 | --if j < 0 then j = math.min(#t, #t + j + 1) else j = math.max(1, j) end -- TODO this is affecting symmath edge cases somewhere ... 277 | local res = {} 278 | for k=i,j do 279 | res[k-i+1] = t[k] 280 | end 281 | setmetatable(res, table) 282 | return res 283 | end 284 | 285 | function table.reverse(t) 286 | local r = table() 287 | for i=#t,1,-1 do 288 | r:insert(t[i]) 289 | end 290 | return r 291 | end 292 | 293 | function table.rep(t,n) 294 | local c = table() 295 | for i=1,n do 296 | c:append(t) 297 | end 298 | return c 299 | end 300 | 301 | -- in-place sort is fine, but it returns nothing. for kicks I'd like to chain methods 302 | local oldsort = require 'table'.sort 303 | function table:sort(...) 304 | oldsort(self, ...) 305 | return self 306 | end 307 | 308 | -- returns a shuffled duplicate of the ipairs in table 't' 309 | function table.shuffle(t) 310 | t = table(t) 311 | -- https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle 312 | for i=#t,2,-1 do 313 | local j = math.random(i-1) 314 | t[i], t[j] = t[j], t[i] 315 | end 316 | return t 317 | end 318 | 319 | function table.pickRandom(t) 320 | return t[math.random(#t)] 321 | end 322 | 323 | -- keys = options, values = probs 324 | function table.pickWeighted(t) 325 | local total = table.values(t):sum() 326 | local x = math.random() * total 327 | for k,v in pairs(t) do 328 | x = x - v 329 | if x <= 0 then 330 | return k 331 | end 332 | end 333 | -- this should never be reached unless you have some not-a-number's as values 334 | end 335 | 336 | -- where to put this ... 337 | -- I want to convert iterators into tables 338 | -- it looks like a coroutine but it is made for functions returned from coroutine.wrap 339 | -- also, what to do with multiple-value iterators (like ipairs) 340 | -- do I only wrap the first value? 341 | -- do I wrap both values in a double table? 342 | -- do I do it optionally based on the # args returned? 343 | -- how about I ask for a function to convert the iterator to the table? 344 | -- this is looking very similar to table.map 345 | -- I'll just wrap it with table.wrap and then let the caller use :mapi to transform the results 346 | -- usage: table.wrapfor(ipairs(t)) 347 | -- if you want to wrap a 'for=' loop then just use range(a,b[,c]) 348 | -- ok at this point I should just start using lua-fun ... 349 | function table.wrapfor(f, s, var) 350 | local t = table() 351 | while true do 352 | local vars = table.pack(f(s, var)) 353 | local var_1 = vars[1] 354 | if var_1 == nil then break end 355 | var = var_1 356 | t:insert(vars) 357 | end 358 | return t 359 | end 360 | 361 | -- https://www.lua.org/pil/9.3.html 362 | local function permgen(t, n) 363 | if n < 1 then 364 | coroutine.yield(t) 365 | else 366 | for i=n,1,-1 do 367 | -- put i-th element as the last one 368 | t[n], t[i] = t[i], t[n] 369 | -- generate all permutations of the other elements 370 | permgen(t, n - 1) 371 | -- restore i-th element 372 | t[n], t[i] = t[i], t[n] 373 | end 374 | end 375 | end 376 | 377 | -- return iterator of permutations of the table 378 | function table.permutations(t) 379 | return coroutine.wrap(function() 380 | permgen(t, table.maxn(t)) 381 | end) 382 | end 383 | 384 | -- I won't add table.getmetatable because, as a member method, that will always return 'table' 385 | 386 | -- if you use this as a member method then know that you can't use it a second time (unless the metatable you set it to has a __index that has 'setmetatable' defined) 387 | table.setmetatable = setmetatable 388 | 389 | return table 390 | -------------------------------------------------------------------------------- /timer.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | I use this too often, so now it goes here 3 | 4 | override T.getTime to change the timing function 5 | override T.out to change the output file handle 6 | --]] 7 | 8 | local hasffi, ffi = pcall(require, 'ffi') 9 | 10 | local T = {} 11 | 12 | T.out = io.stderr 13 | 14 | if not hasffi or ffi.os == 'Windows' then 15 | -- in linux this is the live time, or something other than the actual time 16 | T.getTime = os.clock 17 | else 18 | require 'ffi.req' 'c.sys.time' -- gettimeofday 19 | require 'ffi.req' 'c.string' -- strerror 20 | local errno = require 'ffi.req' 'c.errno' 21 | local gettimeofday_tv = ffi.new'struct timeval[1]' 22 | function T.getTime() 23 | local result = ffi.C.gettimeofday(gettimeofday_tv, nil) 24 | if result ~= 0 then 25 | error(ffi.string(ffi.C.strerror(errno.errno()))) 26 | end 27 | return tonumber(gettimeofday_tv[0].tv_sec) + tonumber(gettimeofday_tv[0].tv_usec) / 1000000 28 | end 29 | end 30 | 31 | T.depth = 0 32 | T.tab = ' ' 33 | 34 | local function timerReturn(name, startTime, indent, ...) 35 | local endTime = T.getTime() 36 | local dt = endTime - startTime 37 | 38 | -- this is all printing ... 39 | T.depth = T.depth - 1 40 | T.out:write(indent..'...done ') 41 | if name then 42 | T.out:write(name..' ') 43 | end 44 | T.out:write('('..dt..'s)\n') 45 | T.out:flush() 46 | 47 | return dt, ... 48 | end 49 | 50 | function T.timer(name, cb, ...) 51 | local indent = T.tab:rep(T.depth) 52 | if name then 53 | T.out:write(indent..name..'...\n') 54 | end 55 | T.out:flush() 56 | local startTime = T.getTime() 57 | T.depth = T.depth + 1 58 | return timerReturn(name, startTime, indent, cb(...)) 59 | end 60 | 61 | 62 | -- same as above but without printing (and no name too cuz we're not printing) 63 | local function timerReturnQuiet(startTime, ...) 64 | local endTime = T.getTime() 65 | local dt = endTime - startTime 66 | return dt, ... 67 | end 68 | 69 | function T.timerQuiet(cb, ...) 70 | local startTime = T.getTime() 71 | return timerReturnQuiet(startTime, cb(...)) 72 | end 73 | 74 | 75 | setmetatable(T, { 76 | -- call forwards to timer: 77 | __call = function(self, ...) 78 | return self.timer(...) 79 | end, 80 | }) 81 | 82 | return T 83 | -------------------------------------------------------------------------------- /tolua.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | how to handle recursion ... 3 | a={} 4 | b={} 5 | a.b=b 6 | b.a=a 7 | 8 | tolua(a) would give ... 9 | {b={a=error('recursive reference')}} 10 | 11 | but how about, if something is found that is marked in touched tables ... 12 | 1) wrap everything in a function block 13 | 2) give root a local 14 | 3) add assignments of self-references after-the-fact 15 | 16 | (function() 17 | local _tmp={b={}} 18 | _tmp.b.a= _tmp 19 | return _tmp 20 | end)() 21 | --]] 22 | 23 | local table = require 'ext.table' 24 | 25 | local function builtinPairs(t) 26 | return next,t,nil 27 | end 28 | 29 | local _0byte = ('0'):byte() 30 | local _9byte = ('9'):byte() 31 | local function escapeString(s) 32 | --[[ multiline strings 33 | -- it seems certain chars can't be encoded in Lua multiline strings 34 | -- TODO find out exactly which ones 35 | -- TODO if 's' begins with [ or ends with ] then you're gonna have a bad time ... 36 | -- in fact ... does Lua support multiline strings that begin with [ or end with ] ? especially the latter ... 37 | local foundNewline 38 | local foundBadChar 39 | for i=1,#s do 40 | local b = s:byte(i) 41 | if b == 10 42 | -- TODO still lua loading will convert \r \n's into \n's ... so this is still not guaranteed to reproduce the original ... 43 | -- I could disable multi line strs when encoding \r's ... 44 | -- but this gets to be a bit os-specific ... 45 | -- a better solution would be changing fromlua() to properly handle newline formatting within multiline strings ... 46 | --or b == 13 47 | then 48 | foundNewline = true 49 | elseif b < 32 or b > 126 then 50 | foundBadChar = true 51 | break -- don't need to keep looking 52 | end 53 | end 54 | if foundNewline and not foundBadChar then 55 | for neq=0,math.huge do 56 | local eq = ('='):rep(neq) 57 | local open = '['..eq..'[' 58 | local close = ']'..eq..']' 59 | if not s:find(open, 1, true) 60 | and not s:find(close, 1, true) 61 | and s:sub(-neq-1) ~= close:sub(1,neq+1) -- if the very end of the string is close without the last ] then it could still do a false match ... 62 | then 63 | -- open and close aren't in the string, we can use this to escape the string 64 | -- ... ig all I have to search for is close, but meh 65 | local ret = open .. '\n' -- \n cuz lua ignores trailing spaces/newline after the opening 66 | .. s .. close 67 | --DEBUG: require 'ext.assert'.eq(load('return '..ret)(), s) 68 | return ret 69 | end 70 | end 71 | end 72 | --]] 73 | 74 | -- [[ 75 | -- this will only escape escape codes 76 | -- will respect unicode 77 | -- but it skips \r \t and encodes them as \13 \9 78 | local o = ('%q'):format(s) 79 | o = o:gsub('\\\n','\\n') 80 | return o 81 | --]] 82 | --[==[ this gets those that builtin misses 83 | -- but does it in lua so it'll be slow 84 | -- and requires implementations of iscntrl and isdigit 85 | -- 86 | -- it's slow and has bugs. 87 | -- 88 | -- TODO 89 | -- for size-minimal strings: 90 | -- if min(# single-quotes, # double-quotes) within the string > 2 then use [[ ]] (so long as that isn't used either) 91 | -- otherwise use as quotes whatever the min is 92 | -- or ... use " to wrap if less than 1 " is embedded 93 | -- then use ' to wrap if less than 1 ' is embedded 94 | -- then use [[ ]] to wrap if no [[ ]] is embedded 95 | -- ... etc for [=...=[ all string escape options 96 | local o = '"' 97 | for i=1,#s do 98 | local c = s:sub(i,i) 99 | if c == '"' then 100 | o = o .. '\\"' 101 | elseif c == '\\' then 102 | o = o .. '\\\\' 103 | elseif c == '\n' then 104 | o = o .. '\\n' 105 | elseif c == '\r' then 106 | o = o .. '\\r' 107 | elseif c == '\t' then 108 | o = o .. '\\t' 109 | elseif c == '\a' then 110 | o = o .. '\\a' 111 | elseif c == '\b' then 112 | o = o .. '\\b' 113 | elseif c == '\f' then 114 | o = o .. '\\f' 115 | elseif c == '\v' then 116 | o = o .. '\\v' 117 | else 118 | local b = c:byte() 119 | assert(b < 256) 120 | if b < 0x20 or b == 0x7f then -- if iscntrl(c) 121 | -- make sure the next character isn't a digit because that will mess up the encoded escape code 122 | local b2 = c:byte(i+1) 123 | if not (b2 and b2 >= _0byte and b2 <= _9byte) then -- if not isdigit(c2) then 124 | o = o .. ('\\%d'):format(b) 125 | else 126 | o = o .. ('\\%03d'):format(b) 127 | end 128 | else 129 | -- TODO for extended ascii, why am I seeing different things here vs encoding one character at a time? 130 | o = o .. c 131 | end 132 | end 133 | end 134 | o = o .. '"' 135 | o:gsub('\\(%d%d%d)', function(d) 136 | if tonumber(d) > 255 then 137 | print('#s', #s) 138 | print'o' 139 | print(o) 140 | print's' 141 | print(s) 142 | error("got an oob escape code: "..d) 143 | end 144 | end) 145 | local f = require 'ext.fromlua'(o) 146 | if f ~= s then 147 | print('#s', #s) 148 | print('#f', #f) 149 | print'o' 150 | print(o) 151 | print's' 152 | print(s) 153 | print'f' 154 | print(f) 155 | print("failed to reencode as the same string") 156 | for i=1,math.min(#s,#f) do 157 | if f:sub(i,i) ~= s:sub(i,i) then 158 | print('char '..i..' differs') 159 | break 160 | end 161 | end 162 | error("here") 163 | 164 | end 165 | return o 166 | --]==] 167 | end 168 | 169 | -- as of 5.4. I could modify this based on the Lua version (like removing 'goto') but misfiring just means wrapping in quotes, so meh. 170 | local reserved = { 171 | ["and"] = true, 172 | ["break"] = true, 173 | ["do"] = true, 174 | ["else"] = true, 175 | ["elseif"] = true, 176 | ["end"] = true, 177 | ["false"] = true, 178 | ["for"] = true, 179 | ["function"] = true, 180 | ["goto"] = true, 181 | ["if"] = true, 182 | ["in"] = true, 183 | ["local"] = true, 184 | ["nil"] = true, 185 | ["not"] = true, 186 | ["or"] = true, 187 | ["repeat"] = true, 188 | ["return"] = true, 189 | ["then"] = true, 190 | ["true"] = true, 191 | ["until"] = true, 192 | ["while"] = true, 193 | } 194 | 195 | -- returns 'true' if k is a valid variable name, but not a reserved keyword 196 | local function isVarName(k) 197 | return type(k) == 'string' and k:match('^[_a-zA-Z][_a-zA-Z0-9]*$') and not reserved[k] 198 | end 199 | 200 | local toLuaRecurse 201 | 202 | local function toLuaKey(state, k, path) 203 | if isVarName(k) then 204 | return k, true 205 | else 206 | local result = toLuaRecurse(state, k, nil, path, true) 207 | if result then 208 | return '['..result..']', false 209 | else 210 | return false, false 211 | end 212 | end 213 | end 214 | 215 | 216 | -- another copy of maxn, with custom pairs 217 | local function maxn(t, state) 218 | local max = 0 219 | local count = 0 220 | for k,v in state.pairs(t) do 221 | count = count + 1 222 | if type(k) == 'number' then 223 | max = math.max(max, k) 224 | end 225 | end 226 | return max, count 227 | end 228 | 229 | 230 | local defaultSerializeForType = { 231 | number = function(state,x) 232 | if x == math.huge then return 'math.huge' end 233 | if x == -math.huge then return '-math.huge' end 234 | if x ~= x then return '0/0' end 235 | return tostring(x) 236 | end, 237 | boolean = function(state,x) return tostring(x) end, 238 | ['nil'] = function(state,x) return tostring(x) end, 239 | string = function(state,x) return escapeString(x) end, 240 | ['function'] = function(state, x) 241 | local result, s = pcall(string.dump, x) 242 | 243 | if result then 244 | s = 'load('..escapeString(s)..')' 245 | else 246 | -- if string.dump failed then check the builtins 247 | -- check the global object and one table deep 248 | -- todo maybe, check against a predefined set of functions? 249 | if s == "unable to dump given function" then 250 | local found 251 | for k,v in state.pairs(_G) do 252 | if v == x then 253 | found = true 254 | s = k 255 | break 256 | elseif type(v) == 'table' then 257 | -- only one level deep ... 258 | for k2,v2 in state.pairs(v) do 259 | if v2 == x then 260 | s = k..'.'..k2 261 | found = true 262 | break 263 | end 264 | end 265 | if found then break end 266 | end 267 | end 268 | if not found then 269 | s = "error('"..s.."')" 270 | end 271 | else 272 | return "error('got a function I could neither dump nor lookup in the global namespace nor one level deep')" 273 | end 274 | end 275 | 276 | return s 277 | end, 278 | table = function(state, x, tab, path, keyRef) 279 | local result 280 | 281 | local newtab = tab .. state.indentChar 282 | -- TODO override for specific metatables? as I'm doing for types? 283 | 284 | if state.touchedTables[x] then 285 | if state.skipRecursiveReferences then 286 | result = 'error("recursive reference")' 287 | else 288 | result = false -- false is used internally and means recursive reference 289 | state.wrapWithFunction = true 290 | 291 | -- we're serializing *something* 292 | -- is it a value? if so, use the 'path' to dereference the key 293 | -- is it a key? if so the what's the value .. 294 | -- do we have to add an entry for both? 295 | -- maybe the caller should be responsible for populating this table ... 296 | if keyRef then 297 | state.recursiveReferences:insert('root'..path..'['..state.touchedTables[x]..'] = error("can\'t handle recursive references in keys")') 298 | else 299 | state.recursiveReferences:insert('root'..path..' = '..state.touchedTables[x]) 300 | end 301 | end 302 | else 303 | state.touchedTables[x] = 'root'..path 304 | 305 | -- prelim see if we can write it as an indexed table 306 | local numx, count = maxn(x, state) 307 | local intNilKeys, intNonNilKeys 308 | -- only count if our max isn't too high 309 | if numx < 2 * count then 310 | intNilKeys, intNonNilKeys = 0, 0 311 | for i=1,numx do 312 | if x[i] == nil then 313 | intNilKeys = intNilKeys + 1 314 | else 315 | intNonNilKeys = intNonNilKeys + 1 316 | end 317 | end 318 | end 319 | 320 | local hasSubTable 321 | 322 | local s = table() 323 | 324 | -- add integer keys without keys explicitly. nil-padded so long as there are 2x values than nils 325 | local addedIntKeys = {} 326 | if intNonNilKeys 327 | and intNilKeys 328 | and intNonNilKeys >= intNilKeys * 2 329 | then -- some metric for when to create in-order tables 330 | for k=1,numx do 331 | if type(x[k]) == 'table' then hasSubTable = true end 332 | local nextResult = toLuaRecurse(state, x[k], newtab, path and path..'['..k..']') 333 | if nextResult then 334 | s:insert(nextResult) 335 | -- else x[k] is a recursive reference 336 | end 337 | addedIntKeys[k] = true 338 | end 339 | end 340 | 341 | -- sort key/value pairs added here by key 342 | local mixed = table() 343 | for k,v in state.pairs(x) do 344 | if not addedIntKeys[k] then 345 | if type(v) == 'table' then hasSubTable = true end 346 | local keyStr, usesDot = toLuaKey(state, k, path) 347 | if keyStr then 348 | local newpath 349 | if path then 350 | newpath = path 351 | if usesDot then newpath = newpath .. '.' end 352 | newpath = newpath .. keyStr 353 | end 354 | local nextResult = toLuaRecurse(state, v, newtab, newpath) 355 | if nextResult then 356 | mixed:insert{keyStr, nextResult} 357 | -- else x[k] is a recursive reference 358 | end 359 | end 360 | end 361 | end 362 | mixed:sort(function(a,b) return a[1] < b[1] end) -- sort by keys 363 | mixed = mixed:map(function(kv) return table.concat(kv, '=') end) 364 | s:append(mixed) 365 | 366 | local thisNewLineChar, thisNewLineSepChar, thisTab, thisNewTab 367 | if not hasSubTable and not state.alwaysIndent then 368 | thisNewLineChar = '' 369 | thisNewLineSepChar = ' ' 370 | thisTab = '' 371 | thisNewTab = '' 372 | else 373 | thisNewLineChar = state.newlineChar 374 | thisNewLineSepChar = state.newlineChar 375 | thisTab = tab 376 | thisNewTab = newtab 377 | end 378 | 379 | local rs = '{'..thisNewLineChar 380 | if #s > 0 then 381 | rs = rs .. thisNewTab .. s:concat(','..thisNewLineSepChar..thisNewTab) .. thisNewLineChar 382 | end 383 | rs = rs .. thisTab .. '}' 384 | 385 | result = rs 386 | end 387 | return result 388 | end, 389 | } 390 | 391 | local function defaultSerializeMetatableFunc(state, m, x, tab, path, keyRef) 392 | -- only serialize the metatables of tables 393 | -- otherwise, assume the current metatable is the default one (which is usually nil) 394 | if type(x) ~= 'table' then return 'nil' end 395 | return toLuaRecurse(state, m, tab..state.indentChar, path, keyRef) 396 | end 397 | 398 | toLuaRecurse = function(state, x, tab, path, keyRef) 399 | if not tab then tab = '' end 400 | 401 | local xtype = type(x) 402 | local serializeFunction 403 | if state.serializeForType then 404 | serializeFunction = state.serializeForType[xtype] 405 | end 406 | if not serializeFunction then 407 | serializeFunction = defaultSerializeForType[xtype] 408 | end 409 | 410 | local result 411 | if serializeFunction then 412 | result = serializeFunction(state, x, tab, path, keyRef) 413 | else 414 | result = '['..type(x)..':'..tostring(x)..']' 415 | end 416 | assert(result ~= nil) 417 | 418 | if state.serializeMetatables then 419 | local m = getmetatable(x) 420 | if m ~= nil then 421 | local serializeMetatableFunc = state.serializeMetatableFunc or defaultSerializeMetatableFunc 422 | local mstr = serializeMetatableFunc(state, m, x, tab, path, keyRef) 423 | -- make sure you get something 424 | assert(mstr ~= nil) 425 | -- but if that something is nil, i.e. setmetatable(something newly created with a nil metatable, nil), then don't bother modifing the code 426 | if mstr ~= 'nil' and mstr ~= false then 427 | -- if this is false then the result was deferred and we need to add this line to wherever else... 428 | assert(result ~= false) 429 | result = 'setmetatable('..result..', '..mstr..')' 430 | end 431 | end 432 | end 433 | 434 | return result 435 | end 436 | 437 | --[[ 438 | args: 439 | indent = default to 'true', set to 'false' to make results concise, true will skip inner-most tables. set to 'always' for always indenting. 440 | pairs = default to a form of pairs() which iterates over all fields using next(). Set this to your own custom pairs function, or 'pairs' if you would like serialization to respect the __pairs metatable (which it does not by default). 441 | serializeForType = a table with keys of lua types and values of callbacks for serializing those types 442 | serializeMetatables = set to 'true' to include serialization of metatables 443 | serializeMetatableFunc = function to override the default serialization of metatables 444 | skipRecursiveReferences = default to 'false', set this to 'true' to not include serialization of recursive references 445 | --]] 446 | local function tolua(x, args) 447 | local state = { 448 | indentChar = '', 449 | newlineChar = '', 450 | wrapWithFunction = false, 451 | recursiveReferences = table(), 452 | touchedTables = {}, 453 | } 454 | local indent = true 455 | if args then 456 | -- indent == ... false => none, true => some, "always" => always 457 | if args.indent == false then indent = false end 458 | if args.indent == 'always' then state.alwaysIndent = true end 459 | state.serializeForType = args.serializeForType 460 | state.serializeMetatables = args.serializeMetatables 461 | state.serializeMetatableFunc = args.serializeMetatableFunc 462 | state.skipRecursiveReferences = args.skipRecursiveReferences 463 | end 464 | if indent then 465 | state.indentChar = '\t' 466 | state.newlineChar = '\n' 467 | end 468 | state.pairs = builtinPairs 469 | 470 | local str = toLuaRecurse(state, x, nil, '') 471 | 472 | if state.wrapWithFunction then 473 | str = '(function()' .. state.newlineChar 474 | .. state.indentChar .. 'local root = '..str .. ' ' .. state.newlineChar 475 | -- TODO defer self-references to here 476 | .. state.recursiveReferences:concat(' '..state.newlineChar..state.indentChar) .. ' ' .. state.newlineChar 477 | .. state.indentChar .. 'return root ' .. state.newlineChar 478 | .. 'end)()' 479 | end 480 | 481 | return str 482 | end 483 | 484 | return setmetatable({}, { 485 | __call = function(self, x, args) 486 | return tolua(x, args) 487 | end, 488 | __index = { 489 | -- escaping a Lua string for load() to use 490 | escapeString = escapeString, 491 | -- returns 'true' if the key passed is a valid Lua variable string, 'false' otherwise 492 | isVarName = isVarName, 493 | -- table of default serialization functions indexed by each time 494 | defaultSerializeForType = defaultSerializeForType, 495 | -- default metatable serialization function 496 | defaultSerializeMetatableFunc = defaultSerializeMetatableFunc, 497 | } 498 | }) 499 | -------------------------------------------------------------------------------- /xpcall.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | shim layer for xpcall if it doesn't support ... forwarding 3 | used by ext.load 4 | xpcall doesn't forward extra args in vanilla lua 5.1, and in luajit without 5.2 compat (I think?) 5 | 6 | TODO tempting to always insert a new xpcall and give it a default error-handler of `function(err) return err..'\n'..debug.traceback() end` 7 | so that it will always capture the stack trace by default. 8 | --]] 9 | return function(env) 10 | env = env or _G 11 | local oldxpcall = env.xpcall or _G.xpcall 12 | local xpcallfwdargs = select(2, 13 | oldxpcall( 14 | function(x) return x end, 15 | function() end, 16 | true 17 | ) 18 | ) 19 | if xpcallfwdargs then return end 20 | local unpack = env.unpack or table.unpack 21 | local function newxpcall(f, err, ...) 22 | local args = {...} 23 | args.n = select('#', ...) 24 | return oldxpcall(function() 25 | return f(unpack(args, 1, args.n)) 26 | end, err) 27 | end 28 | env.xpcall = newxpcall 29 | end 30 | --------------------------------------------------------------------------------