SquareScript Standard Steps - radishengine/drowsy GitHub Wiki

Comment

Step Name: ''
['', 'this is a comment']

The Comment Step is unique in that it genuinely does not resolve to any value, so if a comment is added between two items in a value list, the final value list is exactly the same as if there had been no comment. This is unlike any other step that "does not resolve to a value", but would still add a null to the value list in this example.

The parameter(s) of a Comment step are not evaluated or checked in any way, and should be ignored by automated processing, unless delving into comments is a deliberate part of it.

Block

A Block can be thought of as a kind of Step, that:

  • uniquely, has no name string
  • only allows steps to be its parameters (not literal values like true or 7)
  • always evaluates to null

...but then again, maybe it is better to think of Blocks and Steps as separate entities instead of one being a sub-type of the other. Go with whatever perspective works best for you.

A Block can be converted into an equivalent '...!' Bottom Line Step by adding in the step name, and appending null as a final value.

The Bottom Line

Step Name: '...!'
Evaluates to: whatever the final evaluated parameter was.

Similar to the comma operator in C/C++, the Bottom Line step evaluates each parameter, discarding each value until the last one, and then this final value is the overall result of the step.

This is mostly useful for things like macros, where you want to create something as a self-contained expression and you don't have access to the whole document structure. For example, this expression uses a self-contained import:

['...!', ['< >','imported.thing'], ['<^>','imported.thing'], ['imported.thing']]

Here scoping steps are being used as parameters instead of block steps, which is odd but not invalid. In any actual situation where you might want to use this, it seems like it would make more sense to bubble the import up to a containing block, but this may be a helpful starting point.

Orderless

Step Name: '#?!'
Evaluates to: null

Normally, all steps must evaluate their parameters in order, from first to last. The Orderless step is unique in that it evaluates its parameters in undefined order, but must evaluate all of them, before the next step is run.

Detached

Step Name: '--8<--'
Evaluates to: null

The parameters of a detached step are run in their own isolated context, independent from the rest of the program. The next step in the calling code continues immediately.

The detached context includes its own scope, which by default will only contain values from the outer scope if they have been marked 'final'. One exception to this is if the very first detached step is a '< >' open-scope step, the '<^>' import-to-scope steps immediately following it may pull in copies of values from the outer scope. Otherwise, it is a syntax error for detached steps to refer to non-final scoped values from the outer scope (including trying to export out to them, via a '<v>' step).

Inside a detached scope, you can only use scoped variables for jump labels (not string literals or number literals). This means it is impossible to jump/call into a detached context, but not out of it.

The detached context also has its own stack, initially empty.

If the detached context exits with '<-', any return value(s) don't go anywhere.

Scoping

Open Scope

Step Name: '< >'
Evaluates to: null

Parameters:

  • one or more scoped value name strings

A scope defines one or more named values that exist until either the ] that marks the end of this block, or an explicit ['</>'] end scope step within the same block.

Named scope values can be referred to by using their name as a step name, with no parameters. This reference usually evaluates to the named scope value's current value, but depending on context it can sometimes be used to refer to the variable itself, for example when it is the first parameter of the ['set', ...] step, it is used as a destination to store a new value instead of fetching the current value.

An inner scope can hide the names in an outer block.

For example:

[
  ['< >', 'A'],
  ['set', ['A'], 10],
  [
    ['< >', 'A', 'B', 'C'],
    ['set', ['A'], 3],
    ['set', ['B'], 4],
    ['set', ['C'], 5],
    ['output', ['A']] // output: 3
  ],
  ['output', ['A']] // output: 10
]

Close Scope

Step Name: '</>'
Evaluates to: null

Parameters: (none)

All scopes are usually removed at the end of the block that the < > step was defined in. You can use this step instead to force the end of one or more scopes.

For example, a flattened equivalent to the example in "Open Scope":

[
  ['< >', 'A'],
  ['set', ['A'], 10],
  ['< >', 'A', 'B', 'C'],
  ['set', ['A'], 3],
  ['set', ['B'], 4],
  ['set', ['C'], 5],
  ['output', ['A']] // output: 3
  ['</>'],
  ['output', ['A']] // output: 10
  ['</>'],
]

Import to Scope

Step Name: '<^>'

Parameters:

  • string: scoped value name
  • (optional) external import identifier, if it is different from the scoped value name

Import to Scope steps must only appear immediately after a Scope step, and the scoped value name must be present in that previous step.

The external import identifier is almost always a string, with one exception: if it is of the form ['outerName'], then the value is imported from the outer scope (it must exist there, or it is a syntax error), not from a completely external source. This is useful in the event that you want to make a local copy of an outer scope variable, i.e. one with both the same name and the same value as the outer scope variable, but that you can also replace without also clobbering the one in the outer scope.

