ds: lua data structures and algorithms
ds is a small-ish lua library which fills many of the data structure and
method gaps (needed "batteries") in Lua's standard library. It's only
dependency is
lib/metaty which it uses for defining it's records and
lib/fmt which it uses to define the logging interface.
ds: data structures and algorithms.
Types: PlainStyler TypoSafe Slc Imm Set bt BiMap Deq Error
Functions
- fn setup(args)
Default LUA_SETUP, though vt100 is recommended for most users.
- fn concat(sep, ...) -> string
concatenate the string arguments.
- fn push(t, v) -> index
push the value onto the end of the table, return the index.
- fn name(t) -> string
if t is a table returns t.__name or '?'
- fn inset(t, i, values, rmlen) -> nil
insert values into list at index i.
Uses inset method if available.
rmlen, if provided, will cause t[i:i+rmlen] to be removed first
inset is like extend but the items are insert at any place in the array.
The rmlen will also remove a certain number of items.
These are done together to improve performance.
- fn isPod(v, mtFn)
return true if the value is "plain old data".
Plain old data is defined as any concrete type or a table with no metatable
and who's pairs() are only POD.
- fn noop()
function that does and returns nothing.
- fn nosupport()
Function that indicates an API is not supported for a type.
Throws error'not supported'.
- fn iden(...) -> ...
identity function, return the inputs.
- fn retTrue() -> true
function that always return true.
- fn retFalse() -> false
function that always return false.
- fn newTable() -> {}
Function that creates a new, empty table.
- fn eq(a, b) -> bool
Function that returns a == b.
- fn srcloc(level) -> "/path/to/dir/file.lua:10"
Get the source location of wherever this is called
(or at a higher level).
- fn shortloc(level) -> "dir/file.lua:10"
Same as srcloc but shortens to only the parent dir.
- fn srcdir(level) -> "/path/to/dir/"
Same as srcloc but removes the file:linenum
- fn coroutineErrorMessage(cor, err) -> string
Create an error message for the coroutine which includes
it's traceback.
- fn isWithin(v, min, max) -> bool
Return whether min <= v <= max.
- fn min(a, b) -> minimum
return the minimum value
- fn max(a, b) -> maximum
return the maximum value
- fn lt(a, b) -> bool
Return a < b.
- fn gt(a, b) -> bool
Return a > b.
- fn lte(a, b) -> bool
Return a <= b.
- fn bound(v, min, max) -> int
Return value within [min,max] (inclusive).
- fn sort2(a, b) -> (small, large)
Return the two passed-in values in sorted order.
- fn isEven(a) -> bool
Return whether value is even.
- fn isOdd(a) -> bool
Return whether value is odd.
- fn absDec(v) -> number
Moves the absolute value of v towards 0 by 1.
If v==0 then do nothing.
- fn concat(sep, ...) -> string
concatenate the string arguments.
- fn isupper(c) -> string?
return the string if it is only uppercase letters
- fn islower(c) -> string?
return the string if it is only lowercase letters
- fn trim(subj, pat, si) -> string
Remove pat (default=%s, aka whitespace) from the front and back
of the string.
- fn trimEnd(subj, pat, index) -> string
Trim the end of the string by removing pat (default=%s)
- fn find(subj, pats, si, plain) -> (ms, me, pi, pat)
Find any of a list of patterns. Return the match start, end as well as
the index, pat of the pattern matched.
- fn split(subj, pat--[[%s+]], index--[[1]]) -> (cxt, str) iter
split the subj by pattern. ctx has two keys: si (start index) and
ei (end index)
for ctx, line in split(text, '\n') do -- split lines
... do something with line
end
- fn splitList(...) -> list
Perform a split but returning a list instead of a string.
- fn squash(s, repl) -> string
Squash a string: convert all whitespace to repl (default=single-space).
- fn usub(s, si, ei, len)
utf8 sub. If len is pre-computed you can pass it in for better performance.
- fn simplestr(s)
A way to declare simpler mulitline strings which:
- ignores the first/last newline if empty
- removes leading whitespace equal to the first
line (or second line if first line has no indent)
Example: local s = require'ds'.simplestr
local mystr = s[[
this is
a string.
]]
T.eq('this is\n a string.', mystr)
- fn bin(uint, width--[[8]], sep4--[['_']]) -> str
Convert integer to binary representation (0's and 1's)
- width will be the number of bits.
- sep4 will be used to separate every 4 bits, set to
nil to disable.
- fn get(t, k) -> value
t[k] if t is a raw table, else getmetatable(t).get(t, k)
This lets many types be substitutable for raw-tables in some APIs (i.e.
lines).
- fn set(t, k, v)
t[k] = v if t is a raw table, else getmetatable(t).set(t, k, v)
This lets many types be substitutable for raw-tables in some APIs (i.e.
lines).
- fn isEmpty(t)
Return whether t contains a single value.
- fn pairlen(t) -> int
the full length of all pairs
WARNING: very slow, requires iterating the whole table
- fn sort(t, fn) -> t
Sort table and return it. Eventually this may use the __sort metamethod.
- fn sortUnique(t, sortFn, rmFn) -> t
sort t and remove anything where rmFn(v1, v2)
(normally rmFn is ds.eq)
- fn geti(t, i) -> t[i]
get index, handling negatives
- fn last(t)
get the last value of a list-like table.
- fn only(t) -> t[1]
get the first (and assert only) element of the list
- fn values(t) -> list
get only the values of pairs(t) as a list
- fn keys(t) -> list
get only the keys of pairs(t) as a list.
- fn inext(t, i) -> (i+1, v)
next(t, key) but with indexes
- fn iprev(t, i) -> (i-1, v)
inext but reversed.
- fn ireverse(t) -> iter
ipairs reversed
- fn rawislice(state, i) -> (i+1, v)
You probably want islice instead.
Usage: for i, v in rawislice, {t, ei}, si do ... end
where si, ei is the start/end indexes.
- fn islice(t, starti, endi) -> iter
Usage: for i,v in islice(t, starti, endi)
The default endi is #t, otherwise this ignores the list's length
(v may be nil for some i values).
- fn slice(t, si, ei) -> list
Get a new list of indexes [si-ei] (inclusive).
Defaults: si=1, ei=#t.
- fn ieq(a, b)
Return true if two list-like tables are equal.
Note that this only compares integer keys and ignores
others.
- fn reverse(t) -> t
Reverse a list-like table in-place (mutating it).
- fn extend(t, l) -> t
Extend t with list-like values from l.
This mutates t.
- fn defaultExtend(r, l) -> r
This is used by types implementing :extend.
It uses their get and set methods to implement
extend in a for loop.
types do this if they may yield in their get/set, which
is not allowed through a C boundary like table.move
- fn clear(t, si, len) -> t
Clear list-like elements of table.
default is all of it, but you can also specify a specific
start index and length.
- fn update(t, add) -> t
return t with the key/vals of add inserted
- fn flatten(listOfLists) -> list
Given a list of lists return a single depth list.
- fn updateKeys(t, add, keys) -> t
like update but only for specified keys
- fn orderedKeys(t, cmpFn) -> keys
Get the sorted keys of t.
- fn pushSortedKeys(t, cmpFn) -> t
Adds all key=index to the table so the keys can
be iterated using for _, k in ipairs(t)
- fn merge(t, m) -> t
Recursively merge m into t, overriding existing values.
List (integer) indexes are extended instead of overwritten.
- fn orderedMerge(a, b, to, cmp, a_si, a_ei, b_si, b_ei, ti, mv) -> to
Perform an ordered merge of a[a_si:a_ei and b[b_si:b_ei
to to[ti:a_ei-a_si + b_ei-b_si + 2, using cmp (default ds.lte)
for comparison.
The return values is the table to, which will be created if not provided.
The mv function is used for moving final values after one list
is empty (default=table.move).
This can be part of a sorting algorithm or used to merge two
already-sorted lists.
- fn orderedMergeRaw(a, b, to, cmp, a_si, a_ei, b_si, b_ei, ti, mv) -> to
orderedMerge without any default values.
- fn popk(t, key) -> value
Remove key from t and return it's value.
- fn drain(t, len--[[#t]]) -> table
return len items from the end of t, removing them from t
- fn getOrSet(t, k, newFn) -> t[k] or newFn()
If the key exists, return it's value.
Else return newFn()
- fn setIfNil(t, k, v)
Set the key to a value if it is currently nil.
Else do not change it.
- fn emptyTable()
Return an empty table, useful for newFn/etc in some APIs.
- fn rmleft(t, rm, eq--[[ds.eq]]) -> t (mutated)
remove (mutate) the left side of the table (list).
noop if rm is not exactly equal to the left side.
- fn dotpath(dots) -> list split by '.'
used with ds.getp and ds.setp. Example local dp = require'ds'.dotpath
ds.getp(t, dp'a.b.c')
- fn getp(t, path) -> value? at path
get the value at the path or nil if the value or any
intermediate table is missing.
get(t, {'a', 2, 'c'}) -> t.a?[2]?.c?
get(t, dotpath'a.b.c') -> t.a?.b?.c?
- fn rawgetp(t, path) -> value? at path
same as ds.getp but uses rawget
- fn setp(d, path, value, newFn) -> nil
set the value at path using newFn (default=ds.newTable) to create
missing intermediate tables.
set(t, dotpath'a.b.c', 2) -- t.a?.b?.c = 2
- fn indexOf(t, find) -> int
Return the index where the value is == find.
- fn indexOfPat(strs, pat) -> int
Return the index where value:match(pat).
- fn walk(t, fieldFn, tableFn, maxDepth, state) -> nil
Walk the table up to depth maxDepth (or infinite if nil)
- fieldFn(key, value, state) -> stop is called for every non-table value.
- tableFn(key, tblValue, state) -> stop is called for every table value
If tableFn stop==ds.SKIP (i.e. 'skip') then that table is not recursed.
Else if stop then the walk is halted immediately
- fn icopy(t) -> list
Copy list-elements only
- fn defaultICopy(r)
For types implementing :copy() method.
- fn rawcopy(t)
- fn copy(t, add) -> new t
Copy and update full table
FIXME: remove add
- fn deepcopy(t) -> table
Recursively copy the table.
- fn readPath(path)
Read the full contents of the path or throw an error.
- fn writePath(path, text)
Write text to path or throw an error.
- fn sentinel(name, mt) -> NewType
sentinel(name, metatable)
Use to create a "sentinel type". Return the (singular) instance.
Sentinels are "single values" commonly used for things like: none, empty, EOF, etc.
They have most metatable methods disallowed and are immutable down. Methods can
only be set by the provided metatable value.
- fn bool(v) -> bool
convert to boolean (none aware)
- fn binarySearch(t, v, cmp, si--[[1]], ei--[[#t]]) -> index
Search the sorted table, return i such that:
- cmp(t[i], v) returns true for indexes <= i
- cmp(t[i], v) returns false for indexes > i
If you want a value perfectly equal then check equality
on the resulting index.
- fn dagSort(ids, parentMap) -> sorted?, cycle?
Sort the directed acyclic graph of ids + parentMap
to put children before the parents.
returns nil, cycle in the case of a cycle
- fn check(i, ...)
Throw an error if select(i, ...) is truthy, else return ...
For example, file:read'L' returns line?, errmsg?.
However, the absence of line doesn't necessarily
indicate the presence of errmsg: EOF is just nil.
Therefore you can use line = check(2, f:read'L')
to only assert on the presence of errmsg.
- fn tracelist(tbstr, level) -> {traceback}
convert the string traceback into a list
- fn traceback(level) -> string
Get the current traceback as an indented string.
- fn try(fn, ...) -> (ok, ...)
try to run the fn. Similar to pcall. Return one of:
- successs: (true, ...)
- failure: (false, ds.Error{...})
- fn main(fn, ...) -> errno?
Helper function for running commands as "main".
- fn resume(th) -> (ok, err, b, c)
Same as coroutine.resume except uses a ds.Error object for errors
(has traceback)
- fn wantpath(path) -> value?
Try to get any string.to.path by trying all possible combinations of
requiring the prefixes and getting the postfixes.
- fn resource(relpath)
Include a resource (raw data) relative to the current file.
Example: M.myData = ds.resource'data/myData.csv'
- fn yeet(fmt, ...)
exit immediately with message and errorcode = 99
- fn eprint(...)
Print to io.sderr
A typo-safe table, typically used in libraries for storing constants.
Adding keys is always allowed but getting non-existant keys is an error.
A slice of anything with start and end indexes.
Note: This object does not hold a reference to the object being
sliced.
Fields:
- si
start index
- ei
end index
Methods
- fn:merge(b) -> first, second?
return either a single (new) merged or two sorted Slcs.
Immutable table
Methods
indexed table as Binary Tree.
These functions treat an indexed table as a binary tree
where root is at index=1
Functions
Bidirectional Map.
Maps both
key -> value and
value -> key.
Must use
:remove (instead of
bm[k] = nil to handle deletions.
Note that pairs() will return BOTH directions (in an unspecified order)
Methods
Deq() -> Deq, a deque datastructure.
Use as a first in/out (fifo) with
deq:push(v) and
deq() (pop).
Main methods:
pushLeft() -- push on left side
pushRight() push() -- push on right side
popRight() -- pop on right side
popLeft() pop() __call() -- pop on left side
Fields:
Methods
Error message, traceback and cause
NOTE: you should only use this for printing/logging/etc.
Fields:
Methods
- fn from(msg, tb, cause) -> Error
create the error from the arguments.
tb can be one of: coroutine|string|table
FIXME: have this take T as first arg
- fn msgh(msg, level) -> Error
for use with xpcall. See: try
Working with and representing time.
Types: DateTime Duration Epoch Tz
Functions
- fn isLeap(year)
Leap years are every 4 years. The only exception is on the century,
except every 4th century (since 1600) there is still a leap year.
Therefore:
- 1600 WAS a leap year, as well as 1604, 1608, etc.
- 1700 was NOT a leap year, but 1704, 1708, etc was.
- 1800 was NOT a leap year, but 1804, 1808, etc was.
- 1900 was NOT a leap year, but 1904, 1908, etc was.
- 2000 WAS a leap year, as well as 2004,2008,etc.
Represents a date time. The core fields are documented below, with
methods to convert to more "typical" reprsentations.
Fields:
- y
the year
- yd
the day of the year
- s
seconds in day
- ns
nanoseconds in second
- tz
timezone offset, see #ds.time.Tz.
- wd
weekday, 1=sunday - 7=saturday
Methods
- fn of(T, s, tz)
- fn:isLeap(self.y)
- fn:date() -> month, day
Get the month and day of the month.
To get the year use DateTime.y.
- fn:time() -> hour,min,sec
Get the time of day in hour,min,sec.
To get ns, use DateTime.ns
- fn:epoch() -> Epoch
Convert a DateTime to an Epoch
Represents a Duration of time.
Fields:
Methods
Represents an Epoch: seconds and nano-seconds since 1970-01-01 at 12:00 pm.
Fields:
Usage:
Tz:of(-6) for a -6 hour offset.
Fields:
- s
second offset
- name
calculated name
Methods
Methods
- fn of(T, t) -> T{pairs(t)}
create iterable of pairs(t)
- fn ofList(T, t) -> T{ipairs(t)}
create iterable of ipairs(t)
- fn ofUnpacked(T, t)
create an iterable that returns table.unpack on each
value in ipairs(t).
i.e. Iter:ofUnpacked{{5, 'five'}, {6, 'six'}} will
return (5, 'five') then (6, 'six')
- fn ofOrdMap(T, t, cmpFn) -> sortedIter[k, v]
create an iterable of t which emits keys in order.
WARNING: this first sorts the keys, which can be slow
- fn ofOrdList(T, t, cmpFn) -> sortedIter[i, v]
sort t then iterate over list
- fn ofSlc(T, t, starti, endi) -> iter[i, v]
iterate over slice of starti:endi in t
- fn:map(fn) -> self
emit k, v = fn(v) for each non-nil result
Note: if performance matters this is the most performant
application function since self doesn't create an internal
function.
- fn:mapK(fn) -> self
- emit fn(k), v) for each non-nil result.
- fn:mapV(fn) -> self
emit k, fn(v) for each non-nil result.
(filtered when newK==nil)
- fn:filter(fn) -> self
- emit only if fn(k, v) results
- fn:filterK(fn) -> self
- emit only if fn(k) results
- fn:filterV(fn) -> self
- emit only if fn(v) results
- fn:lookup(t) -> self
emitv, t[k], looking up the iter's values in the table's keys.
- fn:lookupK(t) -> self
emit t[k], v for each non-nil t[k]
- fn:lookupV(t) -> self
emit k, $t[v] for each non-nil t[v]
- fn:keyIn(t) -> self
emit k, v for each non-nil t[k]
- fn:keyNotIn(t) -> self
emit k, v for each nil t[k]
- fn:valIn(t) -> self
emit k, v for each non-nil t[v]
- fn:valNotIn(t) -> self
emit k, v for each nil t[v]
- fn:listen(fn) -> self
emit k, v after calling fn(k, v).
The results of the fn are ignored
- fn:index() -> self
emit i, k, dropping values. i starts at 1 and increments each
time called.
Note: this is most useful for iterators which don't emit a v.
i.e. getting the line number in file:lines()
- fn:swap() -> self[v, k]
emit v, k (swaps key and value)
- fn:find(fn) -> k, v
run the iterator over all values, calling fn(k, v) for each.
return the first k, v where the fn returns a truthy value.
- fn:all(allFn)
return true if any of the values are truthy
- fn:any(anyFn)
return true if any of the values are truthy
- fn:run(fn--[[noop]]) -> nil
run the iterator over all values, calling fn(k, v) for each.
- fn to(self, to={}) -> to
collect non-nil k, v into table-like object to
- fn keysTo(self, to={}) -> to
collect emitted k as a list (vals are dropped)
- fn valsTo(self, to={}) -> to
collect emitted v as a list (keys are dropped)
- fn:concat()
- fn:reset() -> self
reset the iterator to run from the start
- fn:assertEq(it2)
Used for testing. Iter:assertEq(it1, it2) constructs both
iterators using Iter() and then asserts the results are
identical.
Fields:
- l
left node
- r
right node
- v
value
Methods
- fn from(T, list) -> (head, tail) from list of vals
- fn:head()
- fn:tail()
- fn:tolist() -> {a.v, b.v, c.v, ...}
- fn:link(r)
create l -> r link
- fn:insert(v)
insert LL(v) to right of ll
(h -> 2); h:insert(1) ==> (h -> 1 -> 2)
- fn:rm() -> head?
remove node ll from linked list
if ll was the head, returns the new head (or nil)
- fn:get(i) -> node? (at index +/- i)
working with paths
Call directly to convert a list|str to a list of path components.
Functions
- fn read(path)
read file at path or throw error
- fn write(path, text)
write string to file at path or throw error
- fn append(path, text)
append text to path, adds a newline if text doesn't end in one.
- fn pathenv(var, alt)
- fn cwd(dir) -> /...cwd/
get the current working directory
- fn cd(dir)
Set the CWD, changing the result of ds.cwd.
- fn home('HOME', 'HOMEDIR')
get the user's home directory
- fn concat(t, _) -> string
join a table of path components
- fn hasBacktrack(path) -> bool. path: [str|list]
return whether a path has any '..' components
- fn ext(path) -> string. path: [str|list]
- fn abs(path, wd) -> /absolute/path
Ensure the path is absolute, using the wd (default=cwd()) if necessary
This preserves the type of the input: str -> str; table -> table
- fn resolve(path, wd) -> list|str
resolve any `..` or `.` path components, making the path
/absolute if necessary.
The return type is the same as the input type.
- fn canonical(path)
Get the canonical path.
This is a shortcut for resolve(abs(path)).
- fn itemeq(a, b) -> boolean: path items are equal
- fn rmleft(path, rm)
ds.rmleft for path components
- fn nice(path, wd) -> string
return a nice path (string) that is resolved and readable.
It's 'nice' because it has no '/../' or '/./' elements
and has CWD stripped.
- fn small(path, wd)
Return the nice path but always keep either / or ./
at the start.
- fn short(path, wd)
Return only the parent dir and final item.
This is often used for documentation/etc
- fn first(path)
first/middle/last -> ("first", "middle/last")
- fn last(path)
first/middle/last -> ("first/middle/", "last")
- fn dir(path)
Get the directory of the path or nil if it is root.
- fn isDir(-1)
return whether the path looks like a dir.
Note: civstack tries to make all ftype='dir' paths end in '/'
but other libraries or APIs may not conform to this.
- fn toDir(path) -> path/
- fn toNonDir(path) -> path (without ending /)
- fn relative(from, to, wd)
return the relative path needed to get from from to to.
Note: this ignores (pops) the last item in from if it's not a dir/.
For example
T.eq(relative('/foo/bar', '/foo/baz/bob'), 'baz/bob')
T.eq(relative('/foo/bar/', '/foo/baz/bob'), '../baz/bob')
- fn cmpDirsLast(a, b)
path comparison function for table.sort that sorts
dirs last, else alphabetically.
utf8 stream decoding.
Get the length by decodelen(firstbyte), then decode the whole character
with decode(dat)
Functions
- fn decodelen(firstbyte)
given the first byte return the number of bytes in the utf8 char
- fn decode(dat) -> int
decode utf8 data (table) into an integer.
Use utf8.char (from lua's stdlib) to turn into a string.
Binary Heap implementation
Types: Heap
Heap(t, cmp) binary heap using a table.
A binary heap is a binary tree where the value of the parent always
satisfies
cmp(parent, child) == true
- Min Heap: cmp = function(p, c) return p < c end (default)
- Max Heap: cmp = function(p, c) return p > c end
add and push take
O(log n), making it very useful for
priority queues and similar problems.
Fields:
- cmp =ds.lt
comparison function to use
Methods
- fn:add(v)
h:add(v) add value to the heap.
- fn:pop() -> v
h:pop() -> v pop the top node.
Simple logging library, set i.e. LOGLEVEL=INFO to enable logging.
This module has the functions trace info warn err crit with the signature:
function(fmt, ... [, data])
- the ... are the format args which behave like fmt.format (aka %q
formats tables/etc).
- data is optional arbitrary data that can be serialized/formatted.
To enable logging the user should set a global (or env var) LOGLEVEL
to oneof: C/CRIT/1 E/ERROR/2 W/WARN/3 I/INFO/4 T/TRACE/5
This module also sets (if not already set) the global LOGFN to ds.logFn
which logs to stderr. This fn is called with signature
function(level, srcloc, fmt, ...)
Types: LogTable
Functions
- fn levelInt(lvl) -> int
Get the current log-level integer.
- fn levelStr(lvl)
Get the current log-level string.
- fn setLevel(lvl)
Set the global logging level (default=os.getenv'LOGLEVEL')
- fn logFn(lvl, loc, fmt, ...)
- fn crit(1, ...)
Log at level CRIT.
- fn err(2, ...)
Log at level ERROR.
- fn warn(3, ...)
Log at level WARN.
- fn info(4, ...)
Log at level INFO.
- fn trace(5, ...)
Log at level TRACE.
used in tests
Fields:
- tee
call calls will also call tee
Fields:
Methods
- fn:clear() -> self
clear the grid
- fn:insert(l, c, str)
Insert the str into the Grid.
Any newlines will be insert starting at column c.
This will automatically fill [1,c-1] with spaces, but will
NOT clear any data after the insert text, meaning it is essentially a
replace.
FIXME: considere renaming to replace... or something.
load lua modules with custom or default environment in a sandboxed
environment. This is extremely useful for configurations written in lua or
writing your own config-like language.
The default environment (ds.load.ENV) has safe default functions which
cannot access state and missing unsafe functions like getmetatable or the
debug module.
To perform the load, call this module with:
(path, env={}, envMeta=ds.load.ENV) -> ok, result
inputs:
outputs:
- ok: boolean to indicate load success or failure of script.
- result: result or loading or ds.Error.
Throws an error if the path is not valid lua code.
Functions
- fn loadfile(path, env, envMeta) -> fn?, ds.Error?
Similar to lua's loadfile but follows conventions of ds.load(...).
Unlike loadfile, this throws an error if the path doesn't parse.
Fields:
Methods
- fn create(T, sz, path) -> IFile?, errmsg?
This creates a new index file at path (path=nil uses tmpfile()).
Note: Use load if you want to load an existing index.
- fn:reload() -> IFile?, errmsg?
Reload IFile from path.
- fn load(T, sz, path, mode) -> IFile?, errmsg?
load an index file
- fn:flush()
- fn:close()
- fn:closed() -> bool
- fn:getbytes(i)
get bytes. If index out of bounds return nil.
Panic if there are read errors.
- fn:setbytes(i, v)
- fn:move(to, mvFn) -> self
Move the IFile's path to to.
mv must be of type fn(from, to). If not provided,
civix.mv will be used.
This can be done on both closed and opened files.
The IFile will re-open on the new file regardless of the
previous state.
- fn:reader() -> IFile?, err?
Get a new read-only instance with an independent file-descriptor.
Warning: currently the reader's len will be static, so this should
be mostly used for temporary cases. This might be changed in
the future.