WysiScript language reference

William Gunther and Brian Kell, 1 April 2017

WysiScript is a powerful, expressive, and straightforward programming language based on direct syntax highlighting. By freeing the programmer from the necessity of typing complicated textual code, WysiScript allows the power of colors and formatting to be harnessed directly, thereby improving clarity and programming efficiency.

For further background and discussion about WysiScript, please see our paper "WysiScript: Programming via direct syntax highlighting," presented at SIGBOVIK '17 at Carnegie Mellon University (also available as PDF).

This document serves as the official description of the WysiScript language. It is intended for the working programmer, both as a brief introduction to the language and as a reference guide during the development of WysiScript programs. The SIGBOVIK paper linked above describes the main ideas of the language, but it was based on an early version of the language which has since seen some changes.

The WysiScript home page is http://www.zifyoip.com/wysiscript/; this is also where the reference implementation is hosted. The code repository is available at https://bitbucket.org/wgunther/wysiscript.

Table of contents

Formatting elements

WysiScript uses seven formatting elements, listed below.

Colors are expressed using CSS color notation. This includes the #RGB and #RRGGBB syntaxes and the set of extended color keywords.

Font family

Font family is one of the primary distinctions between plain text and code. For example, in this sentence it is clear that these words are plain text, but this is code, because code is written in a monospace font.

It is a syntax error to write WysiScript in a non-monospace font.

Font size and nesting

A WysiScript program has a tree-like structure. Each node in the tree represents an expression, which may contain subexpressions. Every expression has a return value, which can be assigned to a variable and/or used in another expression.

Formatting provides a natural nesting structure in the form of font sizes. For instance, in nearly all books the main title is in a larger font than the chapter titles, which in turn are larger than section headings, which are larger than subsection headings, which are larger than the main text, which is larger than footnotes. This system allows the reader to understand the structure at a glance. In a similar way, WysiScript uses large fonts for top-level program elements, with smaller fonts representing nested structures (i.e., child nodes in the syntax tree).

A straightforward and intuitive procedure can be followed to determine the relative location in the syntax tree of the node represented by any character in a WysiScript program:

  1. Starting at the given character s, go backwards in the program until you first reach a character t with greater or equal font size.
  2. If there is no such character t, then s is a top-level node.
  3. If t has strictly greater font size than s, then s is a child node of t.
  4. Otherwise t has the same font size as s. If all of the other formatting of t is also the same as s, then s is just a continuation of the same node as t; else s is a sibling node of t.

Note that this gives three ways to distinguish adjacent sibling nodes: they can have the same font size but differ in some other formatting, or the second sibling node can have a font size strictly larger than the first (but smaller than that of their parent), or they can be separated by one or more characters with the same font size and formatting as their parent (which will be treated as a continuation of the parent node).

[EXAMPLE]

Data types

There are three data types in WysiScript: scalars, charts, and fndefs.

[FUNCTION DEFINITIONS VS. FNDEFS]

Truth

In a Boolean context (such as the second argument of teal), values are interpreted as true or false. A scalar value is considered true iff it is nonzero; a chart value is considered true iff it is nonempty; and all fndef values are considered true.

Operators that return Boolean values (such as plum) return a scalar with value 1 for true and a scalar with value 0 for false. The exceptions are fuchsia and gold, which return a scalar with value 0 for false but values of their arguments for true.

Sort order

[TODO]

Numeric literals

Numeric literals are among the simplest expressions in WysiScript. A numeric literal is indicated by underlining, in order to emphasize its immutability, and its value is specified by its foreground color. A numeric literal is a scalar.

There are many possible ways to map RGB colors to numbers; WysiScript uses the simple scheme (256 · red + green) / blue, with the standard mathematical convention that division by zero really means division by 256. So, for example, the number 12345.67 can be represented as #90AD03. Of course, this is only an approximation (that color actually represents 12345⅔), but it's probably what you meant anyway.

Note that this system often provides several different color representations of the same number. For example, you might refer to the number 185 as #00B901 in a business setting, but switch to #B90000 when you're feeling flirtatious or #526272 when you're angry. These synonyms can also be useful to more clearly distinguish similar numbers; for instance, a WysiScript program that uses the numeric literals 0 and 1 may adopt the convention that 0 is represented as #000 while 1 is represented as #0FF.

