MM: Second Iteration of JJ

mm is a tool for building programs from source code. It's actually a really simplistic scripting language with a few builtin functions which aid in building programs.

The language is postscript with a stack. You can also use global and local variables along with this stack. Basically, the stack is just there to pass arguments to functions, most of the time it's much better to use variables and not worry about what's going on the stack.

There are two data types: strings and lists of strings. It's not possible to make lists of lists or other complex data structures.

A program consists of words and operators. A word consists of any printable character that is not whitespace or a special character such as an operator. For instance,

  foo!m<e  3.455a-343 \dgt4 
are all valid words. The following are the operators:
  $    =   %   +    [    ]  
  {    }   @   # 
A comment starts with a question mark and lasts until the end of the line. A quoted string starts with a double quote and ends with another one. You may use the backslash character to escape encode a double quote inside, but no interpretation is done for escape sequences. When you write a quoted string such as:
  "escaping the \"quote\" is easy"
That string is exactly what's passed down the line, with the initial and final quote characters intact. This is mostly useful for passing arguments to shell commands.

Let's get on with the operators.

$
Pop a variable name from the stack, push the value of that variable on the stack.
=
Pop a variable name from the stack, pop a value from the stack, set the variable to that value.
%
Pop an escape sequence name from the stack, push the corresponding special string on the stack. Here are the escape sequence names:
NameString
pl +
pc %
ls [
rs ]
lc {
rc }
eq =
dl $
at @
ha #
qm ?
qu"
bs \
nl <Newline>
sp <Space>
tb <Tab>
+
Pop a value from the stack, pop a list from the stack, put the value at the end of the list and push the whole thing back. If value is a list, it's not put as an element, but simply concatenated. If the popped list is not really a list, it's made into one. For instance:
  foo bar+
  this list+
    +
The first two lines make two lists with two items in each. The last line combines them all into one list.
[ and ]
[ puts a mark on the stack. The processing goes on normally, when ] is encountered, it pops all elements until the [ mark, and then combines them all into one string, by placing space characters between each popped element. For instance:
  [this is a good way of making shell commands] 
results in a single string. This mechanism is the primary way of specifying shell commands because the insides of [] is still interpreted. You can make function calls, variable references etc. Everything still works until the ] character. For example:
  dodo name=
  [Hello, name$.] print#
