├── 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 | [](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 |
--------------------------------------------------------------------------------