Rail

From Esolang
Jump to navigation Jump to search

Rail is a two-dimensional language along the lines of Befunge and PATH. It was invented by Jonathon Duerig in 2005. There are four unique things about Rail. First, the program counter in Rail is modelled as a train on a railroad, not as a free-moving pinball. This means that every path must be explicitly laid out and the decision rules are not about absolute direction, but about which rail to choose next. Second, Rail provides both local variables and a clean procedural model. This means that it is about as easy to program as a normal stack-based language. Third, Rail is in many respects a functional language. It provides LISP-style lists, garbage collection, and immutable values. Finally, the creator deliberately avoided the minimalism of many other two-dimensional languages. In addition to a rich set of builtins, a standard library is planned which should make it possible to write a useful program.

Terms

Train
This is the program counter. The metaphor is a railroad. Trains can only move on rails, junctions, and builtins. Since trains cannot move on whitespace, two tracks will never interfere if they are separated by whitespace.
Crash
This is what happens when something illegal happens. The program is halted and debugging information may print. All of the built-ins list situations where they crash. Your modules can use the 'u' and 'b' builtins to build precondition/postcondition checking into your own functions. If a train runs off the track into whitespace or into some unknown type of square, it also crashes.
Rail
These are the pieces which direct the flow of the program. The program counter moves along rails. Rails constrain movement to one of two directions. The choice of directions is based on context. It is possible to move along the same rail in different directions at different points in program execution.
Connection
Two adjacent rails can form a connection. The direction of the train and the connections of a rail determine where the train goes next.

Environment

A train moves along rails, junctions, and commands in a two-dimensional ASCII field. Commands are executed as they are passed. These commands modify a global data stack, execute functions, etc. The train will not move across unknown characters or whitespace. The train can travel in any of the 8 compass directions.

Examples

Hello, world!

Any number of lines may precede the one with the '$' on the far left.

The 'main' next to it is the name of the function. This is a simple program so only the 'main' function need be defined.

$ 'main' (--):
 \
  \-[Hello World!\n\]o-#

Cat program

The (--) next to the 'main' is just a comment. It says that this function takes nothing from the stack and doesn't add anything to the stack. This uses Forth notation, described in detail in "Built-in commands", below.

$ 'main' (--):
 \
 | /---------\
 | |         |
 | \    /-io-/
 \---e-<
        \-#

Ackermann function

