SquareScript - radishengine/drowsy GitHub Wiki
SquareScript is a simplistic mini-language, intended as a target for transforming "alien" code into a manageable common format, that can then (hopefully) be run via a SquareScript interpreter.
SquareScript syntax is based on JavaScript Arrays. It is designed to be JSON-compatible.
There are two fundamental elements of SquareScript:
- Step: An array where the first element is a string (the instruction name of the step), followed by zero or more parameter values, each of which are one of the following:
- string
- numbers
- boolean
null
- Step
- Block
-
Block: An array of steps. May be empty. If used as an expression, always evaluates to
null
.
You can tell the difference between a step or a block using:
if (typeof stepOrBlock[0] === 'string') {
// it's a step
}
else {
// it's a block
}
A SquareScript document is normalized when the following is true:
- The Document is not JSON-encoded text, but an actual value object
- The Document Root is a Normalized Step.
- Get the Document Root:
- If the document is an Array, this is the Document Root.
- If the document is a String, decode it as JSON.
- If the JSON parsing is successful, and the decoded value is an Array, this is the Document Root.
- Otherwise, throw a syntax error.
- If the document is an object with a
.toJSON()
method, try calling this.- If the method exists, and returns an Array, this is the Document Root.
- Otherwise, throw a syntax error.
- Create a Scope-Stack containing a single, empty Scope.
- Perform the Step Normalization process on the Document Root, in the context of this Scope-Stack.
- The result of this process is the Normalized Document Root, and the Document is now normalized.
A SquareScript step is normalized when the following is true:
- It must be an Array.
- The Array's elements must be either:
- ...all normalized SquareScript steps (including the case where there are none)
- ...a String (this is the Step Name), followed by zero, one or more of the following (the Step Parameters):
- a String/Number/Boolean/
null
literal value
(not allowed, for JSON compatibility:undefined
,NaN
,Infinity
,-Infinity
) - a normalized SquareScript step
- a String/Number/Boolean/
- The Step Name cannot be the name of a registered Macro.
- The Array must have been frozen with Object.freeze().
- If the Step is a candidate for being a Re-Used Step, it must be one.
- If the Step relates to scoping and/or jump labels, it must have no syntax errors according to the rules of these concepts.
This process is performed in the context of a supplied Scope-Stack.
- Check if this Step is a candidate for being replaced by a Re-Used Step (see below), and if so, do this.
- If the Step Name is the name of a registered Macro, the corresponding Macro Handler function is called, with the Step in its current form as its sole argument.
- The Handler must return a new step to replace the original, which may itself require normalization (to support, for example, recursive macro expansion), so begin step normalization again.
- If it's not explicitly determined that it's OK to modify the input Step Array, it is cloned using
.slice()
and we work with the clone instead of the original. - The current length of the scope-stack is noted.
- If the Step Name is one of the Scope Steps (
< >
,<^>
,<v>
,<:>
,</>
), the scope-stack must be consulted and/or updated, and there must be no scope-related syntax errors. - If the Step Name is one of the Jump Label Steps (
@:
,->@
), the rules are checked for valid jump labels, particularly when they are scoped. - Every Array found as elements inside the Step Array is normalized in the context of the current scope-stack.
- Once there are no more elements to process, the scope-stack is truncated back to the length it had at the beginning of this process (i.e. any scopes that were not explicitly closed are now implicitly closed).
- The Array is frozen using
Object.freeze()
. - The Step is now Normalized.
Part of Step Normalization is that some steps should be replaced with a Re-Used Step, meaning that two equivalent steps are not just Arrays with equivalent contents, but actually identical objects, after normalization.
These are the rules about when to employ a Re-Used Step:
- Any Step that is an empty block.
- Two or more Steps that are both scoped value references to the exact same scoped value. For example, in a context where "a" is a scoped value, when a step like
['add', ['a'], ['a']]
is normalized, its two parameters should be literally the same Array repeated. But elsewhere in the program where['a']
refers to a different scoped value that happens to have the same name, that will be normalized to a different object.-
'<this>'
is an implicitly created value in every scope, representing the scope object itself.
-
- Two or more
['.[]', ...]
member/element accesses where:- the first parameter is the same Re-Used Step
- the second parameter is the same literal, of type string, boolean or
null
(i.e. not numbers, and not variables) - the third parameter is the same, or equally absent (if present it must be a string, and no more than 3 parameters are allowed)
SquareScript flavors are like contracts which specify rules about the form of the script. Whether or not a script object conforms to a particular flavor can be determined from static analysis.
The two main flavors are:
-
Jumpless: The code never uses a
['->','label']
jump step. - Flat: Only one, top-level block can appear in the code.
The two flavors can be automatically translated between each other. For example, a jumpless squipt:
[
// if (some-expression), then run these steps
['?', ['some-expression'], [
['step-1'],
['step-2'],
['step-3']
]],
['step-4']
]
...could be translated to a flat squipt that looks something like this:
[
// if NOT (some-expression), then jump to __label1
['?', ['!',['some-expression']], ['->','__label1']],
['step-1'],
['step-2'],
['step-3'],
['@','__label1'],
['step-4']
]
...or vice-versa. __label1
here means an automatically-generated label that is guaranteed not to conflict with any other existing label in the script, so it may not be exactly that.
Returns a SquareScript in normalized form for source
, which may be:
- an Array
- a JSON-encoded string where the top-level value is an Array
- an object with a
.toJSON()
method that returns an Array
The return value is always frozen, meaning it cannot be modified. If source
is an Array and there is no other reference to it, you can pass true
for the optional second parameter to indicate that it's OK to freeze and return this same object instead of making a copy of it.
For example, if you call:
var script = Terp.squipt([ ['step-1'], ['step-2'] ]);
...it would be appropriate to pass true
for the second parameter here, because the array only exists here, as a function parameter, and no other reference to it lives on in the calling code after the function call.
On the other hand, in this case:
var source = [ ['step-1'], ['step-2'] ];
var script = Terp.squipt(source);
// "source" continues to exist
...there is still a reference to the source array held in the source
variable, so it would not be appropriate to pass true
for the second parameter of Terp.squipt()
.
Similar to Terp.squipt()
except it returns a JSON-encoded string version of source
.
This is generally similar to JSON.stringify(source)
except that source
must be a valid SquareScript, and the result has specialized whitespace rules intended to make it a bit more human-readable, for debugging purposes.
For example, Terp.squipt.stringify()
will output this:
['?', ['some-expression'], [
['step-1'],
['step-2'],
['step-3']
]]
...where JSON.stringify()
will either output this:
['?',['some-expression'],[['step-1'],['step-2'],['step-3']]]
...or (depending on the 3rd parameter to JSON.stringify()
), something like this:
[
'?',
[
'some-expression',
],
[
[
'step-1'
],
[
'step-2'
],
[
'step-3'
]
]
]
Similar to Terp.squipt()
except the resulting document is flattened, meaning that there is only one block that contains a linear series of steps, with all conditional blocks and loop blocks translated to label-jumps, and any < >
scope that would normally expire at the end of its enclosing block instead given an explicit </>
end-of-scope marker.
The top-level of the result is guaranteed to be a block, even if it only contains one step, and all scopes will be given end-of-scope markers, even ones at the top level.
Similar to Terp.squipt()
except the resulting document is guaranteed to contain no jump labels, which will be converted to conditional blocks and loop blocks. End-of-scope markers may also be removed in cases where they now seem redundant.
A Transcriber is a helper object for building up complex SquareScript documents programmatically.
If you extend this to create a specialized Transcriber type, be sure to set the .constructor
value on the prototype. This is used by existing methods to spawn additional, context-specific transcriber objects for handling parts of the document.
Add a step with the given name and parameters.
Returns the transcriber object.
Get a Transcribed Value Object for the given value
, which must be a string, number, boolean or null
.
Get a Transcribed Value Object for a value that only exists in the current scope, with the given name.
If a value with this name already exists in the current scope, the existing value object will be returned unless the optional forceNew
argument is specified and true
, in which case a new scope will be created.
Get a Scoped Value representing an import from an external location, which is always represented by a string.
The optional localAlias
string can be used to give a "short name" that local code will use to refer to this import, otherwise
Get a new value object that represents an operation on this value object. symbol
is a string containing the operator. If this is a binary operation like a + b
, then the rightHandSide
parameter should be specified (to define b
, in this case). rightHandSide
should only be omitted if this is a unary operation, like -a
.
Get a new value object that represents a member access (.[]
expression step) on this value.
transcriber.scopedValue('a').member('field')
is the equivalent of something like a.field
or a["field"]
, depending on the original syntax.
Get a new value object that represents calling this value as a function, with the given parameters, which may be literal values like true
or 10
, or other value objects.
Same as .member(memberName).call(...)
.