Will work as expected.
{ and }
These are used to specify strings as well. However, the contents between these two are not interpreted at all, just passed as is. These pairs can also contain embedded {} pairs, given that they are all matched up properly. Such strings are typically used for conditional statements or loop bodies: code is just a string. Here is an example:
  foo.c foo.o  #isnewer
  { [it is newer] print# } if#
Here the string within {} is just passed as-is to the if function. It then decides to execute it or not depending on the condition value.
@
Pop a directory name from the stack, pop a list of file names from the stack. For each relative path in the list of file names, re-interpret them relative to the given directory. For instance:
   /home/dodo/lib/graphics/circle  dir=
   init.c main.c+ /tmp/x.c+ ../polygon/init.c+ dir@
The second line will produce the following list:
   /home/dodo/lib/graphics/circle/init.c 
   /home/dodo/lib/graphics/circle/main.c 
   /tmp/x.c
   /home/dodo/lib/graphics/polygon/init.c 
When given as an argument to the @ operator, the word here has a special meaning. It always refers to the directory of the file it appears in. For instance, if you have the following setup:
  current dir:   /home/dodo/lib/build/
  file name:      /home/dodo/lib/graphics/circle/jjcool
  file contents:
      init.c here@
The @ operator will push the following string:
   /home/dodo/lib/graphics/circle/init.c
#
Pop a function name from the stack and execute the corresponding function. This is discussed further below.

Functions

Functions live in a different namespace than variables. You can have a function and a variable with the same name, but that's not a really good idea.

A function is defined using the define builtin. A builtin function may not be redefined, but user functions may be redefined at the cost of a warning.

The define builtin pops a function body and a function name (in this order) and then makes the association. Here is an example function:

  swap  { a= b= a$ b$ } define#
The contents of the function body are not interpreted at all, until the point of execution.

The language has no concept of function arguments, but you can make such things by assigning arguments on the stack to variables. In the example above, the code starts with variable assignments. They will be more clear if written in this way:

  swap  { 
    a= 
    b=
    a$ b$ } define#
Here, the value at the top of the stack is assigned to variable a. The one below gets assigned to b. It's this easy to get function arguments.

Returning values is just as easy, just look at the end of swap. We get the value of a, but do nothing with it, so it stays on the stack. The same for b. So, we got two values from the stack and returned two values.

Variables

Each function has its own scope for variables. When you set a variable, it's created within the scope of the function doing so. Any function you call down the call stack will not see that variable, because they don't have access to scope of the calling function. You may, however, export the variable to the global scope using the export builtin.

In this context, all code appearing outside function definitions is assumed to be running in a top-level function. This top-level function is different for each included file. For instance, if you have:

file1:
  dodo name=
  greet { [hello name$] print# } define#
  greet#
  file2 include#
file2:
  foo name=
  greet#
In both calls to greet, it will print "hello ". This is because the definitions for name are not visible inside the function. In addition, the two definitions of name refer to different variables. The first one creates a variable in the top-function-scope of file1, whereas the second one does so in the top-function-scope of file2.

This setup makes it easy to reuse variable names without fear of clashing with included files. If you want to have some global variables to be used across files, just use the export builtin.

There is a special variable called empty. The value of this variable is always an empty list. You can use it as an initalizer for list valued variables.

Conditional Execution

The builtin functions if and else may be used to do this. The if builtin pops a code section from the stack, pops a condition from the stack, executes the code if the condition was true, and then pushes the condition back on the stack. The else builtin does the same thing, but executes the code only if the condition is not true. These two builtins are completely independent of each other and can be used in any order necessary. After you're done with a condition, remember to remove it from the stack using the drop builtin. Here is an example use:
 foo.c foo.o isnewer#
  { [it is newer] print# }  if#
  { [it isn't newer] print# } else#drop#
Here, the isnewer builtin computes a value based on the given file names. A value is considered true if:

Note that code sections given to if and else can also manipulate the stack. For instance:

  main.c 
   target$ linux eq# { linux.c } if# { windows.c } else#drop# +
is an example where these builtins are used in a similar way to the C ternary operator.

Builtin Functions

  value print#
Pops and prints the TOS. If quiet mode is on, it doesn't print anything.
  dump#
Prints a stack dump. Useful for debugging.
  value drop#
Pops the value and does nothing with it.
  string run#
Runs the given string as a shell command. The program exits with failure if this shell command fails.
  string orun#
Runs the given string as a shell command. Collects the standard output of the command as a string and pushes that. After that, it pushes either False or True, depending on the exit status of the command.
  source target isnewer#
If any file in source is newer than any file in target, push True. Otherwise push False. Original arguments are popped.
  value search replacement rep.str#
Replace the first occurence of search in value with replacement. If value is a list, then do this for each element of the list.
  value suffix rep.sfx#
Replace the suffix of value with suffix. A suffix is a filename extension such as .c or .class etc.
  value1 value2 cat#
Concatenate two strings and push the resulting string back on. If any of the values is a list, then all elements of the list is concatenated with the the other value. If both values are lists, this is an error.
  varname export#
Exports the given variable to the global scope.
  varname getenv#
Pushes the value of the given environment variable.
  filename include#
Executes the given input file. If it's a relative path, it's taken relative to the directory of the file the include call resides in.
  string1 string2 eq#
Pushes True if the two strings are equal. Pushes False otherwise.
  string1 string2 neq#
Pushes True if the two strings are not equal. Pushes False otherwise.
  list code for.each#
Runs code for each element of list. For each element, the element is pushed on the stack and then code is executed.
  code count times#
Execute code for count times. count should be string and is interpreted as a decimal number.

Command Line Arguments

  -V                      print version and exit
  -f input_file_name      can be given multiple times
  -q                      quiet mode, doesn't even print messages
  -v                      verbose, prints commands before executing them
  var=value               predefine variable
  var                     predefine variable=True  
If no input file name is given, jjcool in the current directory is assumed.

Download

Here is the source code. Just compile it and put it somewhere in your $PATH.