diff --git a/README.md b/README.md index f015714..a1f5681 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # spacelang -A concatenative, stack based language with async capabilities. +A (yet another) concatenative, stack based language (but) with async +capabilities. ## Status diff --git a/example/add_2.sp b/example/add_2.sp new file mode 100644 index 0000000..9cb1429 --- /dev/null +++ b/example/add_2.sp @@ -0,0 +1 @@ +slurp slurp + . diff --git a/example/presentation.sp b/example/presentation.sp new file mode 100644 index 0000000..b90830f --- /dev/null +++ b/example/presentation.sp @@ -0,0 +1,180 @@ +[" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + " ,] [newPage] ^ +[(slurp :rs newPage ! )] [waitForCharacter] ^ + +[:s] [results] ^ + +[ print waitForCharacter ! ] [printThenWait] ^ +"Hello and welcome to TechDive presentation of spacelang" , +waitForCharacter ! + +"What is spacelang you ask?" , +waitForCharacter ! + +"It is a stack based programming language (akin to Forth), that: " , waitForCharacter ! +"Pause for dramatic effect." , waitForCharacter ! +"- is simpler (?)," , waitForCharacter ! +"- proposes a novel async model (as far as stack languages go)," , waitForCharacter ! +"- leverages lazy evaluation," , waitForCharacter ! +"- has some interesting features," , waitForCharacter ! +"- meta-programming is built-in and part of the normal functioning of the language." , waitForCharacter ! + +"We have a stack to which we can push Terms: +We simply push them by writing them down:" , + +"> 1 2 " . waitForCharacter ! + +"In this example the interpreter reads '1' pushes it to the stack, then reads +'2' and pushes it to the stack resulting in:" . waitForCharacter ! +1 2 results ! waitForCharacter ! + +"The evaluation model of the language is a key aspect of what makes it special. +Left to right, sequential evaluation ensures that our programs are confluent, +even with the addition of effects." . + +waitForCharacter ! + +"Our terms can be numbers, strings, booleans, words, or thunks." , +"> 1 'strings' true + [thunk]" , +waitForCharacter ! + +"Words, and terms in spacelang are from a type perspective arrows, which means +that they compose. Let's use the following notation: + - '(input -- output)' + - capitalised for defined types + - uncapitalised for variables + + Therefore we have +1 : ( -- Number) ++ : ( Number Number -- Number) + etc. + + This 'arrow' property of terms means that any term is a function, and it also + gives us the concatenative property of the language. In essence a sequence of + two terms is a composition." , waitForCharacter ! + +"As the TechDive is a bit short, to cover 'Principia Mathematica', we'll accept +that numbers are primitives that evaluate to themselves. As seen before. So are strings. + +From a type perspective we say +'a' : ( -- String) +1 : ( -- Number) + " , +waitForCharacter ! + +"The first notable exception are words. Words evaluate to their binding (whatever +that is). The language provides some built in words: + - numerical operations: + - / * < <= etc. + - machine operations: :h :r :debug :m : rs etc. + - memory operations: ^ + - evaluator operations: ! + - inter-machine operations: $", +waitForCharacter ! + +"But the funkiest one is the thunk which we write by enclosing a term within +square brakets, like so: + > [1 3 +] + + Which has the type: + [1 3 +] : ( -- ( -- Number)) + + So what's going on here? + ", waitForCharacter ! + +"We force evaluation of a thunk with the operator '!', which behaviour we can +describe with the term + + ((a) -- a) +", +waitForCharacter ! + +"We can bind terms to words with the word `^`, together with thunks we can now +define our own funky words. + + > [1 +] [addOne] " , +waitForCharacter ! + +"Word bindings have a notion of scope, therefore if we do the following: + + > ( [1 +] [addOne] 2 addOne ) 4 addOne + +The program will crash and burn as addOne is not defined/bound outside the scope +defined by the parens. + +As the parens themselves are terms - we can use scope local to our thunks as so + > [ ([x] ^) ] + A term which would pop x and discard it essentially. + " , waitForCharacter ! + +"Last thing to mention is that spacelang is more or less a language to + orchestrate a machine. Imagine many such machines each with their own address. + Here is where $ operator comes into play. + This is the async model of space. + " , waitForCharacter ! + +"Enough talking, let's find bugs!" , waitForCharacter ! + +"The End." , waitForCharacter ! +"*Pause for aplause.*" , waitForCharacter ! + +"Any questions?" , diff --git a/flake.nix b/flake.nix index f7118f6..2b386f7 100644 --- a/flake.nix +++ b/flake.nix @@ -9,6 +9,7 @@ pkgs = import nixpkgs { inherit system; }; + ncurses = pkgs.ncurses; compiler = pkgs.stdenv.mkDerivation { name = "compiler"; @@ -17,13 +18,16 @@ sbcl gnumake lispPackages.quicklisp + ncurses ]; + + shellHook = '' + export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${ncurses}/lib; + ''; }; in { defaultPackage = compiler; - - } ); } diff --git a/makefile b/makefile index 05ab3e9..258ef98 100644 --- a/makefile +++ b/makefile @@ -6,7 +6,13 @@ bin/spci: bin/ $(lisp_files) $(asd_files) --eval '(ql:quickload "spacelang")' \ --eval '(asdf:load-system "spacelang")' \ --eval '(use-package :spacelang)' \ - --eval "(sb-ext:save-lisp-and-die #p\"bin/spci\" :toplevel #'space! :executable t)" + --eval "(sb-ext:save-lisp-and-die #p\"bin/spci\" :toplevel #'space! :executable t)" bin/: mkdir bin + +.PHONY: DockerRun + +DockerRun: + docker build . -t spacelang + docker run -it spacelang diff --git a/parser.lisp b/parser.lisp index 10f2510..92407e0 100644 --- a/parser.lisp +++ b/parser.lisp @@ -139,10 +139,10 @@ (.read (.number)) (.read (.slurp)) (.read (.word)) - (.read (.opp)) (.read (.dict-up)) (.read (.dict-down)) (.read (.cons)) + (.read (.opp)) (.read (.send)) (.read (.bind-term)) (.read (.eval-term)) diff --git a/spacelang.lisp b/spacelang.lisp index 6996c14..9109e81 100644 --- a/spacelang.lisp +++ b/spacelang.lisp @@ -84,8 +84,7 @@ (cond ((consp term) (format nil "[~s]" (apply #'str:concat (loop :for x :in term :collect (pretty-term x))))) - - (t (format nil "~a"term)))) + (t (format nil "~a" term)))) (defun print-stack! (memory) "Prints the current stack." @@ -256,25 +255,32 @@ true 0 if false." "Pops two terms, subtracts them, evaluates the result." (f2 memory #'-)) -(defmethod evaluate ((memory space-memory) (term (eql :/))) +(defmethod evaluate ((memory space-memory) + (term (eql :/))) (f2 memory #'/)) -(defmethod evaluate ((memory space-memory) (term (eql :<))) +(defmethod evaluate ((memory space-memory) + (term (eql :<))) (f2p memory #'<)) -(defmethod evaluate ((memory space-memory) (term (eql :>))) +(defmethod evaluate ((memory space-memory) + (term (eql :>))) (f2p memory #'>)) -(defmethod evaluate ((memory space-memory) (term (eql :<=))) +(defmethod evaluate ((memory space-memory) + (term (eql :<=))) (f2p memory #'<=)) -(defmethod evaluate ((memory space-memory) (term (eql :>=))) +(defmethod evaluate ((memory space-memory) + (term (eql :>=))) (f2p memory #'>=)) -(defmethod evaluate ((memory space-memory) (term (eql :=))) +(defmethod evaluate ((memory space-memory) + (term (eql :=))) (f2p memory #'eql)) -(defmethod evaluate ((memory space-memory) (term (eql :cons))) +(defmethod evaluate ((memory space-memory) + (term (eql :cons))) (let ((binding (pop! memory)) (term (pop! memory))) (if (and (= 1 (length binding)) @@ -283,7 +289,8 @@ true 0 if false." (car binding) (cons term (get-word!? memory (car binding))))))) -(defmethod evaluate ((memory space-memory) (term (eql :bind-term))) +(defmethod evaluate ((memory space-memory) + (term (eql :bind-term))) (let ((binding (pop! memory)) (term (pop! memory))) (if (and (= 1 (length binding)) @@ -291,7 +298,8 @@ true 0 if false." (set-word! memory (car binding) term) (error "Cannot use term \" ~a \" as a binder." binding)))) -(defmethod evaluate ((memory space-memory) (term (eql :send))) +(defmethod evaluate ((memory space-memory) + (term (eql :send))) (let ((binding (pop! memory)) (term (pop! memory))) (labels ((send-1-term (term) @@ -305,7 +313,8 @@ true 0 if false." (t (send-1-term term))) (error "Cannot use term \" ~a \" as a machine name." binding))))) -(defmethod evaluate ((memory space-memory) (term (eql :if))) +(defmethod evaluate ((memory space-memory) + (term (eql :if))) (let ((t1 (pop! memory)) (t2 (pop! memory)) (t3 (pop! memory))) @@ -318,7 +327,8 @@ true 0 if false." (let ((t1 (pop! memory))) (funcall f t1))) -(defmethod evaluate ((memory space-memory) (term (eql :eval-term))) +(defmethod evaluate ((memory space-memory) + (term (eql :eval-term))) (f1 memory (lambda (term) (1-EVALUATE memory term)))) (defun describe-term (memory binding) @@ -328,14 +338,16 @@ true 0 if false." (format t "~a ~~ ~a ~%" (car binding) (pretty-term (get-word! memory (car binding)))) (sep!))) -(defmethod evaluate ((memory space-memory) (term (eql :describe))) +(defmethod evaluate ((memory space-memory) + (term (eql :describe))) (let ((binding (pop! memory))) (if (and (= 1 (length binding)) (eql 'symbol (type-of (car binding)))) (describe-term memory binding) (error "Cannot use term \" ~a \" as a binder." (pretty-term binding))))) -(defmethod evaluate ((memory space-memory) (term symbol)) +(defmethod evaluate ((memory space-memory) + (term symbol)) (labels ((cons-terms () (let ((binding (pop! memory)) @@ -372,67 +384,85 @@ true 0 if false." (otherwise (push! memory (get-word! memory term)))))) -(defmethod evaluate ((memory space-memory) (term (eql :print))) +(defmethod evaluate ((memory space-memory) + (term (eql :print))) (let ((last-term (pop! memory))) (format t "~a~%" (pretty-term last-term)))) -(defmethod evaluate ((memory space-memory) (term (eql :format))) +(defmethod evaluate ((memory space-memory) + (term (eql :format))) (let ((last-term (pop! memory))) (format t "~a~%" (format-term last-term)))) -(defmethod evaluate ((memory space-memory) (term (eql :slurp))) +(defmethod evaluate ((memory space-memory) + (term (eql :slurp))) (let ((terms (parse-terms (read-line)))) (mapcar (lambda (term) (evaluate memory term)) terms))) -(defmethod evaluate ((memory space-memory) (_term (eql :dictionary-up))) +(defmethod evaluate ((memory space-memory) + (_term (eql :dictionary-up))) (push-dictionary! memory)) -(defmethod evaluate ((memory space-memory) (_term (eql :dictionary-down))) +(defmethod evaluate ((memory space-memory) + (_term (eql :dictionary-down))) (pop-dictionary! memory)) -(defmethod evaluate ((memory space-memory) (term (eql :r))) +(defmethod evaluate ((memory space-memory) + (term (eql :r))) (reset-memory memory) (when (not *silent-mode*) (format t "Memory reset.~%"))) -(defmethod evaluate ((memory space-memory) (term (eql :rs))) +(defmethod evaluate ((memory space-memory) + (term (eql :rs))) (reset-stack memory) (when (not *silent-mode*) (format t "Stack reset.~%"))) -(defmethod evaluate ((memory space-memory) (term (eql :m))) +(defmethod evaluate ((memory space-memory) + (term (eql :m))) (print-memory! memory)) -(defmethod evaluate ((memory space-memory) (term (eql :s))) +(defmethod evaluate ((memory space-memory) + (term (eql :s))) (print-stack! memory)) -(defmethod evaluate ((memory space-memory) (term (eql :d))) +(defmethod evaluate ((memory space-memory) + (term (eql :d))) (print-dictionary! memory)) -(defmethod evaluate ((_memory space-memory) (_term (eql :debug))) +(defmethod evaluate ((_memory space-memory) + (_term (eql :debug))) (if *debug-mode* (setf *debug-mode* nil) (setf *debug-mode* t))) -(defmethod evaluate ((_memory space-memory) (term (eql :bye))) +(defmethod evaluate ((_memory space-memory) + (term (eql :bye))) (sb-ext:exit)) -(defmethod evaluate ((_memory space-memory) (term (eql :noop))) +(defmethod evaluate ((_memory space-memory) + (term (eql :noop))) "Do nothing." nil) -(defmethod evaluate ((memory space-memory) (term (eql nil))) +(defmethod evaluate ((memory space-memory) + (term (eql nil))) "Do nothing." nil) -(defmethod evaluate ((memory space-memory) (term (eql :load))) +(defmethod evaluate ((memory space-memory) + (term (eql :load))) "Read a file, and evaluate it." (let ((location (pop! memory))) (run-reader! memory #'read-line (read-file-into-string location)))) -(defmethod evaluate ((memory space-memory) (_term (eql :h))) + +(defmethod evaluate ((memory space-memory) + (_term (eql :h))) "Returns the help string." (evaluate memory :help)) -(defmethod evaluate ((_memory space-memory) (_term (eql :help))) +(defmethod evaluate ((_memory space-memory) + (_term (eql :help))) "Returns the help string." (format t " :help - for help. @@ -572,6 +602,32 @@ true 0 if false." (defun add-machine (universe name machine) (setf (gethash name (space-instances universe)) machine)) +(defun remove-machine (universe name) + (setf (gethash name (space-instances universe)) nil)) + +(defun start-machine-thread! (universe machine-name) + (let ((top-level *standard-output*) + (memory (gethash machine-name (space-instances universe)))) + (bt:make-thread + (lambda () + (handler-case + (run-reader-term! memory (lambda () (pop-inbox memory))) + (error (err) + (handle-err err) + (format top-level "In Machine: ~a.~%" machine-name) + (format top-level "Error Type: ~s.~%" (type-of err)) + (format top-level "Error: ~A.~%" err) + (reset-stack memory) + (format top-level "Stack reset.~%") + (reset-inbox memory) + (format top-level "Inbox reset.~%") + (start-machine-thread! universe machine-name))))))) + +(defun initialise-machine (universe new-machine-name) + (setf (gethash new-machine-name (space-instances universe)) + (make-instance 'space-memory :name new-machine-name :parent-universe universe)) + (start-machine-thread! universe new-machine-name)) + (defparameter *universe* (init-universe)) (defun get-memory (memory-name universe) @@ -581,32 +637,12 @@ true 0 if false." (defun send-to-inbox (name-from to-name message universe) (let ((msg (make-instance 'message :sender name-from :contents message))) (bt:with-lock-held (*universe-lock*) - (progn - ;; The machine at the address hasn't been initialised/started. - (when (not (get-memory to-name universe)) - (progn - (setf (gethash to-name (space-instances universe)) - (make-instance 'space-memory :name to-name :parent-universe universe)) - (let ((top-level *standard-output*) - (memory (gethash to-name (space-instances universe)))) - (bt:make-thread - (lambda () - (handler-case - (run-reader-term! memory (lambda () (pop-inbox memory))) - (error (err) - (handle-err err) - (format top-level "In Machine: ~a.~%" to-name) - (format top-level "Error Type: ~s.~%" (type-of err)) - (format top-level "Error: ~A.~%" err) - (reset-stack memory) - (format top-level "Stack reset.~%") - (reset-inbox memory) - (format top-level "Inbox reset.~%") - (run-reader-term! memory (lambda () (pop-inbox memory)))))))))) - - (setf (inbox (gethash to-name (space-instances universe))) - (concatenate 'list (inbox (gethash to-name (space-instances universe))) - (list msg))))))) + ;; The machine at the address hasn't been initialised/started. + (when (not (get-memory to-name universe)) (initialise-machine universe to-name)) + (setf (inbox (gethash to-name (space-instances universe))) + (concatenate 'list + (inbox (gethash to-name (space-instances universe))) + (list msg)))))) (defun pop-inbox (memory) "Pops a term from the inbox."