Base Library
The base library is a set of functions dealing with some language aspects which aren't covered by the syntax of the language, as well as miscellaneous functions that don't really fit anywhere else. The base library is always loaded when you create an instance of the MiniD VM.
Table of Contents
- Evaluation of functions in tailcalls
- Basic Functions
- Functional Things
- Reflection Functions
- Conversions
- Console IO
- Dynamic Compilation
- Namespace metamethods
- Thread metamethods
- Function metamethods
- Objects
(note to self: this library is getting out of hand. some of this could probably stand to be separated out into separate modules.)
Changes from MiniD 1
The following functions have been removed:
- assert: now a built-in construct rather than a function.
- methodsOf: fields and methods of an object are now both retrieved using fieldsOf.
- isClass, isInstance: replaced by isObject.
- readf: using regular expressions to parse input is more powerful than a formatted read function.
The following functions have been added:
- haltThread
- reloadModule
- rawSet
- rawGet
- runMain
- findGlobal
- isSet
- hasField
- hasMethod
- hasAttributes
- attributesOf
- isObject
- toBool
- dumpVal
- allFieldsOf
Some individual functions have had their behaviors changed. These are noted in the function descriptions.
Evaluation of functions in tailcalls
Some functions in the base library will fail by throwing an exception if they are tailcalled, giving a message about trying to get the environment of a function whose activation record was overwritten. This is because information about the calling function's environment is lost when the tailcall occurs, making it impossible to evaluate these functions in the context of the calling function.
The solution is not to tailcall them. This is simple: just wrap the call to the function in parentheses, which will force evaluation within the context of the calling function. If the original code is something like "return func("foo")", replace it with "return (func("foo"))".
Functions that have this issue will be marked as such.
Basic Functions
getTraceback()
getTraceback gets a string representing the call stack that was unwound during the last exception being thrown. You can use this to print out more meaningful errors if you catch an exception in your MiniD code, i.e.
try { // ... } catch(e) { writeln("caught exception: ", e) writeln(getTraceback()) }
If there is no traceback information available, this simply returns an empty string.
haltThread([t: thread])
Halts a given thread, causing it to enter the "dead" state but without causing an error. This is one of two ways a thread can successfully enter the "dead" state; the other is to return from its top-level function. If a parameter is given, it should be a thread to halt. If no parameter is given, it defaults to the current thread.
currentThread()
This returns the thread object of the currently-executing thread, or null if the current thread is the main thread.
setModuleLoader(name: string, func: function)
Sets a custom loader function for the given module. This way, when the module is imported, if it hasn't already been loaded (which, chances are if you're using this function, it hasn't), it will call this function to load it instead of defaulting to other mechanisms. So, if you call "setModuleLoader("mod", someFunction)", whenever someone else imports "mod" for the first time, it will call that function to load it.
The loader function must take two parameters: the name of the module to load, and a namespace into which the symbols should be inserted. It is not required to return anything.
reloadModule(name: string)
Reloads the module whose name is in the given string. The module must have already been loaded for this to work, or else an error is thrown.
removeKey(container: table|namespace, key)
Removes the key-value pair with the key key from the given table or namespace. You can also remove key-value pairs from tables by assigning a null value to the key-value pair (which is the fast way to do it), but this function is the only way to remove a key-value pair from a namespace.
rawSet(container: table|object, key, value)
Sets a key-value pair in the given table or object, bypassing any opFieldAssign (or opIndexAssign for tables) metamethods.
rawGet(container: table|object, key)
Gets the value held at the key in the given table or object, bypassing any opField (or opIndex for tables) metamethods.
runMain(mod: namespace, vararg)
If the given namespace contains a function named "main", it will call that function with the given variadic arguments (if any) as parameters. If "main" is not a function or if there is nothing called "main" in the namespace, does nothing and returns.
Functional Things
curry(func: function, param)
This function takes a function and a parameter, and returns a new function which takes one less parameter. The new function, when called, will call the old function with the given parameter as its first parameter, and will pass along any other parameters. Example:
function foo(x, y) { writefln("foo: ", x, ", ", y); } foo(4, 5); // prints "foo: 4, 5" local func = curry(foo, 8); func(9); // prints "foo: 8, 9"
The curried function will also pass along the 'this' parameter to the old function.
bindContext(func: function, context)
This is similar to curry, but instead of returning a function which takes one less parameter, it returns a function which will always use the same context, regardless of how it's called. This allows you to create D-style "delegates", that is, a closure which has an associated object.
Reflection Functions
findGlobal(name: string)
Looks for the global with the given name. If found, returns the namespace that contains it; otherwise, returns null.
This function must not be tailcalled.
isSet(name: string)
Similar to findGlobal, except returns a boolean value: true if the global exists, false otherwise.
This function must not be tailcalled.
typeof(value)
This will get the type of the passed-in value and return it as a string. Possible return values are "null", "bool", "int", "float", "char", "string", "table", "array", "function", "object", "namespace", and "thread".
fieldsOf(value: object)
Returns a namespace that holds the fields of the given object. Each object has its own unique field namespace. Note, however, that since the fields of an object are lazily created (i.e. an object will not have a field unless it has been assigned), you won't necessarily get all the fields that can be accessed from the object, only those which have been set in this object. If you want to get all the fields, use the allFieldsOf iterator function.
allFieldsOf(value: object)
Returns a coroutine iterator that will iterate through all fields in the object, traversing its prototype chain up to the root. When using this, remember that the coroutine opApply function inserts an integer index before the values returned by the coroutine:
foreach(_, name, value; allFieldsOf(SomeObject)) { ... }
hasField(value, name: string)
Sees if value contains the field name. Works for tables, namespaces, and objects. For any other type, always returns false.
hasMethod(value, name: string)
Sees if the method named name can be called on value. Looks in metatables as well, for i.e. strings and arrays. Works for all types.
hasAttributes(value)
Returns whether or not the given value has an attributes table. Works for all types, but only functions, namespaces, and objects can have attributes.
attributesOf(value)
Returns the attributes table of value, or null if it has none.
isNull(value), isBool(value), isInt(value), isFloat(value), isChar(value), isString(value), isTable(value)
isArray(value), isFunction(value), isObject(value), isNamespace(value), isThread(value)
These all return a boolean value. They return true if the passed-in value is of the given type, and false otherwise.
The advantage of using these functions over "typeof(value) == "type" is that they will be faster, as no string comparison has to be done. However using "value is null" is still the quickest way to determine if something is of the null type.
Conversions
toString(value, style: char = 'd')
This is like rawToString, but it will call any toString metamethods defined for the value. Arrays have a toString metamethod defined for them if the array stdlib is loaded, and any toString methods defined in tables and objects will be used.
The optional style parameter only has meaning if the value is an integer. It can be one of the following:
- 'd': Default: signed base 10.
- 'b': Binary.
- 'o': Octal.
- 'x': Lowercase hexadecimal.
- 'X': Uppercase hexadecimal.
- 'u': Unsigned base 10.
Changes from MiniD 1
Added the style parameter.
rawToString(value)
This returns a string representation of the given value depending on its type, as follows:
- null: the string "null".
- bool: "true" or "false".
- int: The decimal representation of the number.
- float: The decimal representation of the number, to about 7 digits of precision.
- char: A string with just one character, the character that was passed in.
- string: The string itself.
- table: A string in the format "table 0x00000000" where 0x00000000 is the address of the table.
- array: A string in the format "array 0x00000000" where 0x00000000 is the address of the array.
- function: If the function is native code, a string formatted as "native function <name>"; if script code, a string formatted as "script function <name>(<location>)".
- object: A string formatted as "object <name> (0x00000000)", where 0x00000000 is the address of the object.
- namespace: A string formatted as "namespace <names>", where <name> is the hierarchical name of the namespace.
- thread: A string formatted as "thread 0x00000000", where 0x00000000 is the address of the thread.
toBool(value)
This returns the truth value of the given value. null, false, integer 0, and float 0.0 will all return false; all other values and types will return true.
toInt(value: bool|int|float|char|string)
This will convert a value into an integer. Only the following types can be converted:
- bool: Converts true to 1 and false to 0.
- int: Just returns the value.
- float: Truncates the fraction and returns the integer portion.
- char: Returns the UTF-32 character code of the character.
- string: Attempts to convert the string to an integer, and assumes it's in base 10. Throws an error if it fails.
toFloat(value: bool|int|float|char|string)
This will convert a value into a float. Only the following types can be converted:
- bool: Converts true to 1.0 and false to 0.0.
- int: Returns the value cast to a float.
- float: Just returns the value.
- char: Returns a float holding the UTF-32 character code of the character.
- string: Attempts to convert the string to a float. Throws an error if it fails.
Other types will throw an error.
toChar(value: int)
This will convert an integer value to a single character. Only integer parameters are allowed.
format(vararg)
Functions much like Tango's tango.text.convert.Layout class. The "{}" style format specifiers are used. It differs from Tango's formatting in a few key ways, though:
- Tango requires the first parameter to formatting functions to be a format string, and the rest of the parameter list is inserted into the string using the format specifiers. MiniD is more like Phobos' formatting in that you can simply format anything, which will use the "{}" format specifier by default, and any string in the parameter list can be a format string. So you can write something like "format(5, " hi {} ", "bye", 4)" which will result in the string "5 hi bye 4".
- Tango formatting allows you to specify the index of the parameter to format in a format specifier (like "{1}"). In MiniD, this is still allowed, using the following rules: formatting indices are relative to the position of the format string, so "{1}" will use the second item after the format string, regardless of where the format string is. Formatting an item causes it to be skipped over when format scans the rest of the parameters. So, something like "format(5, " hi {1} ", "two ", "bye", 7)" will format the 5, then it'll insert "bye" into the string, format "two ", skip "bye" because it was formatted, and format 7, resulting in "5 hi bye two 7". This "skipping" rule is a logical extension of when you use a non-indexed formatting specifier.
- By default, when you format an item, it will call any toString metamethod defined for it. If you want to use the "raw" formatting for a parameter instead, write a lowercase 'r' immediately after the opening brace of a format specifier. So something like "format("{r}", [1, 2, 3])" will call rawToString on the array parameter, resulting in something like "array 0x00000000" instead of a string representation of the contents of the array.
Just about everything else works exactly as it does in Tango. You can use any field width and formatting characters that Tango allows.
MiniD's writef and writefln functions (as well as their analogues in the IO library) use the same internal formatting as this function, so any rules that apply here apply for those functions as well.
Console IO
write(vararg)
Prints out all its arguments to the console without any formatting (i.e. strings will not be searched for formatting specifiers). It would be the same as using "{}" for all arguments to writef.
writeln(vararg)
Same as write, but prints a newline after the text has been output.
writef(vararg)
Formats the arguments into a string using the same formatting rules as format, then outputs the resulting string to the console. No newline is printed.
writefln(vararg)
Just like writef, but prints a newline after the text has been output.
dumpVal(value)
Dumps an exhaustive string representation of the given value to the console. This will recurse (safely, you don't need to worry about infinite recursion) into arrays and tables, as well as escape non-printing characters in strings and character values. If a table has a toString metamethod, it will be called instead of recursing into the table. All other values will basically have toString called on them.
readln()
Reads one line of input (up to a linefeed) from the console and returns it as a string, without any trailing linefeed characters.
Dynamic Compilation
loadString(code: string [, name: string][, environment: namespace])
Compiles the given string containing MiniD source code into a function and returns that function. The code string is treated like the body of a function that takes variadic arguments, so the vararg expression is legal within the code string.
You can optionally pass a name for the function. If no name is passed, a default name is given to the function.
Because of the way MiniD is implemented, the dynamically-compiled function cannot access any locals or upvalues from the context in which it is loaded. It can, however, access globals in the environment in which it is created. If the environment parameter is given, it will use that environment; otherwise, it will use the environment of the calling function.
If you don't pass an explicit environment, this function must not be tailcalled. If you pass an explicit environment, it can safely be tailcalled.
Changes from MiniD 1
Added the environment parameter.
eval(code: string [, environment: namespace])
Compiles and evaluates an expression, returning its result. Note that just like loadString, eval can only access globals in its environment, and not locals or upvalues.
The optional environment parameter works just like loadString. If it is not given, it defaults to the environment of the calling function.
If you don't pass an explicit environment, this function must not be tailcalled. If you pass an explicit environment, it can safely be tailcalled.
Changes from MiniD 1
Added the environment parameter.
loadJSON(data: string)
JSON is a standard for structured data interchange based on the JavaScript object notation. JSON is so similar to MiniD code that the compiler can parse it with very little modification. This function will take a string containing JSON formatted data, parse it directly into a value, and return the top-level table or array. This is a safe parser: it only accepts strictly conforming JSON data, so you don't have to worry about malicious code being embedded in the data being compiled or executed.
toJSON(value: table|array, pretty: bool = false)
The opposite of loadJSON, this function will convert either a MiniD table or array into a JSON string which adheres to the specification. The value you pass must be either a table or array, and the only values they can contain are null, bool, int, float, char (these are converted into single-character strings in the output), string, table, and array. Any cycles in the data are detected and are an error. The return value is a string containing the formatted JSON data. If the pretty parameter is false (the default), the string will have as little whitespace as possible while still adhering to the spec; this makes the data slightly smaller for transmission. If pretty is true, it will insert line breaks and tabs as necessary to make it look a little more human-readable.
Namespace metamethods
Metamethods for namespace objects.
- opApply
- This allows you to iterate over all the members of a namespace. You can use this to iterate over the members of a module, for example.
import blah foreach(k, v; blah) // k is the name, and v is the value.
Thread metamethods
Metamethods for thread objects. These functions are all accessed as methods of threads.
- reset([func: function])
- Once a coroutine is in the 'dead' state, it's pretty much useless. However, you can use this method to reset a dead coroutine, placing it back in the 'initial' state so you can start it over again. Throws an error if the coroutine isn't in the 'dead' state.
The optional func parameter can be used to reset a coroutine object but give it a new function to use. You can get almost the same result by using a coroutine expression, except using the reset method of a dead coroutine object allows you to save a memory allocation by reusing an otherwise useless object.
Changes from MiniD 1
Added the func parameter.
- state()
- Returns a string representing the current state of the thread. Possible return values are "initial", "running", "waiting", "suspended", and "dead".
- isInitial(), isRunning(), isWaiting(), isSuspended(), isDead()
- These all return boolean values of whether the thread is in that state or not. These are a bit quicker and shorter to type than comparing the result of state() to a string literal.
- opApply()
- This allows threads to be iterated over using a foreach loop. See Functions for info on that.
Changes from MiniD 1
The integer index that this function inserts now counts from 0 instead of 1.
Function metamethods
Metamethods for function objects. These are accessed as methods of functions.
- environment([newEnv: namespace])
- If passed no parameters, returns the current environment (a namespace) of the function. If passed a namespace parameter, sets the namespace as the function's new environment and returns the function's old environment (the one that was just overwritten).
- isNative()
- Returns a bool telling if the function is implemented in native code or in MiniD.
- numParams()
- Returns an integer telling how many non-variadic parameters the function takes. For native functions, always returns 0.
- isVararg()
- Returns a bool telling whether or not the function takes variadic parameters. For native functions, always returns true.
Changes from MiniD 1
Added the isNative, numParams, and isVararg functions.
Objects
object !Object
The root of the object hierarchy, Object, is declared here. It has only one method, clone.
- clone()
- This method is defined as follows (using MiniD syntax; the method is actually implemented in native code):
Object.clone = function clone() = object : this {}
object StringBuffer
Since MiniD's strings are immutable, this object provides a mutable, resizable string object for building up strings piecewise, or just for having a modifiable string.
- clone([v: int|string])
- There are three ways to construct a StringBuffer. One is to not pass any values to the clone method at all. This creates the string buffer with a small amount of empty space, but with a length of 0. The second way is to pass the clone method an int. This indicates how many characters of empty space to reserve, as a performance enhancement in case you know in advance about how many characters you'll need. The length is still set to 0. The last way is to pass a string, which will become the contents of the new string buffer.
- append(vararg)
- This is the main way to add values to the end of the string buffer. Each value will have toString called on it if it's not a string already, and it will be appended to the end of the buffer. If a value is an instance of StringBuffer, its contents will be appended to the end of this buffer.
- opCatAssign(vararg)
- This is an overload of the append operator (~=) to allow the appending of items. This actually just calls the append function. So something like "sb ~= a ~ b ~ c" is exactly the same as "sb.append(a, b, c)".
- insert(position: int, value)
- This inserts the string representation of the value at the given position in the buffer. The position can be negative, which means an index from the end of the buffer.
- remove(start: int [, end: int = start + 1])
- This removes a sequence of characters from a StringBuffer. The start index is inclusive, the end index noninclusive. Either index can be negative, which means an index from the end of the buffer. The end index defaults to one after the start index, which means calling this function with just a start index will remove one character from the buffer.
- toString()
- Converts the contents of the string buffer to an immutable string value.
- opLengthAssign(newLength: int)
- This overload of the length operator sets the length of the buffer to the given length. If the new length is longer than the old length, the new slots are filled with the UTF-32 character U+00FFFF.
- opLength()
- This is an overload of the length operator (#), so the length of the buffer can be retrieved.
- opIndex(index: int)
- An overload of the indexing operator, for getting a character in the buffer. It returns a character. Negative indices mean an index from the end of the buffer.
- opIndexAssign(index: int, value: char)
- An overload of the index-assign operator, for setting a character in the buffer. Negative indices mean an index from the end of the buffer.
- opSlice(lo: int, hi: int)
- An overload of the slicing operator, for retrieving a subset of the buffer. The low and high indices can be negative to indicate an index from the end of the array. This returns a string, not a StringBuffer.
- opSliceAssign(lo: int, hi: int, s: string)
- An overload of the slice-assign operator, for setting a subset of the buffer. The low and high indices can be negative to indicate an index from the end of the array. The source string must be the same length as the slice.
- opApply([reverse: string])
- This allows you to iterate over a StringBuffer with a foreach loop. The optional reverse parameter will make the iteration go in reverse if you pass in the string value "reverse".
- reserve(size: int)
- You can improve performance by requesting that the internal buffer be allocated to a certain size. This is similar to constructing a string buffer with an integral size. If the given size is less than or equal to the existing buffer, this function has no effect.
- format(vararg)
- Calls format on the given parameters and appends the result to the end of the buffer.
- formatln(vararg)
- Similar to above, except it also appends the character '\n' after the formatted string.
Changes from MiniD 1
Added the format and formatln methods.
The length method has been renamed opLengthAssign, to go with the new ability to overload the length operator.
