/01 10\/11 20\/21 30\/31 40\/41 50\/51 60\/61 70\/71 80\
|--------||--------||--------||--------||--------||--------||--------||--------|
Programming Language Apocalypse #00014
Author: Clifford Wolf
Date: 2009-11-12
Yet another LISP dialect for numeric and CAS applications
*********************************************************
This document describes a LISP-like programming language for pocket calculators
and computer algebra systems.
The design goals have been:
* To overcome some of the imo less comforting aspects of LISP by adding some
syntacic suggar, such as the [..] notation from Scheme and reducing the
trailing ')' hells by introducing the '|' notation that allows for e.g.
"(not | is-nan | first x)" instead of "(not (is-nan (first x)))".
* To ease the implementation of the VM on a wide range of hardware platforms.
E.g. be leaving out the more complex VM features found in some of the 'big'
LISP dialects.
* To optimize the language for mixed numeric and CAS applications. E.g. by
handling undefined identifiers as self-referencing identifiers and so
minimizing the gap between an evaluateable term and an algebraic expression.
Main differences to existing LISP dialects such as Common LISP:
===============================================================
A more formal definition of the language can be found in the rest of this
document. but for starters I'd like to point out the main differences between
this language and existing LISP dialects:
* Undefined identifiers are handled as self-referencing identifiers.
Arithmetic expressions do not fail on such arguments but the term is
expanded as far as possible and instead of a concrete value a (possibly
simpler) term is returned (see example at the end of this document).
This should help closing the gap between numeric and algebraic
computations.
* In addition to NIL there are the values TRUE and FALSE for boolean values.
This is needed because the expression (< A (+ 3 5)) might evaluate to
something like (< A 8) which is different from NIL but nevertheless not
automatically true.
* There is no (let ...) statement. Instead there is a (local ...) statement
for declaring a block with local variables and within that statement
the functions (def ...) and (undef ...) can be used to define and undefine
local variables.
This has two advantages: First it allows for easy mixing of code and
variable definitions without the need to cascade many (let ...) statements
and second it eases reusing one algebraic expression with different
variables beeing defined and not defined without the need to change
the variable quoting within the algebraic expression.
* The only difference between a list and a function is that the function also
has an assoziation with a parent context for the lexical variable lookup.
Arguments are passed using a special variable and may be transfered to local
variables using the (args 'arg1 'arg2 ...) command. The argument list is not
part of the formal function declaration.
It is even possible to use a simple list like a function, in which case the
list is evaluated as function with a dynamic scope.
This also means that there is only one common namespace for functions and for
data variables.
* In addition to lists and atoms this language also has a primitive type
for low-level (hash based) dictionaries. They are needed for the containers
for local variables anyways and by making this feature accessable from the
language we also get support for plain dictionaries but also for namespaces.
* This LISP dialect is case sensitive and all unicode characters are allowed
as identifiers. This allows e.g. the use of greek letters or combining
characters in mathematical expressions.
Extensions to basic S-Expressions:
==================================
[...] may be used instead of (...)
(A | B) is a shortcut for (A (B))
'xyz is a shortcut for (quote xyz)
#n(...) is a shortcut for (function '(...))
Variable scoping and resolution:
================================
- Normal variables are using lexical scoping
- Variables starting with '$' are lisp-like special variables
- Identifiers starting with ':' are always self-referencing
- Unkown identifiers are self-referencing by default
Primitive functions:
====================
(def identifier value)
defines and sets a variable in the most inner (local ...) block
(undef identifier)
removes a variable definition from the most inner (local ...) block.
(set identifier value)
sets a previously defined variable to a new value
produce a runtime error if the variable is not defined
(args argnames...)
define all N argnames and assign the N first elements from $$ to this
variables. $$ itself is reset to point to the remaining elements of $$.
(local expressions...)
container for local variables, evaluates all expressions
and returns the value of the last one
(namespace expressions...)
like (local ...) but returns the dictionary holding the local variables.
(group expressions...)
evaluates all expressions and returns the value of the last one
(loop expressions...)
evaluates all expressions in an endless loop
(block label expressions...)
evaluates all expressions and returns the value of the last one
(return label value)
terminate the specified block and return the value
the label is optional - when it is ommited the most
inner while loop is terminated
(first value), (rest value), (nth n value)
lisp-like list access primitives
unlike in lisp, nth starts counting with 1
(cons first rest), (quote value), (list values...)
lisp-like list generation primitives
(if cond then else)
if cond is equal TRUE, "then" is evaluated, if it is equal
to FALSE, "else" is evaluated. If cond is neighter TRUE nor
FALSE a runtime error is produced. The "else" branch is optional.
(function list)
Convert the given list to a function with the current local block
as its parent context.
(eval expr)
Evaluate expression. (actually: evaluate it twice.. - otherwise
this would be equivialent to a [group ...] statement)
(parse string)
Parse a string and return the s-expression, or return NIL on
parser errors.
(read prompt)
Display the prompt and read a line from input.
(print expr)
Print out the value of the expression.
Special variables:
==================
Special variables (variables starting with a '$') are global variables.
They can be set as any other normal variable using (set ...) and
(def ...), but when (def ...) is used they are automatically reset to
their previous value when the current local block is left or they
are undefined using the (undef ...) statement.
Functions and evaluation algorithm:
===================================
Evaluation of an expression E is done using the following algorithm:
if E is a list or function:
FUNC = evaluation of first list element of E
if FUNC is a list or function:
- push current value of '$$' on the stack
- create new '$$' list variable
- evaluate all further elements of E and append results to '$$'
RET = evaluation of FUNC
- restore old value of '$$' by popping it from the stack
- result of evaluation is RET. end.
else:
- handle E as builtin function or generate runtime error. end.
else (E is not a list or function):
if E is an resolveable identifier:
- result of evaluation is the resolved identifier. end.
else:
- result of evaluation is the atom itself. end.
The only difference between functions and lists is that functions
have their own context pointers (closures) and simple lists don't.
Primitive data types:
=====================
Number types:
[+-]?[0-9]+
Integer with bigint support
{Integer}/{Integer}
Rationale number
[+-]?([0-9]+\.[0-9]*|[+-]?\.[0-9]+)
[+-]?([0-9]*\.)?[0-9]+e[+-]?[0-9]+
Floating point number
{Number}i
Imaginary number
Boolean types:
TRUE, FALSE
The two boolean values
Strings (utf8):
"[^"]+"
Some kind of yet not defined backslash escaping notation allows the
use of all unicode characters even in a 7bit ascii source file.
"""\n?(.*?)\n?"""
Multi-line-string. Different notation but same atom type.
Identifiers:
([UTF8]|[a-zA-Z!@$%^&*_=/?<>~])([UTF8]|[a-zA-Z!@#$%^&*_=/?<>~.0-9+-])*
I.e. almost everything else is an identifier
(The placeholder [UTF8] matches all chars that are not in 7bit ascii)
The backslash notation can be used to type untypeable characters.
Dots are special and indicate a dictionary lookup. This way it is
possible to implement namespaces.
Dictionaries:
{ KEY VALUE ... }
Lists:
( A . B )
Functions:
#( .. code .. )
When generating a function atom the context number is ignored
and the current local block is used parent context. Unless when
evaluating the function it behaves exactly like a list.
Nothing:
NIL
Used e.g. to terminate lists.
Functions for operating on primitive data types:
================================================
+, -, *, **, /, %
Arithmetic operations
When called with non-numerical arguments the term is
simplified as much as possible and then returned as result.
<, <=, ==, !=, >, >=
Relational operations (returns TRUE or FALSE)
When called with non-numerical arguments the term is
simplified as much as possible and then returned as result.
not, and, or, xor
Relational operations (returns TRUE or FALSE)
When called with non-boolean arguments the term is
simplified as much as possible and then returned as result.
(max values), (min values), (abs value), (floor value), (ceiling value),
(sin value), (cos value), (asin value), (acos value), ...
The usual math functions.
(str-cat string1 string2 ...)
Concatenate strings
(str-slice string1 first num)
Returns a "num" chars long segment of the string starting with the char
number "first". The first char has the index 1.
(str-length string)
Returns the number of (utf8) chars in the string.
(str-chr number ...)
Convert unicode character codes to a string.
(str-ord string)
Convert a string to a list of integers containing the
unicode character codes.
(is-number value)
(is-complex value)
(is-boolean value)
(is-string value)
(is-id value)
(is-nil value)
(is-atom value)
(is-dict value)
(is-list value)
(is-function value)
Returns TRUE or FALSE depending on the type of the argument.
(An atom is everything that is not a dict, list or function)
(is-defined identifier)
Returns TRUE if the identifier is defined and FALSE otherwise.
(eq value1 value2)
The ultimate test function as known from lisp
(might not do what you'd expect on lists and dictionaries.)
(dict-keys dict)
(dict-has dict key)
(dict-del dict key)
(dict-set dict key value)
Manage dictionaries
Examples:
=========
a function that generates accumulators. i.e. a function that
takes a number n, and returns a function that takes another
number i and returns n incremented by i.
(def 'foo #|local
(args 'n)
#[set 'n (+ n (first $$))]
)
a function that calculates the biggest prime factor of its argument.
(def 'mpf #|local
(args 'n)
(def 'x [floor (** n 1/2)])
(loop
(if (= x 1) [return n])
(if (= [% n x] 0) [return (max (mpf x) (mpf (/ n x)))])
(set 'x (- x 1))
)
)
two functions in a namespace:
(def 'my-name-space |namespace
(def 'foo NIL)
(def 'set-foo #|set 'foo |first $$)
(def 'print-foo #|print foo)
)
(my-name-space.set-foo "Hello World!")
(my-name-space.print-foo)
interactive example using hypothetical 'alg-parse' and 'alg-isolate'
algebra functions.
=> (is-defined 'a)
%1: FALSE
=> (alg-parse "a + 100 = 15 * 20")
%2: (= (+ a 100) (* 15 20))
=> (eval %2)
%3: (= (+ a 100) 300)
=> (alg-isolate %3 a)
%4: (- 300 100)
=> (eval %4)
%5: 200
=> (def 'a %5)
%6: 200
=> (eval %2)
%7: TRUE