['< >', 'a'],
['set', 'a', 10],
['< >', 'a'],
['<^>', 'a', ['a']],
['output', ['a']], // output: 10
['set', ['a'], 5000],
['output', ['a']], // output: 5000
['</>'],
['output', ['a']], // output: 10

Try to Import to Scope

Step Name: '<^?>'
Evaluates to: the imported value, or null if it wasn't found

Alternate version of Import to Scope that does not make the import an absolute requirement before continuing, but instead returns null if the import was not possible.

Export from Scope

Step Name: '<v>'
Evaluates to: null

Parameters:

  • either:
    • a string, for a "global" export
    • a scoped value reference, but taken from the immediate outer scope, not the current one.
      This is only useful in the case that the current scope has a value with the same name as one in the outer scope, otherwise you could just assign to it with 'set'.
  • the value to export

Scope Value Constraint

Step Name: '<:>'

Parameters:

  • a constraint specifier
  • a constrainable value reference

See Standard Macros:

Constraints are most useful for ensuring that an imported object fulfills an expected contract (for example, a class definition includes the right methods that we are going to want to call).

Constrainable Value References

  • existing scoped value
  • a member/element access on a constrainable value reference
    If the indexer is 0, this stands in for "any integer". If the indexer is the empty string '', this stands in for "any string".

Constraints

At its simplest, this is a string like 'u8' which specifies that the value is and must remain an unsigned 8-bit integer.

TODO: more complex

  • 0.5: a floating-point number (while not technically specified, can be assumed to be 64-bit double)
  • 0: an integer (of no particular bit width or signedness)
  • '': a string (of no particular charset)
  • true: a boolean

'struct' is a special constraint. It means the value must be a JavaScript constructor function that:

  • takes three parameters: buffer (ArrayBuffer), byteOffset (number), byteLength (number)
  • has a .byteLength property (finite, positive integer) set on the constructor function object itself

The idea is to be able to create a struct-like object by doing something like:

var structInstance = new StructConstructor(
  new ArrayBuffer(StructConstructor.byteLength),
  0, StructConstructor.byteLength);

Get Scope Object

Step Name: '<this>'

Retrieves an object representing the current scope.

Program Flow

Position Label

['@:', 'myLabel']
['@:', 12345]
['@:', ['scopedLabel']]

Defines an identifier for the following step position, so that execution can jump to this position from elsewhere.

