audacia/nyquist/sal.lsp

631 lines
23 KiB
Common Lisp

;;; **********************************************************************
;;; Copyright (C) 2006 Rick Taube
;;; This program is free software; you can redistribute it and/or
;;; modify it under the terms of the Lisp Lesser Gnu Public License.
;;; See http://www.cliki.net/LLGPL for the text of this agreement.
;;; **********************************************************************
;;; $Revision: 1.2 $
;;; $Date: 2009-03-05 17:42:25 $
;; DATA STRUCTURES AND ALGORITHMS (for sal.lsp and parse.lsp)
;;
;; TOKENIZE converts source language (a string) into a list of tokens
;; each token is represented as follows:
;; (:TOKEN <type> <string> <start> <info> <lisp>)
;; where <type> is one of:
;; :id -- an identifier
;; :lp -- left paren
;; :rp -- right paren
;; :+, etc. -- operators
;; :int -- an integer
;; :float -- a float
;; :print, etc. -- a reserved word
;; <string> is the source string for the token
;; <start> is the column of the string
;; <info> and <lisp> are ??
;; Tokenize uses a list of reserved words extracted from terminals in
;; the grammar. Each reserved word has an associated token type, but
;; all other identifiers are simply of type :ID.
;;
;; *** WHY REWRITE THE ORIGINAL PARSER? ***
;; Originally, the code interpreted a grammar using a recursive pattern
;; matcher, but XLISP does not have a huge stack and there were
;; stack overflow problems because even relatively small expressions
;; went through a very deep nesting of productions. E.g.
;; "print note(between(30,odds(.5, 60, 90)))" 0 t nil))" was at recursion
;; level 46 when the stack overflowed. The stack depth is 2000 or 4000,
;; but all locals and parameters get pushed here, so since PARSE is the
;; recursive function and it has lots of parameters and locals, it appears
;; to use 80 elements in the stack per call.
;; *** END ***
;;
;; The grammar for the recursive descent parser:
;; note: [ <x> ] means optional <x>, <x>* means 0 or more of <x>
;;
;; <number> = <int> | <float>
;; <atom> = <int> | <float> | <id> | <bool>
;; <list> = { <elt>* }
;; <elt> = <atom> | <list> | <string>
;; <aref> = <id> <lb> <pargs> <rb>
;; <ifexpr> = ? "(" <sexpr> , <sexpr> [ , <sexpr> ] ")"
;; <funcall> = <id> <funargs>
;; <funargs> = "(" [ <args> ] ")"
;; <args> = <arg> [ , <arg> ]*
;; <arg> = <sexpr> | <key> <sexpr>
;; <op> = + | - | "*" | / | % | ^ | = | != |
;; "<" | ">" | "<=" | ">=" | ~= | ! | & | "|"
;; <mexpr> = <term> [ <op> <term> ]*
;; <term> = <-> <term> | <!> <term> | "(" <mexpr> ")" |
;; <ifexpr> | <funcall> | <aref> | <atom> | <list> | <string>
;; <sexpr> = <mexpr> | <object> | class
;; <top> = <command> | <block> | <conditional> | <assignment> | <loop> | <exec>
;; <exec> = exec <sexpr>
;; <command> = <define-cmd> | <file-cmd> | <output>
;; <define-cmd> = define <declaration>
;; <declaration> = <vardecl> | <fundecl>
;; <vardecl> = variable <bindings>
;; <bindings> = <bind> [ , <bind> ]*
;; <bind> = <id> [ <=> <sexpr> ]
;; <fundecl> = <function> <id> "(" [ <parms> ] ")" <statement>
;; <parms> = <parm> [ , <parm> ]*
;; this is new: key: expression for keyword parameter
;; <parm> = <id> | <key> [ <sexpr> ]
;; <statement> = <block> | <conditional> | <assignment> |
;; <output-stmt> <loop-stmt> <return-from> | <exec>
;; <block> = begin [ with <bindings> [ <statement> ]* end
;; <conditional> = if <sexpr> then [ <statement> ] [ else <statement> ] |
;; when <sexpr> <statement> | unless <sexpr> <statement>
;; <assignment> = set <assign> [ , <assign> ]*
;; <assign> = ( <aref> | <id> ) <assigner> <sexpr>
;; <assigner> = = | += | *= | &= | @= | ^= | "<=" | ">="
;; <file-cmd> = <load-cmd> | chdir <pathref> |
;; system <pathref> | play <sexpr>
;; (note: system was removed)
;; <load-cmd> = load <pathref> [ , <key> <sexpr> ]*
;; <pathref> = <string> | <id>
;; <output-stmt> = print <sexpr> [ , <sexpr> ]* |
;; output <sexpr>
;; <loop-stmt> = loop [ with <bindings> ] [ <stepping> ]*
;; [ <termination> ]* [ <statement> ]+
;; [ finally <statement> ] end
;; <stepping> = repeat <sexpr> |
;; for <id> = <sexpr> [ then <sexpr> ] |
;; for <id> in <sexpr> |
;; for <id> over <sexpr> [ by <sexpr> ] |
;; for <id> [ from <sexpr> ]
;; [ ( below | to | above | downto ) <sexpr> ]
;; [ by <sexpr> ] |
;; <termination> = while <sexpr> | until <sexpr>
;; <return-from> = return <sexpr>
;(in-package cm)
; (progn (cd "/Lisp/sal/") (load "parse.lisp") (load "sal.lisp"))
(setfn defconstant setf)
(setfn defparameter setf)
(setfn defmethod defun)
(setfn defvar setf)
(setfn values list)
(if (not (boundp '*sal-secondary-prompt*))
(setf *sal-secondary-prompt* t))
(if (not (boundp '*sal-xlispbreak*))
(setf *sal-xlispbreak* nil))
(defun sal-trace-enter (fn &optional argvals argnames)
(push (list fn *sal-line* argvals argnames) *sal-call-stack*))
(defun sal-trace-exit ()
(setf *sal-line* (second (car *sal-call-stack*)))
(pop *sal-call-stack*))
;; SAL-RETURN-FROM is generated by Sal compiler and
;; performs a return as well as a sal-trace-exit()
;;
(defmacro sal-return-from (fn val)
`(prog ((sal:return-value ,val))
(setf *sal-line* (second (car *sal-call-stack*)))
(pop *sal-call-stack*)
(return-from ,fn sal:return-value)))
(setf *sal-traceback* t)
(defun sal-traceback (&optional (file t)
&aux comma name names line)
(format file "Call traceback:~%")
(setf line *sal-line*)
(dolist (frame *sal-call-stack*)
(setf comma "")
(format file " ~A" (car frame))
(cond ((symbolp (car frame))
(format file "(")
(setf names (cadddr frame))
(dolist (arg (caddr frame))
(setf name (car names))
(format file "~A~% ~A = ~A" comma name arg)
(setf names (cdr names))
(setf comma ","))
(format file ") at line ~A~%" line)
(setf line (second frame)))
(t
(format file "~%")))))
'(defmacro defgrammer (sym rules &rest args)
`(defparameter ,sym
(make-grammer :rules ',rules ,@args)))
'(defun make-grammer (&key rules literals)
(let ((g (list 'a-grammer rules literals)))
(grammer-initialize g)
g))
'(defmethod grammer-initialize (obj)
(let (xlist)
;; each literal is (:name "name")
(cond ((grammer-literals obj)
(dolist (x (grammer-literals obj))
(cond ((consp x)
(push x xlist))
(t
(push (list (string->keyword (string-upcase (string x)))
(string-downcase (string x)))
xlist)))))
(t
(dolist (x (grammer-rules obj))
(cond ((terminal-rule? x)
(push (list (car x)
(string-downcase (subseq (string (car x)) 1)))
xlist))))))
(set-grammer-literals obj (reverse xlist))))
'(setfn grammer-rules cadr)
'(setfn grammer-literals caddr)
'(defun set-grammer-literals (obj val)
(setf (car (cddr obj)) val))
'(defun is-grammer (obj) (and (consp obj) (eq (car obj) 'a-grammer)))
(defun string->keyword (str)
(intern (strcat ":" (string-upcase str))))
(defun terminal-rule? (rule)
(or (null (cdr rule)) (not (cadr rule))))
(load "sal-parse.lsp" :verbose nil)
(defparameter *sal-print-list* t)
(defun sal-printer (x &key (stream *standard-output*) (add-space t)
(in-list nil))
(let ((*print-case* ':downcase))
(cond ((and (consp x) *sal-print-list*)
(write-char #\{ stream)
(do ((items x (cdr items)))
((null items))
(sal-printer (car items) :stream stream
:add-space (cdr items) :in-list t)
(cond ((cdr items)
(cond ((not (consp (cdr items)))
(princ "<list not well-formed> " stream)
(sal-printer (cdr items) :stream stream :add-space nil)
(setf items nil))))))
(write-char #\} stream))
((not x) (princ "#f" stream) )
((eq x t) (princ "#t" stream))
(in-list (prin1 x stream))
(t (princ x stream)))
(if add-space (write-char #\space stream))))
(defparameter *sal-printer* #'sal-printer)
(defun sal-message (string &rest args)
(format t "~&; ")
(apply #'format t string args))
;; sal-print has been modified from the original SAL to print items separated
;; by spaces (no final trailing space) and followed by a newline.
(defun sal-print (&rest args)
(do ((items args (cdr items)))
((null items))
;; add space unless we are at the last element
(funcall *sal-printer* (car items) :add-space (cdr items)))
(terpri)
(values))
(defmacro keyword (sym)
`(str-to-keyword (symbol-name ',sym)))
(defun plus (&rest nums)
(apply #'+ nums))
(defun minus (num &rest nums)
(apply #'- num nums))
(defun times (&rest nums)
(apply #'* nums))
(defun divide (num &rest nums)
(apply #'/ num nums))
;; implementation of infix "!=" operator
(defun not-eql (x y)
(not (eql x y)))
; dir "*.*
; chdir
; load "rts.sys"
(defun sal-chdir ( dir)
(cd (expand-path-name dir))
(sal-message "Directory: ~A" (pwd))
(values))
;;; sigh, not all lisps support ~/ directory components.
(defun expand-path-name (path &optional absolute?)
(let ((dir (pathname-directory path)))
(flet ((curdir ()
(truename
(make-pathname :directory
(pathname-directory
*default-pathname-defaults*)))))
(cond ((null dir)
(if (equal path "~")
(namestring (user-homedir-pathname))
(if absolute?
(namestring (merge-pathnames path (curdir)))
(namestring path))))
((eql (car dir) ':absolute)
(namestring path))
(t
(let* ((tok (second dir))
(len (length tok)))
(if (char= (char tok 0) #\~)
(let ((uhd (pathname-directory (user-homedir-pathname))))
(if (= len 1)
(namestring
(make-pathname :directory (append uhd (cddr dir))
:defaults path))
(namestring
(make-pathname :directory
(append (butlast uhd)
(list (subseq tok 1))
(cddr dir))
:defaults path))))
(if absolute?
(namestring (merge-pathnames path (curdir)))
(namestring path)))))))))
(defun sal-load (filename &key (verbose t) print)
(progv '(*sal-input-file-name*) (list filename)
(prog (file extended-name)
;; first try to load exact name
(cond ((setf file (open filename))
(close file) ;; found it: close it and load it
(return (generic-loader filename verbose print))))
;; try to load name with ".sal" or ".lsp"
(cond ((string-search "." filename) ; already has extension
nil) ; don't try to add another extension
((setf file (open (strcat filename ".sal")))
(close file)
(return (sal-loader (strcat filename ".sal")
:verbose verbose :print print)))
((setf file (open (strcat filename ".lsp")))
(close file)
(return (lisp-loader filename :verbose verbose :print print))))
;; search for file as is or with ".lsp" on path
(setf fullpath (find-in-xlisp-path filename))
(cond ((and (not fullpath) ; search for file.sal on path
(not (string-search "." filename))) ; no extension yet
(setf fullpath (find-in-xlisp-path (strcat filename ".sal")))))
(cond ((null fullpath)
(format t "sal-load: could not find ~A~%" filename))
(t
(return (generic-loader fullpath verbose print)))))))
;; GENERIC-LOADER -- load a sal or lsp file based on extension
;;
;; assumes that file exists, and if no .sal extension, type is Lisp
;;
(defun generic-loader (fullpath verbose print)
(cond ((has-extension fullpath ".sal")
(sal-loader fullpath :verbose verbose :print print))
(t
(lisp-loader fullpath :verbose verbose :print print))))
#|
(defun sal-load (filename &key (verbose t) print)
(progv '(*sal-input-file-name*) (list filename)
(let (file extended-name)
(cond ((has-extension filename ".sal")
(sal-loader filename :verbose verbose :print print))
((has-extension filename ".lsp")
(lisp-load filename :verbose verbose :print print))
;; see if we can just open the exact filename and load it
((setf file (open filename))
(close file)
(lisp-load filename :verbose verbose :print print))
;; if not, then try loading file.sal and file.lsp
((setf file (open (setf *sal-input-file-name*
(strcat filename ".sal"))))
(close file)
(sal-loader *sal-input-file-name* :verbose verbose :print print))
((setf file (open (setf *sal-input-file-name*
(strcat filename ".lsp"))))
(close file)
(lisp-load *sal-input-file-name* :verbose verbose :print print))
(t
(format t "sal-load: could not find ~A~%" filename))))))
|#
(defun lisp-loader (filename &key (verbose t) print)
(if (load filename :verbose verbose :print print)
t ; be quiet if things work ok
(format t "error loading lisp file ~A~%" filename)))
(defun has-extension (filename ext)
(let ((loc (string-search ext filename
:start (max 0 (- (length filename)
(length ext))))))
(not (null loc)))) ; coerce to t or nil
(defmacro sal-at (s x) (list 'at x s))
(defmacro sal-at-abs (s x) (list 'at-abs x s))
(defmacro sal-stretch (s x) (list 'stretch x s))
(defmacro sal-stretch-abs (s x) (list 'stretch-abs x s))
;; splice every pair of lines
(defun strcat-pairs (lines)
(let (rslt)
(while lines
(push (strcat (car lines) (cadr lines)) rslt)
(setf lines (cddr lines)))
(reverse rslt)))
(defun strcat-list (lines)
;; like (apply 'strcat lines), but does not use a lot of stack
;; When there are too many lines, XLISP will overflow the stack
;; because args go on the stack.
(let (r)
(while (> (setf len (length lines)) 1)
(if (oddp len) (setf lines (cons "" lines)))
(setf lines (strcat-pairs lines)))
; if an empty list, return "", else list has one string: return it
(if (null lines) "" (car lines))))
(defun sal-loader (filename &key verbose print)
(let ((input "") (file (open filename)) line lines)
(cond (file
(push filename *loadingfiles*)
(while (setf line (read-line file))
(push line lines)
(push "\n" lines))
(close file)
(setf input (strcat-list (reverse lines)))
(sal-trace-enter (strcat "Loading " filename))
(sal-compile input t t filename)
(pop *loadingfiles*)
(sal-trace-exit))
(t
(format t "error loading SAL file ~A~%" filename)))))
; SYSTEM command is not implemented
;(defun sal-system (sys &rest pairs)
; (apply #'use-system sys pairs))
(defun load-sal-file (file)
(with-open-file (f file :direction :input)
(let ((input (make-array '(512) :element-type 'character
:fill-pointer 0 :adjustable t)))
(loop with flag
for char = (read-char f nil ':eof)
until (or flag (eql char ':eof))
do
(when (char= char #\;)
(loop do (setq char (read-char f nil :eof))
until (or (eql char :eof)
(char= char #\newline))))
(unless (eql char ':eof)
(vector-push-extend char input)))
(sal input :pattern :command-sequence))))
(defmacro sal-play (snd)
(if (stringp snd) `(play-file ,snd)
`(play ,snd)))
(if (not (boundp '*sal-compiler-debug*))
(setf *sal-compiler-debug* nil))
(defmacro sal-simrep (variable iterations body)
`(simrep (,variable ,iterations) ,body))
(defmacro sal-seqrep (variable iterations body)
`(seqrep (,variable ,iterations) ,body))
;; function called in sal programs to exit the sal read-compile-run-print loop
(defun sal-exit () (setf *sal-exit* t))
(setf *sal-call-stack* nil)
;; read-eval-print loop for sal commands
(defun sal ()
(progv '(*breakenable* *tracenable* *sal-exit* *sal-mode*)
(list *sal-break* *xlisp-traceback* nil t)
(let (input line)
(setf *sal-call-stack* nil)
(read-line) ; read the newline after the one the user
; typed to invoke this fn
(princ "Entering SAL mode ...\n");
(while (not *sal-exit*)
(princ "\nSAL> ")
(sal-trace-enter "SAL top-level command interpreter")
;; get input terminated by two returns
(setf input "")
(while (> (length (setf line (read-line))) 0)
(if *sal-secondary-prompt* (princ " ... "))
(setf input (strcat input "\n" line)))
;; input may have an extra return, remaining from previous read
;; if so, trim it because it affects line count in error messages
(if (and (> (length input) 0) (char= (char input 0) #\newline))
(setf input (subseq input 1)))
(sal-compile input t nil "<console>")
(sal-trace-exit))
(princ "Returning to Lisp ...\n")))
;; in case *xlisp-break* or *xlisp-traceback* was set from SAL, impose
;; them here
(cond ((not *sal-mode*)
(setf *breakenable* *xlisp-break*)
(setf *tracenable* *xlisp-traceback*)))
t)
(defun sal-error-output (stack)
(if *sal-traceback* (sal-traceback))
(setf *sal-call-stack* stack)) ;; clear the stack
;; when true, top-level return statement is legal and compiled into MAIN
(setf *audacity-top-level-return-flag* nil)
;; SAL-COMPILE-AUDACITY -- special treatment of RETURN
;;
;; This works like SAL-COMPILE, but if there is a top-level
;; return statement (not normally legal), it is compiled into
;; a function named MAIN. This is a shorthand for Audacity plug-ins
;;
(defun sal-compile-audacity (input eval-flag multiple-statements filename)
(progv '(*audacity-top-level-return-flag*) '(t)
(sal-compile input eval-flag multiple-statements filename)))
;; SAL-COMPILE -- translate string or token list to lisp and eval
;;
;; input is either a string or a token list
;; eval-flag tells whether to evaluate the program or return the lisp
;; multiple-statements tells whether the input can contain multiple
;; top-level units (e.g. from a file) or just one (from command line)
;; returns:
;; if eval-flag, then nothing is returned
;; otherwise, returns nil if an error is encountered
;; otherwise, returns a list (PROGN p1 p2 p3 ...) where pn are lisp
;; expressions
;;
;; Note: replaced local variables here with "local" names to avoid
;; collisions with globals that compiled code might try to use:
;; eval uses local bindings, not global ones
;;
(defun sal-compile (sal:input sal:evflag sal:mult-stmts sal:filename)
;; save some globals because eval could call back recursively
(progv '(*sal-tokens* *sal-input* *sal-input-text*) '(nil nil nil)
(let (sal:output sal:remainder sal:rslt sal:stack)
(setf sal:stack *sal-call-stack*)
;; if first input char is "(", then eval as a lisp expression:
;(display "sal-compile" sal:input)(setf *sal-compiler-debug* t)
(cond ((input-starts-with-open-paren sal:input)
;(print "sal:input is lisp expression")
(errset
(print (eval (read (make-string-input-stream sal:input)))) t))
(t ;; compile SAL expression(s):
(loop
(setf sal:output (sal-parse nil nil sal:input sal:mult-stmts
sal:filename))
(cond ((first sal:output) ; successful parse
(setf sal:remainder *sal-tokens*)
(setf sal:output (second sal:output))
(when *sal-compiler-debug*
(terpri)
(pprint sal:output))
(cond (sal:evflag ;; evaluate the compiled code
(cond ((null (errset (eval sal:output) t))
(sal-error-output sal:stack)
(return)))) ;; stop on error
(t
(push sal:output sal:rslt)))
;(display "sal-compile after eval"
; sal:remainder *sal-tokens*)
;; if there are statements left over, maybe compile again
(cond ((and sal:mult-stmts sal:remainder)
;; move sal:remainder to sal:input and iterate
(setf sal:input sal:remainder))
;; see if we've compiled everything
((and (not sal:evflag) (not sal:remainder))
(return (cons 'progn (reverse sal:rslt))))
;; if eval but no more sal:input, return
((not sal:remainder)
(return))))
(t ; error encountered
(return)))))))))
;; SAL just evaluates lisp expression if it starts with open-paren,
;; but sometimes reader reads previous newline(s), so here we
;; trim off initial newlines and check if first non-newline is open-paren
(defun input-starts-with-open-paren (input)
(let ((i 0))
(while (and (stringp input)
(> (length input) i)
(eq (char input i) #\newline))
(incf i))
(and (stringp input)
(> (length input) i)
(eq (char input i) #\())))
(defun sal-list-equal (a b)
(let ((rslt t)) ;; set to false if any element not equal
(dolist (x a)
(if (sal-equal x (car b))
t ;; continue comparing
(return (setf rslt nil))) ;; break out of loop
(setf b (cdr b)))
(and rslt (null b)))) ;; make sure no leftovers in b
(defun sal-plus(a b &optional (source "+ operation in SAL"))
(ny:typecheck (not (or (numberp a) (soundp a) (multichannel-soundp a)))
(ny:error source 0 number-sound-anon a t))
(ny:typecheck (not (or (numberp b) (soundp b) (multichannel-soundp b)))
(ny:error source 0 number-sound-anon b t))
(nyq:add2 a b))
(defun sal-equal (a b)
(or (and (numberp a) (numberp b) (= a b))
(and (consp a) (consp b) (sal-list-equal a b))
(equal a b)))
(defun not-sal-equal (a b)
(not (sal-equal a b)))
(defun sal-list-about-equal (a b)
(let ((rslt t)) ;; set to false if any element not equal
(dolist (x a)
(if (sal-about-equal x (car b))
t ;; continue comparing
(return (setf rslt nil))) ;; break out of loop
(setf b (cdr b)))
(and rslt (null b)))) ;; make sure no leftovers in b
(setf *~=tolerance* 0.000001)
(defun sal-about-equal (a b)
(or (and (numberp a) (numberp b) (< (abs (- a b)) *~=tolerance*))
(and (consp a) (consp b) (sal-list-about-equal a b))
(equal a b)))