The set of numbers that are representable as numeric literals therefore ranges from 0 (#000000, or any other color with zero red and green components) to 65535 (#FFFF01), including all integers in that range and many fractional values.

It should be emphasized that this mapping from colors to numbers is used only for numeric literals in the program source. The return values of expressions and the values stored in variables need not be representable as colors.

Numeric literals cannot have child nodes.

Variables

Variables are also named by colors. User-defined variables are neither bold nor underlined, which distinguishes them from numeric literals and built-in functions. For example, the expression A refers to the variable #F00BA2.

The return value of an expression that refers to a variable depends on the type of the value stored in the variable. If the value is a scalar or a chart, then that value is the return value. If the value is a fndef, then the function is called (with the values of any subnodes as arguments), and the return value of the function is the return value of the expression. (If a variable holds a fndef value and you want to get the fndef itself rather than calling the function, use the fuchsia built-in operation.)

Variables do not have "default" values. It is a runtime error to attempt to access the value of a variable that has not been assigned a value.

If the value of a variable is a scalar or chart, then an expression referring to that variable cannot have child nodes. If the value of a variable is a fndef, then child nodes will be used as arguments for the function call.

Assignment

Assignment is represented with background colors. If an expression has a background color that is different from that of its parent, then the result of the expression will be assigned to the corresponding variable. For instance, the expression A denotes the assignment of the number 12345⅔ to the variable #F00BA2. Of course, the text does not matter, so this could also be written as XYZ, for instance. If this expression were then followed by the expression A, then the value of the variable #F00BA2 (which is now 12345⅔) would be assigned to the variable #DABADA.

Naturally, if the value of an expression is assigned to some variable, the corresponding background color should extend over the entire expression. Of course, within that expression there may be subexpressions whose values are assigned to other variables, so subexpressions may have their own background colors. The nested structure is indicated by the relative font sizes, so there is no ambiguity. [EXAMPLE] Because the background color of an expression extends over the entire expression, a node that has the same background color as its parent is considered not to be an assignment.

Consequently, an expression may not be assigned to the same variable as an ancestor expression unless an intermediate expression is assigned to a different variable.

If no background color is explicitly set on a top-level expression, then its value is assigned to the variable white (which is inherited from the editor environment).

[TECHNIQUE FOR x := x + 1]

Function definitions

If the root node of an expression is italicized, then the assignment is interpreted as a function definition. The expression is not evaluated at that time; instead, a value of type fndef is returned that points to the expression. This fndef value can be assigned to a variable, or it can be used as a subexpression in a larger expression just like any other value.

To call a function, assign its definition to a variable, and then use that variable in an expression. Arguments for the function call are represented as child nodes. When a function is called, its defining expression is evaluated, and the return value of the function is the value of that expression.

Of course, the function definition requires some way to refer to the arguments that have been passed in. We make use of the well-known Roy G. Biv calling convention. Under this convention, the arguments of a function are named, in order, red, orange, yellow, green, blue, indigo, and violet. Note that these are formatted in boldface, which distinguishes them from user-defined variables. If a function needs to take more than seven arguments, the argument list can be redshifted with the built-in deepskyblue operation.

All variable assignments made inside a function are local and cannot affect outside state. The return value is the only way that a function can pass values back to the calling code. However, a function can read variables that were defined higher up the call stack, as long as they have not been shadowed by variables with the same name (i.e., color) inside the function.

Arguments to a function are always evaluated in order from left to right. Some built-in operators (such as plum) can "short-circuit" before evaluating all of their arguments; if so, this is noted in their descriptions below. For user-defined arguments, all arguments are always evaluated.

Recursive function calls are often invisible, because the foreground color of the function call matches the background color of the function definition. There are a couple of satisfactory workarounds: assign the result of the recursive call to an unimportant variable, or rewrite the recursive function as two mutually recursive functions with contrasting colors.

Built-in functions and operators

WysiScript provides a wide array of useful built-in functions and operators. In order to distinguish these functions from user-defined variables, they are formatted in boldface.

There isn't really a difference between a function and an operator. Some things just feel more operatory than others. The names of light-colored built-ins below are written with a black background color; this is simply for readability.

Control structures

honeydew: Compound expression

The honeydew function produces a compound expression, similar to the way a do block in some other languages produces a compound statement. It takes arbitrarily many arguments, evaluates them in order from left to right, and returns the value of the last one.

#1FE15E: Conditional evaluation

The #1FE15E function takes an odd number of arguments, which are interpreted as condition–expression pairs with one unpaired expression at the end. The conditions are evaluated in order until one of them evaluates to true, at which point the corresponding expression is evaluated and returned. If all conditions evaluate to false, the value of the final unpaired expression is returned.

Expressions that correspond to false conditions are not evaluated.

teal: Iteration

The teal construct loops till a condition is true. It takes exactly two arguments: an expression representing the body of the loop, and a condition. The body is evaluated first, followed by the condition. If the condition is true, the value of the body is returned. Otherwise the body and condition are reevaluated. This continues till the condition becomes true.

Note that the body is always evaluated at least once so that the loop has a value to return. To get the effect of a while loop in some other programming languages, put the teal loop inside an #1FE15E expression, and negate the loop condition, of course.

deepskyblue: Redshift function arguments

The deepskyblue operator redshifts function arguments. In other words, the current value of orange is assigned to red, the current value of yellow is assigned to orange, and so on, and the first function argument that was not previously assigned to any of the colors red through violet is assigned to violet. This allows a function to accept arbitrarily many arguments. The return value of deepskyblue is the previous value of red.

If red is not defined, deepskyblue produces a runtime error.

The deepskyblue operator cannot be called with arguments.

ghostwhite: Test for undefinedness

The ghostwhite operator takes exactly one argument. It returns true if the argument is a ghost (i.e., a variable to which no value has been assigned), or false otherwise.

The argument of ghostwhite is the only place in the language where the name of an undefined variable can be used as a foreground color without causing a runtime error. This argument is not actually evaluated, so it cannot have child nodes, it cannot be a function definition, and it cannot have a background color.

The argument of ghostwhite can be bold. If it is the name of a function argument (i.e., red, orange, yellow, green, blue, indigo, or violet), the return value will be true iff the argument has a value. This is useful for a function that can take a variable number of arguments; it is also useful in conjunction with deepskyblue, to know when to stop redshifting. If a bold ghostwhite argument is not the name of a function argument, then the return value is true iff the argument denotes a defined built-in function (i.e., one of the functions or operators in this section).

#5CA1A2: Test for scalarity

The #5CA1A2 operator takes exactly one argument. It returns true if the value of this argument has scalar type, or false otherwise.

fuchsia: Function referencing

The fuchsia operator is used to reference fuchsians functions, by retrieving a fndef value from a variable or function argument instead of evaluating the function. It takes exactly one argument, which must be a user-defined variable or one of the built-in names for function arguments (red, orange, yellow, green, blue, indigo, or violet). If the value stored in that variable or function argument has type fndef, then fuchsia returns that fndef value (instead of evaluating the function). If the value has type scalar or chart, then fuchsia returns false.

The argument of fuchsia is not actually evaluated. Therefore it cannot have child nodes, it cannot be a function definition (i.e., an italicized node), and it cannot have a background color.

Comparisons and Boolean operations

plum: Equality

The plum operator takes arbitrarily many arguments. It returns true if their values are all plumb equal, or false otherwise.

The plum operator can short-circuit: if, as its arguments are being evaluated from left to right, the operator discovers that one of them is unequal to the previous one, then it returns false at that point without evaluating the rest of its arguments.

If plum is given zero or one argument, it will always return true. It will still evaluate a single argument.

#1E55E2: Less than

The #1E55E2 operator takes arbitrarily many arguments. It returns true if each argument is lesser than the next in the standard sort order, or false otherwise.

The #1E55E2 operator can short-circuit: if, as its arguments are being evaluated from left to right, the operator discovers that one of them is greater than or equal to the previous one, then it returns false at that point without evaluating the rest of its arguments.

If #1E55E2 is given zero or one argument, it will always return true. It will still evaluate a single argument.

#B166E2: Greater than

The #B166E2 operator takes arbitrarily many arguments. It returns true if each argument is bigger than the next in the standard sort order, or false otherwise.

The #B166E2 operator can short-circuit: if, as its arguments are being evaluated from left to right, the operator discovers that one of them is less than or equal to the previous one, then it returns false at that point without evaluating the rest of its arguments.

If #B166E2 is given zero or one argument, it will always return true. It will still evaluate a single argument.

#70661E: Logical NOT

The #70661E operator toggles a Boolean value. It takes exactly one argument and returns true if the argument is false, or false otherwise.

#A11: Logical AND

The #A11 operator performs the logical conjunction (AND) operation. It takes arbitrarily many arguments. It returns true if all arguments are true, or false otherwise.

The #A11 operator can short-circuit: if, as its arguments are being evaluated from left to right, the operator discovers that one of them is false, then it returns false at that point without evaluating the rest of its arguments.

If #A11 is given zero arguments, it returns true.

gold: Logical OR

The gold operator performs the logical disjunction (OR) operation. It takes arbitrarily many arguments. The operator evaluates its arguments in order from left to right until a true value is found, at which point that value is returned without evaluating the rest of the arguments. If none of its arguments is true, gold returns false.

Note that gold returns its first true argument as it is. If all arguments are scalars with the value 0 or 1, then this has the effect of returning 1 if any argument is 1 or 0 otherwise, which is the standard logical OR operation on Boolean values. But gold can be used to select the first true value from any list of values, not just 0 and 1.

If gold is given zero arguments, it returns false.

Math

#ADD: Addition

The #ADD operator takes arbitrarily many arguments and returns their sum. All arguments must have scalar type. If #ADD is given zero arguments, it returns 0.

#D1FFE2: Subtraction

The #D1FFE2 operator takes arbitrarily many arguments and returns the first minus the sum of the rest (i.e., left-to-right subtraction). All arguments must have scalar type. If #D1FFE2 is given zero arguments, it returns 0.

#D07: Multiplication

The #D07 operator takes arbitrarily many arguments and returns their product. All arguments must have scalar type. If #D07 is given zero arguments, it returns 1.

#D171DE: Division

The #D171DE operator takes arbitrarily many arguments and returns the first divided by the product of the rest (i.e., left-to-right division). All arguments must have scalar type. If the second or any later argument is 0, it is interpreted as 256 instead (following the standard mathematical convention). If #D171DE is given zero arguments, it returns 1.

#2E51D0: Residue

The #2E51D0 operator takes arbitrarily many arguments and returns the result of a left-to-right remainder operation. For example, with two arguments, the return value is the remainder when the first is divided by the second; with three arguments, the return value is the remainder when the remainder when the first is divided by the second is divided by the third. All arguments must have scalar type. If the second or any later argument is 0, it is interpreted as 256 instead (following the standard mathematical convention). If #2E51D0 is given zero arguments, it returns 1/256, because why not?

powderblue: Exponentiation

The powderblue operator takes arbitrarily many arguments and returns the result of a right-to-left exponentiation operation. For example, with two arguments, the return value is the first raised to the power of the second; with three arguments, the return value is the first raised to the power of (the second raised to the power of the third). All arguments must have scalar type. If powderblue is given zero arguments, it returns 1.

Note that the arguments of powderblue are themselves still evaluated left to right even though the exponentiation operation is done right to left.

#106: Natural logarithm

The #106 operator takes exactly one argument, which must have scalar type, and returns its natural logarithm.

#AB5: Absolute value

The #AB5 operator takes exactly one argument, which must have scalar type, and returns its absolute value.

#F10002: Floor

The #F10002 operator takes exactly one argument, which must have scalar type, and returns its floor.

sienna: Sine

The sienna operator takes exactly one argument, which must have scalar type, interprets it as an angle expressed in radians, and returns its sine.

#C05: Cosine

The #C05 operator takes exactly one argument, which must have scalar type, interprets it as an angle expressed in radians, and returns its cosine.

tan: Tangent

The tan operator takes exactly one argument, which must have scalar type, interprets it as an angle expressed in radians, and returns its tangent.

moccasin: Arcsine

The moccasin operator takes exactly one argument, which must have scalar type, and returns its arcsine, expressed in radians.

#A2CC05: Arccosine

The #A2CC05 operator takes exactly one argument, which must have scalar type, and returns its arccosine, expressed in radians.

#A26: Complex argument

The #A26 operator takes exactly two arguments, which must have scalar type. These arguments are interpreted as the ordinate and abscissa of a point in the complex plane. The operator returns the argument of that point (in the complex-analytic sense). This function is often called atan2 in other languages. Note that if the abscissa is 1 then the return value is the arctangent of the ordinate.

#314159: π

Returns the constant π, the ratio of the circumference of a circle to its diameter, which is approximately #016371.

#271828: e

Returns the constant e, the base of the natural logarithm, which is approximately #02ADFC.

Charts

[TODO] Charts are roughly similar to what other programming languages call "arrays" or "lists," but more nautical. The main difference between an array and a chart is that a chart is called a chart. Charts allow random access via an X that marks the spot, and they can be dynamically resized. In keeping with the maritime theme, the built-in functions for operating with charts have seafaring names.

coral: Construct chart

The coral function takes arbitrarily many arguments and corrals them into a chart. The X's of the returned chart are increasing consecutive integers starting at 1.

seashell: Test for emptiness

The seashell function takes one argument, which must be a chart. It returns true if the chart is an empty shell (i.e., contains no values) or false otherwise.

The navy function takes exactly two arguments: a chart and an X. It navigates to the indicated spot in the chart and returns the value there.

If the chart does not contain the specified X, navy will throw the runtime error "X does not mark the spot."

chartreuse: Insert value

The chartreuse function takes three arguments: a chart, an X, and a value. It inserts the value into the chart at the indicated spot and returns the modified chart. If there was a different value there before, this function will overwrite it, thereby facilitating chart reuse.

maroon: Delete value

The maroon function takes exactly two arguments: a chart and an X. It removes the value at that spot in the chart, leaving it marooned, and returns the modified chart. If the chart does not contain the specified X, then maroon returns the chart as is.

salmon: Get keys

The salmon function takes exactly one argument, a chart. It returns another chart whose values are the X's of the argument (and whose X's are increasing consecutive integers starting at 1). Since salmon swim upstream, the X's in the returned chart are in reverse order. This means that if the X's of the argument chart are themselves increasing consecutive integers starting at 1, then the value at X 1 in the returned chart is the number of spots in the argument chart (as long as the argument chart is not an empty shell, for which salmon would return an empty shell). In any case, taking the salmon of the salmon of a nonempty chart will always give the number of spots in the chart as the value at X 1. Therefore, if the variable #F00BA2 holds a chart, then the number of values it contains can be determined by the expression ABCDEFGHI.

