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.
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 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.
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:
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]
There are three data types in WysiScript: scalars, charts, and fndefs.
[FUNCTION DEFINITIONS VS. FNDEFS]
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.
[TODO]
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 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 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]
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.
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.
honeydew
: Compound
expressionThe 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 evaluationThe #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
:
IterationThe 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 argumentsThe 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 undefinednessThe 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 scalarityThe #5CA1A2
operator
takes exactly one argument. It returns true if the value
of this argument has scalar type, or false otherwise.
fuchsia
:
Function referencingThe 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.
plum
:
EqualityThe 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 thanThe #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 thanThe #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 NOTThe #70661E
operator
toggles a Boolean value. It takes exactly one argument and returns
true if the argument is false, or false otherwise.
#A11
: Logical
ANDThe #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 ORThe 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.
#ADD
:
AdditionThe #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
:
SubtractionThe #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
:
MultiplicationThe #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
:
DivisionThe #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
:
ResidueThe #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
:
ExponentiationThe 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
logarithmThe #106
operator takes
exactly one argument, which must have scalar type, and
returns its natural logarithm.
#AB5
: Absolute
valueThe #AB5
operator takes
exactly one argument, which must have scalar type, and
returns its absolute value.
#F10002
:
FloorThe #F10002
operator
takes exactly one argument, which must have scalar type,
and returns its floor.
sienna
:
SineThe sienna
operator takes
exactly one argument, which must have scalar type,
interprets it as an angle expressed in radians, and returns its sine.
#C05
: CosineThe #C05
operator takes
exactly one argument, which must have scalar type,
interprets it as an angle expressed in radians, and returns its cosine.
tan
: TangentThe tan
operator takes
exactly one argument, which must have scalar type,
interprets it as an angle expressed in radians, and returns its tangent.
moccasin
:
ArcsineThe moccasin
operator takes exactly one argument, which must have scalar
type, and returns its arcsine, expressed in radians.
#A2CC05
:
ArccosineThe #A2CC05
operator
takes exactly one argument, which must have scalar type,
and returns its arccosine, expressed in radians.
#A26
: Complex
argumentThe #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
:
eReturns the constant e, the base of the natural logarithm,
which is approximately
#02ADFC
.
[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 chartThe 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
emptinessThe 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.
navy
: Retrieve
valueThe 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
valueThe 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 valueThe 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
keysThe 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
A
B
C
D
E
F
G
H
I
.
Characters, or chars, are represented by their Unicode code points. A string is simply a chart of chars.
#DEC0DE
: Parse string
as numberThe #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 stringThe #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.
#6E7
: Standard
inputThe #6E7
function gets one
character from standard input and returns it. It returns
#E0F
on EOF. It cannot be
called with arguments.
#FACADE
: Standard
outputThe #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 errorThe #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
: AbortThe #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.