$ 'main' (--)
 \  Read m and n as input. Just one number each for now
  \  Input is in the form "m\rn" where \r is a return character
   \-[Enter m: ]oi(!m!)[\n\]o-[Enter n: ]oii(!n!)[\n\]o-----(n)-(m)-{ackermann}-[): ](n)[,](m)[A(]oooooo--#
  

$ 'ackermann' (n m -- A(m,n))  A(m,n) = n+1             if m=0
 \                                    = A(m-1,1)        if m>0 and n=0
  \                                   = A(m-1,A(m,n-1)) if m>0 and n>0
   \-(!m!)-(!n!)-\
                 | 
                 /
      /---q(m)0--     
      |
     t^f        f/-(n)1s-(m)-{ackermann}-(m)1s-{ackermann}-#  
     / \-(n)0q--<
     |          t\-1-(m)1s-{ackermann}-#
     \-(n)1a-#

Movement

Movement is along the four kinds of rail:

'-' := Horizontal rail
'|' := Vertical rail
'/' := Ascending rail
'\' := Descending rail

Or along one of the seven kinds of junction:

'*' := Universal junction
'+' := HV junction
'x' := Diagonal junction
'v', '^', '<', '>' := Y junctions (a -- ). Left on f. Right on t. Else crash.

All of the built-in command characters described below are considered universal junctions for the purposes of movement.

Rails

Each rail has two basic directions. These directions are the two that run along it. For instance, the ascending rail forces the train to either go northeast or southwest. The rails can be connected together to determine which direction the train travels in. The most basic kind of connection is when rail of the same type is placed in series:

$
 \
  \
   \
    #

A train entering at the northwest end of this track will go southeast through each square in turn until it reaches the end square at the southeast end of the track. The '$' is where the train always starts. '#' is where the train ends.

Each kind of rail can have a connection with itself. In addition, each kind of rail can connect to two other kinds of rails:

'|' connects to '\' and '/'
'-' connects to '\' and '/'
'/' connects to '|' and '-'
'\' connects to '|' and '-'

For instance, here is a legal connection, from '\' to '-':

$
 \
  -#

The transition from '\' to '/', on the other hand, is illegal. This kind of perpendicular transition doesn't make sense on trains.

$
 \
  \ #
   /

When a train moves across a connection, it changes its direction slightly. The general rule is that it changes to the closest direction on the compass rose.

Here, the train starts moving southeast, but when it connects from '\' to '|', it changes direction from southeast to south.

$
 \
 |
 #

More exhaustively:

North '|' to '\' changes to Northwest
North '|' to '/' changes to Northeast
South '|' to '\' changes to Southeast
South '|' to '/' changes to Southwest

East '-' to '\' changes to Southeast
East '-' to '/' changes to Northeast
West '-' to '\' changes to Northwest
West '-' to '/' changes to Southwest

Northwest '\' to '|' changes to North
Northwest '\' to '-' changes to West
Southeast '\' to '|' changes to South
Southeast '\' to '-' changes to East

Northeast '/' to '|' changes to North
Northeast '/' to '-' changes to East
Southwest '/' to '|' changes to South
Southwest '/' to '-' changes to West

There are two kinds of connections. Primary connections are where there is a rail one square in the direction of movement of the appropriate type. Secondary connections are when there is a rail in one of the two squares next to the one in the direction of movement of the appropriate type. If there is a primary connection, then secondary connections are ignored. If there is no primary connection and two secondary connections, then the train crashes. If there is no primary connection and just one secondary connection, then the secondary connection is used.

If there is a single secondary connection, it can be used. The train is moving southeast on a '\' tile, A '-' tile is one tile away from the direction of movement. This is therefore a valid transition:

$
 \-#

The other secondary connection is also valid:

$
 \
 |
 #

If there is a choice between a secondary and primary connection, the primary connection is preferred. In the below case, the train will continue southeast:

$
 \----#
  \
   #

However, if both of these secondary connections are available, there is no way for the interpreter to choose. This is an error:

$
 \-#
 |
 #

Below is an exhaustive list of potential primary and secondary connections. However, all of this follows the relatively intuitive rule that a train can turn 45 degrees as it moves from one square to another, but that it will keep going straight if possible.

Here are diagrams showing where rails may be placed to make connections on each of the tiles. 'P' is the primary connection location. The primary connection can be with any kind of rail that the source rail can connect with (see above). 'L' is the left secondary connection location. 'R' is the right secondary connection location. There is only one rail type that can be part of a secondary connection for the left and for the right locations. They are specified below each diagram.

 '|' North:
 
  LPR
   |
 
 
 L = '\'
 R = '/'
 '|' South:
 
 
   |
  RPL
 
 L = '\'
 R = '/'
 '-' East:
 
    L
   -P
    R
 
 L = '/'
 R = '\'
 '-' West:
 
  R
  P-
  L
 
 L = '/'
 R = '\'
 '/' Northeast:
 
   LP
   /R
 
 L = '|'
 R = '-'
 '/' Southwest:
 
 
  R/
  PL
 
 L = '|'
 R = '-'
 '\' Northwest:
 
  PR
  L\
 
 
 L = '-'
 R = '|'
 '\' Southeast:
 
 
   \L
   RP
 
 L = '-'
 R = '|'

'$' is the starting point of a function. It is the northwest corner of the playing field. The train starts there heading southeast.

'#', the end location (of which there can be more than one), must be in the primary connection spot of the rail leading the train to it.

'@' is a reflector. It reverses the direction of the train. In the following example, the train moves southeast from the '$', changes direction to the east at the right '-'. It reverses direction at the '@', and moves to the end, '#'.

$
 \
#--@

Junctions and builtins can be put at the primary connection location. Their effect on rail direction is noted below.

Simple junctions

The universal junction, '*', allows a train entering from any direction to leave in the opposite direction.

The HV junction, '+', acts like the universal junction, but only for the four cardinal directions (north, south, east, and west).

The diagonal junction, 'x', acts like the universal junction, but only for the secondary directions (northwest, southwest, northeast, and southeast).

Y-junctions and decisions

The Y-junctions are used for control path decisions. Each Y-junction must have three rails connected to it. The directions of those rails is determined by the Y-junction. Whenever the train visits the Y-junction, the stack is popped. If the value is true, the train turns right and if the value is false, the train turns left. If the stack is empty or the value at the top of the stack is neither true nor false, then the train crashes. Here is an example of a Y-junction:

\
 >-
/

If the train enters the Y-junction in the middle from the rail to the east, then right is the northwest rail and left is the southwest rail. Likewise if the train enters from the northwest, then right is the southwest rail and left is the east rail.

Here are the ways that each of the four Y-junctions must be connected:

\ /   |   \      /
 v    ^    >-  -<
 |   / \  /      \

A rail must be put at each 'leg' of the Y-junction in the middle, and one more sticking out in the opposite direction as the legs.

All characters not elsewhere defined (this includes whitespace!) are unpassable. These characters can be used as comments, but the actual train route must follow rails -- if a train runs out of rail, it will crash.

Multi-character commands

Constants, variable binding, variable use, and function invocation all require a multi-character command. Each of these commands has a particular pair of delimiters. Either delimiter may come first as long as it is paired with the opposite kind of delimiter. The characters inside the delimiters are read in the direction that the train is travelling. This means that moving in different directions along a rail can have completely different results. A programmer must use palindromic names/constants if this is to be avoided.

Constants

A constant is a multi-character command between '[' and ']'. Here is an example usage:

$ 'print-star'
 \
  \-[star]o-#

Everything between the '[' and ']' is pushed onto the stack. 'o' outputs it. It doesn't matter which bracket comes first as long as the other bracket comes after. So the above example is equivalent to:

$ 'print-star-equivalent'
 \
  \-]star[o-#

Also, the direction of the train determines the string. Therefore:

$ 'print-star-reverse'
 \
  \
#oo-[star]-@

Will print 'ratsstar'. Execution starts at the '$', heading southeast. The train turns east at the '-', then pushes 'star' onto the stack. It reaches '@', the reflector, and reverses direction heading west. It pushes 'rats' onto the stack. Then it prints out 'rats' then 'star'.

The '\' is used for quoting in a way similar to C (but palindromically). '\', '[', ']', newlines, and tabs must be quoted by '\\', '\[\', '\]\', '\n\', or '\t\', respectively. There is no distinction between string, numeric, and boolean data. A number is just a string that is all digits. A boolean is just a string that is either [0] or [1].

Variables

All variables are really references. Binding a variable or pushing it onto the stack merely copies the reference. This should be relatively transparent as all variables are also immutable and garbage collected. All variables are immutable because no built-in command changes a variable. All built-in commands operate on the variables on the stack.

Named variables are provided to make it convenient to manipulate and store values on the stack. First, a variable must be bound to a name. In the following example, the variable on top of the stack is popped and bound to the variable 'a'. The '(!' and '!)' are delimiters which signify that the variable is to be bound. This has the effect of popping the top element from the stack and discarding it, a 'drop' operation on the stack:

$ 'drop' (a -- )
 \
  \-(!a!)-#

Again, like constants, switching the parentheses around results in an equivalent program. The following is perfectly valid and has just the same effect as the previous program:

$ 'drop-equivalent' (a -- )
 \
  \-)!a!(-#

After a variable has been bound to a name, it can be used anywhere else in that function. Using a named variable pushes the value on top of the stack. Here is an example of this. First, the value at the top of the stack is popped and bound to the variable 'store'. Then that named variable is used twice in succession to push two copies onto the stack. The '(' and ')' delimiters signify that the variable should be used. This effectively duplicates the value on top of the stack, so we'll call it 'dup':

$ 'dup' (a -- a a)
 \
  \-(!store!)--(store)-(store)-#

New values can be bound to a name any number of times. This is equivalent to nested '(let ...)' declarations in LISP or Scheme. The scope of a bound name is the function. Variables can consist of any characters except '{', '}', '!', '(', ')', or a single quote '. In a future version, variables and functions will be merged into the same namespace.

Functions

A program file is a list of function declarations. There must be a function named 'main' in the program file (or in one of the files in a multi-file program). This is the entrypoint into the code. Each function declaration starts with a '$' symbol as the first character of a line. Somewhere on that same line, there must be the function name between single quotes. Below that line are the various rails and commands of the function. The train always starts the function at the '$', heading southeast. Here is the simplest function that completes without crashing:

$ 'simple-function'
 #

Each function definition defines a separate two-dimensional space. Surrounding the rails defined by the function is an infinite array of whitespace. Even if two functions are adjacent in the file, there is no way to lay rail between them. The only way to move between functions is through function invocation, described below. Note that the header line of the function is also part of the space defined by the function. This means that the following example will crash rather than end gracefully. After moving northeast through the ascending rails, the train will continue on and visit the 'e' command rather than turning. Then it will encounter the whitespace that surrounds every function and crash:

$ 'broken-function'
 \    /----#
  \--/

Functions are invoked using the '{' and '} delimiters around the name of the function. Here is a simple example program. The system invokes the 'main' function. After printing a message, 'main' invokes the function 'other-function', which prints a message as well:

$ 'main'
 \
  \-[Printing from main.\n\]o-{other-function}-#

$ 'other-function'
 \
  \-[Printing from other-function.\n\]o-#

Communication between functions is done through the data stack. When a function uses the stack, it should specify in a comment using Forth-style notation how the stack will change. Here is a program which uses function that prints the item that is second from the top of the stack:

$ 'main'
 \
  \-[bottom]-[middle]-[top]-{print-second}-#

$ 'print-second' (a b -- b), a gets printed.
 \
  \-(!b!)o(b)-#

Functions can call themselves recursively. Each function starts execution at the '$', and ends execution at one of the '#' cells. Function names have the same restrictions as variable names.

Lambdas

Lambdas are functions inside functions. They can use variables defined in the parent function.

$ 'main' (--):
 \
  \-[hello ](!h!)-[world\n\](!w!)-\
                                  |
   /------------------------------/
   |
   \
  /--&-(w)o-#
  |
  \-(!l!)-(h)o-(l){}#

& works like reflector and pushes lambda to the stack. When lambda is called, train is moved to the rail after the & character.

Built-in commands

Here is a template of how each command is laid out and what the parts mean.

'[letter]' [name] [stack-frame]
[description]
Pre: [condition]
Post: [condition]

[stack-frame] is ([stack] -- [stack]) where the first [stack] specifies how the stack looks before execution of the command and the second [stack] specifies how the top of the stack looks after execution. All of the elements in the first <stack> should be popped and all of the elements of the second stack should be pushed by the time the command ends. If there is underflow (not enough stack space to get all of the arguments), then the train crashes.

[stack] is a list of variable names. The right-most name represents the top of the stack, with the other names representing their respective positions on the stack.

[condition] is a C expression.

'~' is logical not.
'is_numeric(n)' checks to see whether n has only digits
'is_list(n)' checks to see whether n is a list
'is_nil(n)' checks to see whether n is NIL
'is_string(n)' checks to see whether n is a string
'is_bool(n)' checks to see whether n is a boolean value (equal to
  't' or 'f').
'size(n)' returns the length of n.
is_bool(n) ==> is_numeric(n) ==> is_string(n)
is_nil(n) ==> is_list(n)
is_list(n) <==> ~(is_string(n))

System

'b' Boom (a --)
Crash the program and print the string a.
Pre: is_string(a)
Post:
'e' EOF ( -- a)
Check for input. a is t if there is no more input. If there is more input, it is f.
Pre:
Post: is_bool(a)
'i' Input ( -- a)
Gets the next character from input as a size one string. If there is no more to read, then crash.
Pre:
Post: is_string(a)
'o' Output (a --)
Print the top value to output.
Pre: is_string(a)
Post:
'u' Underflow Check ( -- a)
Push the number of elements in the stack (before the push).
Pre:
Post: is_numeric(a)
'?' Type? (a -- b)
Returns 'string' or 'nil' or 'list' or 'lambda', depending on the type of the argument.
Pre:
Post: is_string(b)

Delimiters

'[' Left constant
If there is not a corresponding ']' in a straight line, then crash.
']' Right constant
'(' Left variable push
(value is read from the variable and pushed onto the stack). If there is no ')' in a straight line, crash.
')' Right variable push
'(!' Left variable pop
(value is popped off of the stack and written into the variable). If there is no '!)' in a straight line, crash. If the stack is empty, set the variable to an empty string.
'!)' Right variable pop
'{' Left function call
If there is not '}' in a straight line, crash.
'}' Right function call

Note that all of these should be direction-agnostic. It doesn't matter whether a left or right delimiter is encountered first. As long as there is a reverse one on the other side. Characters are read in the direction of travel, which means that a delimiter can refer to different things (unless the characters inside the delimiter are palindromic).

Math

'a' Add (a b -- c)
c = a+b
Pre: is_numeric(a) && is_numeric(b)
Post: is_numeric(c)
'd' Divide (a b -- c)
c = a/b
Pre: is_numeric(a) && is_numeric(b)
Post: is_numeric(c)
'm' Multiply (a b -- c)
c = a*b
Pre: is_numeric(a) && is_numeric(b)
Post: is_numeric(c)
'r' Remainder (a b -- c)
c = a%b
Pre: is_numeric(a) && is_numeric(b)
Post: is_numeric(c)
's' Subtract (a b -- c)
c = a-b
Pre: is_numeric(a) && is_numeric(b)
Post: is_numeric(c)
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9' Constant number (-- a)
Push the associated number onto the stack.
Pre:
Post: is_numeric(a)

String

'c' Cut (a b -- c d)
c is the substring of a at indices [0, b) and d is the substring of a at the indices [b, size(a)) where n is the length. if b is 0, then c is an empty string.
Pre: is_string(a) && is_numeric(b) && b >= 0 && b <= size(a)
Post: is_string(c) && is_string(d)
'p' Append (a b -- c)
c is a new string with b appended to the end of a. If a is 'abc' and b is 'def', then c is 'abcdef'.
Pre: is_string(a) && is_string(b)
Post: is_string(c)
'z' Size (a -- b)
b is the number of characters in a.
Pre: is_string(a)
Post: is_numeric(b)

List

'n' NIL (-- a)
Push the empty list onto the stack.
Pre:
Post: is_list(a) && is_nil(a)
':' Cons (a b -- c)
c is a list cell with a == cdr(c) and b == car(c)
Pre: is_list(a)
Post: is_list(c) && ~is_nil(c)
'~' List breakup (a -- b c)
b is cdr(a) and c is car(a)
Pre: is_list(a) && ~is_nil(a)
Post: is_list(b)

Conditional

'f' False (-- a)
Push 0 (f is represented as 0)
Pre:
Post: is_bool(a)
'g' Greater than (a b -- c)
c is the result of a>b
Pre: is_numeric(a) && is_numeric(b)
Post: is_bool(c)
'q' Is equal to (a b -- c)
If a and b are strings, then c is true iff the two strings are equal. If a and b are lists, then they are equal iff car(a) == car(b) && cdr(a) == cdr(b). The NIL list is always equal to itself. If a and b are of different types, then they are always unequal.
Pre:
Post: is_bool(c)
't' True (-- a)
Push 1 (t is represented as 1)
Pre:
Post: is_bool(a)

Misc

'@'
Reflector
'$'
Start
'#'
Finish
'&'
Lambda

Letters used

'a' -> Math
'b' -> System
'c' -> String
'd' -> Math
'e' -> System
'f' -> Conditional
'g' -> Conditional
'i' -> System
'm' -> Math
'n' -> List
'o' -> System
'p' -> String
'q' -> Conditional
'r' -> Math
's' -> Math
't' -> Conditional
'u' -> System
'v' -> Junction
'x' -> Junction
'z' -> String

More examples

Here are some familiar functions and an explanation of how they are implemented in Rail.

Swap the top two variables on the stack. Execution starts at the '$' in the northwest corner heading southeast. On line three, the rail turns east because the '-' is at the secondary connection point for '\'. Then it pops a variable off the stack and binds it to the name 'b'. A second variable is popped off and bound to the name 'a'. The variable bound to 'b' is then pushed back onto the stack. Finally, the variable bound to 'a' is pushed onto the stack as well, swapping the relative locations of those variables on the stack. The function then ends:

$ 'swap' (a b -- b a):
 \
  \-(!b!)-(!a!)-(b)-(a)-#


Logical not. The '<' is a y-junction. When it is encountered, it pops the value on top of the stack, and changes direction based on whether it is true or false. If that value is true, it picks the path to the right (true == right). If the value is false, it picks the path to the left. We enter the '<' going east. 'right' in this case is southeast. 'left' is northeast. If the value is true, then the path to the right is chosen, we move to the southeast. We go around the bottom of that middle loop, pushing false onto the stack when we encounter 'f'. Finally, we end the function by hitting the '#' on the northeast. If the original value is true, the train moves northeast, pushes true onto the stack when it passes the 't', and encounters the '@' at the bottom-right corner. That is the reflector which reverses the direction of the train, sending it north to the '#'.

$ '~' (a -- b):
 \
 |   /-t-\ #
 |  /     \|
 \-<       |
    \     /|
     \-f-/ @
$ '&&' (a b -- c):
 \
 |   /-{drop}-f-\
 |  /            \
 \-<   -f----------#
    \ /    /
     v    /
     |    |
     t    |
     |    |
     \----/
$ '||' (a b -- c):
 \
 |   /----\
 |   |    |
 |   f    |
 |   |    |
 |   ^    |
 |  / \   \
 \-<   -t----------#
    \            /
     \           |
      \-{drop}-t-/
$ '>' (a b -- c):
 \
  \-g-#
$ '<=' (a b -- c):
 \
  \-{>}-{~}-#
$ '<' (a b -- c):
 \
  \-(!b!)-(!a!)-(a)-(b)-q-{~}-(a)-(b)-{<=}-{&&}-#
$ '>=' (a b -- c):
 \
  \-{<}-{~}-#
$ '==' (a b -- c):
 \
  \-q-#
$ '~=' (a b -- c):
 \
  \-{==}-{~}-#

This function determines whether or not a one-character string is a digit or not. It uses many of the previous examples as subroutines. First it cuts the string into the first character and the rest, dropping the rest. If a multi-character string is given, this function only cares about the first one. Then it binds that character to the name 'digit'. It initializes 'counter' to 0, and 'flag' to f. Then it enters the main loop. It compares digit and counter and binds flag to the result orred with itself. Counter is bound to one greater than it previously was, and the loop ends when counter is no longer less than 10.

$ 'is-digit' (a -- c):
 \
  \-1c-{drop}-(!digit!)-0-(!counter!)-f-(!flag!)-t-\
                                                   |
                                                   |/------------\
                                                   |             |
                                                   ^             |
                                                  / \-(flag)-#   |
   /----------------------------------------------               |
   |                                                             |
   \-(digit)-(counter)-{==}-(flag)-{||}-(!flag!)-\               |
                                                 |               |
   /---------------------------------------------/               |
   |                                                             |
   \-(counter)-1a-(!counter!)---(counter)-[10]-{<}---------------/


This function uses 'is-digit' above to determine whether a string as a whole is a number. It also demonstrates the power of palindromic variable names. 'restser' is used on both left-to-right rails and right-to-left rails to mean the same variable. This is possible because it is a palindrome.

$ 'is-number' (a -- c):
 \                                            /-(flag)-#
  \-t-(!flag!)(!restser!)--(restser)-z-0-{>}-<
                         /                    \-(restser)-1c-------\
                        /                                          |
                        |  /-(firstsrif)-(!firstsrif!)-(!restser!)-/
                        |  |
                        |  \-{is-digit}-(flag)-{&&}-(!flag!)-\
                        |                                    |
                        \------------------------------------/

Something larger

Now lets look at something larger. Notable for both its size, and the fact it doesn't use any subroutines, only built in rail functions:

$'parse string' (s -- list ) parses a string representing a nested list to a list.  eg: the string: [a,b,c,[d,e,[f,g],h],i,j,k,l]
 \
  \--{inner parse string}(!r!)(!s!)--(r)--#
$'inner parse string' ( s -- rs list ) parses the string [a,b,[c,d],e,f] to the corresponding list. Note that nested lists work. rs is the string left over.
 \
 |                                   /------------------------------------------\
 |                                   |                                          |
 |                                   |                     /--(s)1c(!s!)(!c!)---/
 |                                   \           /--(s)z0q<
 |                /--(s)1c(!s!)(!c!)---(c)[\[\]q<          \--[]n--#
 \--(!s!)--(s)z0q<                               \
                  \--[]n--#                       \--n(!r!)--[](!ws!)---\
                                                                        |
 /----------------------------------------------------------------------/
 |
 |                                                                         /--(ws)(c)p(!ws!)------------\
 |          /--[](r)(ws):--#                             /---------(c)[,]q<                             |
 \---(s)z0g<                               /---(c)[\]\]q<                  \--(r)(ws):(!r!)--[](!ws!)---\
   /        \--(s)1c(!s!)(!c!)---(c)[\[\]q<              \--(s)(r)(ws):--#                              |
   |                                       \                                                            |
   |                                        \--(c)(s)p(!s!)--(s){inner parse string}(!ws!)(!s!)---------\
   |                                                                                                    |
   \----------------------------------------------------------------------------------------------------/

An example use of parse string:

$'main'
 \
  \---[some-junk\[\a,b,c,\[\d,e,f,g,h\]\,i,j,k,l\]\more-junk](!str!)(str)--(str){parse string}{show stack}o--#

Outputs:

stack
pos:  val
---------
1:   [a,b,c,[d,e,f,g,h],i,j,k,l].
0:   some-junk[a,b,c,[d,e,f,g,h],i,j,k,l]more-junk.
---------

Once more for luck! Here is a version of inner-parse-string that can handle slightly badly formed string representations of lists.

For example, ajunk[a,[b[c,d]e,f,g]h,i]zjunk parses to the list: [a,[b,[c,d],e,f,g],h,i] even though there is a missing comma between b and [, one between ] and e, and one between ] and h. The version above ignores the b and crashes with "Type mismatch" in the last two cases because the line: (ws)(c)p(!ws!) is trying to join a string on the end of a list. This version checks first with: (ws)?[list]q before deciding what to do.

$'inner parse string' ( s -- rs list ) parses the string [a,b,[c,d],e,f] to the corresponding list. Very tolerant of badly formatted strings version!
 \
 |                                   /------------------------------------------\
 |                                   |                                          |
 |                                   |                     /--(s)1c(!s!)(!c!)---/
 |                                   \           /--(s)z0q<
 |                /--(s)1c(!s!)(!c!)---(c)[\[\]q<          \--[]n--#
 \--(!s!)--(s)z0q<                               \
                  \--[]n--#                       \--n(!r!)--[](!ws!)---\
                                                                        |
 /----------------------------------------------------------------------/                   /--(ws)(c)p(!ws!)--------------------\
 |                                                                          /--(ws)?[list]q<                                     |
 |                                                                         /                \--(r)(ws):(!r!)--(c)(!ws!)----------\
 |          /--[](r)(ws):--#                             /---------(c)[,]q<                                                      |
 \---(s)z0g<                               /---(c)[\]\]q<                  \--(r)(ws):(!r!)--[](!ws!)----------------------------\
   /        \--(s)1c(!s!)(!c!)---(c)[\[\]q<              \--(s)(r)(ws):--#                                                       |
   |                                       \                                                                                     |
   |                                        \           /------------------/--(c)(s)p(!s!)--(s){inner parse string}(!ws!)(!s!)---\
   |                                         \--(ws)z0g<                   |                                                     |
   |                                                    \--(r)(ws):(!r!)---/                                                     |
   |                                                                                                                             |
   \-----------------------------------------------------------------------------------------------------------------------------/

External resources