Neck Sheen
Neck Sheen is a programming language that features concurrency and message passing.
Neck Sheen programs can do calculations on bits by combining nand expressions and can fork new threads with which it can send and receive bits over a queue associated with the thread.
Neck Sheen variables are static single-assignment. The
previous-variable expression, written as variable < expression
,
serves as the Φ function in loops.
Deadlocks are easy to create in Neck Sheen. I believe that the only possible race condition is sending to an empty queue that may or may not be closed yet.
Grammar
program = statement* statement = assignment | break | continue | fork | loop | receive | send assignment = variable '=' expression '.' break = loop-identifier? 'break' expression? '.' continue = loop-identifier? 'continue' expression? '.' fork = queue '+' (queue '.' | loop-body) loop = loop-identifier? loop-body receive = queue '>' ((variable | '>') loop-identifier?)? '.' send = queue '<' expression ('.' | loop-body) loop-body = '{' statement* '}' expression = variable | previous-variable | nand | '(' expression ')' previous-variable = variable '<' expression nand = expression expression loop-identifier = identifier queue = identifier variable = identifier
Comments begin with ==
and extend to the end of the line.
Identifiers are uninterrupted sequences of non-space characters,
excluding =
, .
, (
, )
, {
, }
, <
, >
, +
, and may not be
break
or continue
.
There are two identifier name spaces: one for loops and queues, and
one for variables.
Loop identifiers are declared with loop statements and fork statements and are optionally referenced by break and continue statements. The scope of a loop identifier is its body, excluding the bodies of any fork statements. The loop identifier must not duplicate any loop identifier or queue identifier that is in scope.
Queue identifiers are declared with fork statements and are referenced
by fork, receive, and send statements. The scope a queue identifier
starts with the declaration and ends at the end of the innermost
enclosing loop, excluding the bodies of other fork statements. Queue
identifiers must not duplicate any queue identifier that is in scope
or any loop identifier that is in scope. There is one predefined
queue, io
, which can be used to receive input and send output. If
the fork statement has a loop body, the queue identifier is its loop
identifier.
Variable identifiers are declared with assignment and receive
statements. The scope of the variable starts with the statement
following the declaration and ends at the end of the innermost
enclosing loop. A variable also has a pre-scope, which starts with
the start of the innermost enclosing loop and ends with the
declaration. Variable identifiers must not duplicate any variable
identifier that is in scope. There is one predefined variable, 0
,
which evaluates to false. Expressions may only reference variables
that are in scope, except for the left variable in previous-variable
expressions, which must be either in the variable's scope or the
variable's pre-scope.
Statements
The statements of a program form an implicit unnamed loop. Execution of the program terminates when that loop is exited.
An assignment statement declares and assigns the (single bit) value of the expression to the variable. A variable may not be reassigned.
A break statement exits the named or, if unspecified, the innermost enclosing loop if the expression evaluates to true or if the expression is omitted. If a loop is exited, all enclosed loop bodies are exited. If a loop is exited, all queues declared in that loop are closed.
A continue statement returns execution to the top of the named or, if unspecified, the innermost enclosing loop if the expression evaluates to true or if the expression is omitted. If the loop is continued, all enclosed loops are exited, and all queues declared in the loop are closed. If a loop is exited, all queues declared in that loop are closed.
A fork statement declares a queue with either a loop body or another queue. Executing a fork starts a new thread. The forking thread can communicate with the new thread by receiving from or sending to the queue. The new thread executes the loop body of the fork statement and can communicate with the forking thread by receiving from or sending to the queue. If the fork statement references another queue instead of specifying a loop body, the other queue must be declared with a loop body and the other queue identifier must be in scope, and the new thread communicates with the forking thread with the other queue identifier, while the forking thread communicates with the new thread with the queue identifier of the fork statement. The new thread exits when it exits the loop body of the fork statement. For the forked thread, the values and previous values of variables that are declared outside of the fork statement and whose scope includes the fork statement are frozen at the time the fork statement is executed.
A loop statement is an optionally named list of statements that are
executed in sequence. When execution reaches the end of the list of
statements or when the loop is continued with a continue
statement,
all queues declared in the loop are closed and execution resumes at
the start of the list of statements. If the loop is exited, either
due to a break
statement, a receive statement, or a continue
statement for an enclosing loop, all queues declared in the
loop are closed.
A receive statement optionally declares a variable, which is set to
the bit received from the queue. If the queue is closed for
receiving, the named, or, if unspecified, the innermost enclosing loop
is exited. If a loop is exited, all queues declared in the loop are
closed. If the variable is omitted, the received bit is ignored. To
ignore the received bit while specifying a loop, put a second >
in the
place of the variable.
A send statement evaluates the expression and sends the result to the
queue. If the queue is closed for sending and the loop is specified,
the loop is executed. The loop is unnamed, so if a named break
or a
named continue
is needed, a nested named loop should be used. The
loop cannot have the name of the queue when in the body of a fork
statement that declares the queue, since the loop in the fork has that
name.
Expressions
A variable expression evaluates to the value assigned to the variable in its declaration. The expression must be in the scope of the variable.
A previous-variable expression evaluates to the value last assigned to
the variable in a previous iteration of the loop in which it is
declared. If executing the first iteration of the loop or if the
variable declaration was not executed in any previous iteration, the
previous-variable expression evaluates to its subexpression (to the
right of the <
). The expression must be in either the scope or the
pre-scope of the variable. The predefined variable 0
has no
previous value, so previous-variable expressions with 0
always
evaluate to their subexpressions.
A nand expression is two expressions, which evaluates to the nand of its subexpressions. The nand operation is left-associative.
Parentheses can be used to specify the associativity of the nand expressions or to limit the extent of previous-variable expressions.
Queues
A fork statement declares a queue that can be used to send and receive bits between the forking thread and the new thread. A queue is initially open. The scope of the queue in the forking thread are the statements in the innermost enclosing loop that follow the fork statement. The scope of the queue in the new thread is the body of the fork statement.
All queues declared in a loop are closed when the loop iterates (either by a continue statement or by reaching the end of the loop) or when the loop exits. A closed queue is closed for both sending and receiving.
A queue is also closed when its forked thread exits.
The predefined io
queue is used for input and output. The io
queue is open for receiving as long as there is pending input. After
the input has been completely received, the io
queue is closed for
receiving. The queue is always open for sending. The scope of the
io
queue is the program, excluding the bodies of any fork
statements. The io
queue has no fork body, and thus cannot be
referenced by a fork statement.
Examples
cat
io > bit. io < bit.
race condition
outputs zero or more 0 bits, depending on a race
loop { q+{ break. } q < 0 { loop break. } io < 0. }
2-bit variable
var+{ == input: op input-bit0 input-bit1 == output: bit0 bit1 == if op is true set var to input-bit0 and input-bit1 == if op is false get var, input-bit0 and input-bit1 are ignored initial-bit0 = 0. initial-bit1 = 0. var>op. var>input-bit0. var>input-bit1. bit0 = (op input-bit0) ((0 0 op) bit0 > initial-bit0). bit1 = (op input-bit1) ((0 0 op) bit1 > initial-bit1). var<bit0. var<bit1. }