The single parameter of the Position Label step can either be a literal string, a literal number, or a scoped value reference. If it is a string or number, it is a syntax error for there to be any other Position Label with the same value elsewhere in the same document (though it's OK to have the same value as a string and a number, like 1 and "1").

If it is a scoped value reference, it is given a value representing the step position immediately from the point that it is declared, and also is automatically marked final from the point that it is declared.

If the label is a number, the actual numerical value has no special significance.

A position label can be made into an execution entry point by using a scoped value, and exporting it.

Callable Block

Step Name: 'c():'
Evaluates to: callable object, to be exported and/or used in 'c()' and 'push c()' steps

Parameters:

  • the function body as a block/step

Callable Block: Flattened

['final', ['myfunc'], ['c()', [
  ['<-']
]]]

to

['->', '__END'],
['@:', ['myfunc']],
['<-'],
['@:', '__END'],

(where '__END' represents some automatically-generated, non-conflicting jump label)

Jump

['->@', 'myLabel']
['->@', 12345]
['->@', ['scopedLabel']]

An unconditional jump to the given label, which must exist or it is a syntax error.

Exit Point

Step Name: '<-'
Evaluates to: (never evaluates)

Parameters:

  • Return value(s). May be none (for a "void" type)

When returning values from the stack, particularly multiple values, it may be more convenient to use the Pop and Exit step, '<- pop'.

If there is a step immediately after this one, and it is not a position label, it will be unreachable.

If-Then-Else

Step Name: '?'
Evaluates to: same as the successful clause, or null if all failed

Parameters:

  • primary "if" clause:
  • non-block expression
  • "then" step or block
  • (optional, repeated) "else-if" clause(s):
  • non-block expression
  • "then" step or block
  • (optional) final "else" clause:
  • "else" step or block

An example of the simplest form:

['?', ['some-expression'], [
  /* steps to run if (some-expression) evaluates to true */
]]

Loop

Step Name: '?*'
Evaluates to: null

Parameters:

  • loop label:
    • a string, to create a scope where this scoped value represents the loop itself
    • null, to not create a scope - this also means you cannot 'break' or 'continue' this loop
  • top condition, consulted before every iteration
  • step/block to repeat
  • (optional): bottom condition, consulted after every iteration (default: true)

Example of a loop like "while (some-expression) {...}":

['?*', null, ['some-expression'], [
  /* steps to be executed while (some-expression) evaluates to true */
]]

Example of a loop like "do {...} while (some-expression)":

['?*', null, true, [
  /* steps to be executed until (some-expression) does not evaluate to true */
], ['some-expression']]

Break Loop

["break", ["loop"]]

Breaks an enclosing loop that must have been given a scoped name (i.e. the first parameter of '?*' is not null) and the sole parameter is a reference to this scoped value.

['?*', 'loop', ['some-expression'], [
  ['?', ['some-other-expression'], ['break', ['loop']]],
  /*
    steps to be executed while (some-expression) evaluates to true
    and while (some-other-expression) also evaluates to true
  */
]]

Continue Loop

["continue", ["loop"]]

Jump to the bottom condition of an enclosing loop that must have been given a scoped name (i.e. the first parameter of '?*' is not null) and the sole parameter is a reference to this scoped value.

['?*', 'loop', ['some-expression'], [
  ['?', ['some-other-expression'], ['continue', ['loop']]],
  /*
    steps to be executed while (some-expression) evaluates to true
    unless (some-other-expression) also evaluates to true, in this particular iteration
  */
]]

Call and Return

Step Name: 'c()'
Evaluates to: the return value, or null

Parameters:

  • the function to call
  • the call argument(s), if any

If the function returns multiple values, all but the first will be lost. Use Call and Push to retain them all.

Some languages may require specialized variants. For example in Lua, a.b() is different from a:b() (the method call syntax), so these could (for example) translate to:

['c()', ['.[]', ['a'], 'b']]

...and:

[':c()', ['.[]', ['a'], 'b']]`

...respectively (where ':c()' would be a non-standard step type, specifically for Lua method calls).

Having said that, even in languages where there are multiple kinds of "callable", it is appropriate to use c() for any kind of call, so long as it can always be unambiguously deduced in practice.

Assignment

Assignable Values

A value is assignable if it is one of these:

  • a scoped name value
  • a member/element access (on any value, not necessarily an assignable value)
  • ['.[]', ['stack'], 'length'] - must be an integer >= 0. values on the stack will be truncated or padded with nulls

...and also, if it has not (yet) had the 'final' step applied to it.

Set

Step Name: 'set'

Parameters:

  • assignable value
  • the value to be assigned

Final

Step Name: 'final'

Parameters:

  • assignable value (except ['stack size'])
  • (optional) value

Mark a value as constant, not to change again from its current value (or the new value, if one is specified via the second parameter). This means the value can no longer be used as an assignable value.

Allocation

Signed 64-Bit Integer Literal

Step Name: 'i64'

Parameters:

  • string literal containing an integer literal, preferably hexadecimal (with 0x prefix)

Unsigned 64-Bit Integer Literal

Step Name: 'u64'

Parameters:

  • string literal containing an integer literal, preferably hexadecimal (with 0x prefix)

New Generic Object

Step Name: 'new {}'

Parameters: (none)

New Generic Array

Step Name: 'new []'

Parameters:

  • (optional) initial length

New Typed Array

Step Name:

  • 'new i8[]'
  • 'new u8[]'
  • 'new i16[]'
  • 'new u16[]'
  • 'new i32[]'
  • 'new u32[]'
  • 'new i64[]'
  • 'new u64[]'
  • 'new f32[]'
  • 'new f64[]'

Parameters:

  • length (mandatory, fixed)

New Object by Constructor

Step Name: 'new C()'
Evaluates to: the newly-constructed object

Parameters:

  • the constructor function (must be an actual JavaScript function object, not a SquareScript callable)
  • the constructor call argument(s)

New Struct

Step Name: 'new struct'
Evaluates to: the new struct object

Parameters:

  • the struct constructor function (must fulfill the constraint for a struct constructor)

New Struct Array

Step Name: 'new struct[]'
Evaluates to: the new array

Parameters:

  • the struct constructor function (must fulfill the constraint for a struct constructor)
  • length (mandatory, fixed)

Derivation

Cast to Basic Type

  • 0+: to number
  • s+: to string
  • i8, i16, i32, i64: to signed integer
  • u8, u16, u32, u64: to unsigned integer
  • f32, f64: to floating-point
  • !!: to boolean

Member/Element Access

Step Name: '.[]'

Parameters:

  • The object to access. Often a Scoped Value
  • The name or index.
  • (optional) A literal string representing the access type, if it is ambiguous

['.[]', ['a'], 'field'] represents something like a.field in the original syntax.

Note that the access type is expected to only be specified if it is ambiguous. To give an example, let's look at plain C, and the various ways you can make an access on value a:

  • a.field (struct field access)
  • a->field (struct pointer field access)
  • a[0] (array access)
  • *a (pointer dereference)

It might be tempting, looking at this list, to assume it's going to be necessary to use a series of access-type strings (maybe '.', '->', '[]' and '*'). Well, first of all, if a is a pointer then a.field is never valid, and if a is not a pointer, a->field is never valid. Since we expect to always have the information about whether or not a is a pointer-type, there's actually no ambiguity between these two cases, so they do not need separate access-type strings.

Secondly, in a C array-access expression, the indexer is always going to be an integer or an expression that resolves to an integer. If you try to do a["string"] in C, the code won't compile[1]. So, we can automatically disambiguate between field access like a.field and a->field (where the indexer will be a string literal) and an array access (where the indexer will never be a string literal).

Finally, with a *a pointer dereference there is no actual indexer. Since '*' is not a valid C field name, we could use that as the indexer, or alternatively we might use true since C has no true boolean type. Either way, we don't need an access-type for pointer dereference either. So all four kinds of access can be represented unambiguously without resorting to specifying an access-type.

[1] The closest you might see is something like a['c'], but in that case what the compiler sees is actually a[99] -- in C, a single-quoted char literal like 'c' (as opposed to a double-quoted string literal) is essentially just another way to describe an integer, like 30 or 0xFF.

Boolean Logic

Step Name:

  • '!'
  • '&&'
  • '||'

Same as the JavaScript operators, including the fact that && and || will stop evaluating once they hit a logically false or logically true operand, respectively.

Add/Concatenate

Step Name: +
Evaluates to: number or string

Parameters:

  • any number of values (usually 2)

Exactly the same semantics as the JavaScript + operator, including use of .toString(), .valueOf() etc.

If there is only 1 value, cast the value to a number (or NaN if impossible to do so).

Numerical Add

Step Name: 0+
Evaluates to: number or NaN

Like the JavaScript + operator except the result must be a number, so NaN if any of the values are not numbers (or have a relevant .valueOf() method).

String Concatenate

Step Name: s+
Evaluates to: string

Like the JavaScript + operator if both sides are coerced to strings so it must be a string concatenation, not a numerical addition.

If only one value, cast the value to a string.

Generic Math Operations

These are identical to the JavaScript operators (except // which is the same as using Math.floor() on the result of a division). They should not be used for 64-bit integers as the result is likely to lose precision.

  • subtract/unary minus: '-'
  • floating-point divide: '/'
  • integer divide: '//'
  • multiply: '*'

32-bit Integer Math Operations

Note: For add, subtract, modulus and divide you can use the generic operator and then cast the result. Multiply, however, will not get the correct "purist's" result in the edge-case of a massively overflowing (or underflowing) result, which is why it is included here.

  • multiply: 'i32*', 'u32*'
  • bitwise and: 'i32&', 'u32&'
  • bitwise or: 'i32|', 'u32|'
  • bitwise xor: 'i32^', 'u32^'
  • bitwise not: 'i32~', 'u32~'
  • left shift: 'i32<<', 'u32<<'
  • arithmetic right shift: 'i32>>', 'u32>>'
  • logical right shift (unsigned): 'i32>>>', 'u32>>>'

For 8-bit and 16-bit versions, you can just cast the result, including the generic multiply.

64-bit Integer Math Operations

  • add: 'i64+', 'u64+'
  • subtract/unary minus: 'i64-', 'u64-'
  • multiply: 'i64*', 'u64*'
  • divide: 'i64/', 'u64/'
  • modulus: 'i64%', 'u64%'
  • bitwise and: 'i64&', 'u64&'
  • bitwise or: 'i64|', 'u64|'
  • bitwise xor: 'i64^', 'u64^'
  • bitwise not: 'i64~', 'u64~'
  • left shift: 'i64<<', 'u64<<'
  • arithmetic right shift: 'i64>>', 'u64>>'
  • logical right shift: 'i64>>>', 'u64>>>'

Stack Operations

Stack Push

Step Name: 'push'
Evaluates to: null
Parameters:

  • value(s) to push

Pushes a single value onto the stack.

Stack Pop

Step Name: 'pop'
Evaluate to: (the popped value)
Parameters: (none)

Pops a single value from the stack.

Stack Object

['stack']

Stack Size

['.[]', ['stack'], 'length']

The total number of values currently on the stack

Call and Push

Step Name: 'push c()'
Evaluates to: the number of values pushed (may be zero)

Parameters:

  • the function to call
  • the call argument(s), if any

Unlike Call and Return, this step pushes the return value(s) from the function call onto the stack. This function is necessary for calling functions with multiple return values.

Pop and Exit

Step Name: '<- pop'
Evaluates to: (never evaluates)

Parameters:

  • (optional) the number of return values to pop from the stack (default: 1)
  • (optional) multiple value ordering flag: true to return the stack top first, false to return it last (default: false)

A stack-based variant of the Exit Point step.

Pop to Array

Step Name: 'pop []'
Evaluates to: a new generic array

Parameters:

  • number of values to pop
⚠️ **GitHub.com Fallback** ⚠️