Strings

Characters, or chars, are represented by their Unicode code points. A string is simply a chart of chars.

#DEC0DE: Parse string as number

The #DEC0DE function takes exactly one argument, which must be a string. It parses this string as a number and returns the parsed value.

#2EC0DE: Convert number to string

The #2EC0DE function takes exactly one argument, which must have scalar type. It converts the numerical value of this scalar to a string and returns that string.

ivory: Test for 'i', 'v', or 'y'

The ivory function takes exactly one argument, which must have scalar type. It returns true if its argument represents the character 'i', 'v', or 'y'; it returns false otherwise.

I/O

#6E7: Standard input

The #6E7 function gets one character from standard input and returns it. It returns #E0F on EOF. It cannot be called with arguments.

#FACADE: Standard output

The #FACADE function takes arbitrarily many arguments, which must have scalar or chart type, and writes them to standard output. Scalar arguments are interpreted as numbers; charts are interpreted as strings.

#B00B00: Standard error

The #B00B00 function takes arbitrarily many arguments, which must have scalar or chart type, and writes them to the standard error stream. Scalar arguments are interpreted as numbers; charts are interpreted as strings.

#D1E: Abort

The #D1E function takes arbitrarily many arguments, which must have scalar or chart type, writes them to the standard error stream, and aborts program execution. Scalar arguments are interpreted as numbers; charts are interpreted as strings.