Improve parser - handle structure of netlogo programs
[clnl] / src / main / parse.lisp
1 (in-package #:clnl-parser)
2
3 ; Ok, after thinking about this a little, the parser is completely contextual
4 ; based on what has come before.  We can't do a contextless parsing, like we
5 ; could in other languages, due to amiguity about reporters vs reporter tasks
6 ;
7 ; So, for instance, we could have:
8 ;   x + y => (+ x y)
9 ;   x + y => (x (task +) y)
10 ; So the definition of "+" is completely dependent on the nature of x
11 ;
12 ; The goal of this parser should be to turn in the amiguous lexed ast representing
13 ; NetLogo into an unambigious S-expression, and nothing more, so things like
14 ; expectation of commands being the first symbol is not be necessary until later
15 ;
16 ; In general, the parser will:
17 ;  * Parse the structure of the lexed output first
18 ;  * Parse the structure of the individual expressions (finding ('s and ['s and doing the right thing)
19 ;  * Coalate things into an unambigious expressions
20 ;  * Then we're done, let someone else make it evaluatable
21 ;    - We don't really care if things are commands or reporters right now
22
23 (defparameter *prims* nil)
24 ; prims that are created when compiling the netlogo file
25 ; usually via procedures or top level things like breed declarations
26 (defparameter *dynamic-prims* nil)
27 (defvar *in-structure* nil)
28
29 (defun prim-name (prim) (getf prim :name))
30 (defun prim-num-args (prim) (length (getf prim :args)))
31 (defun prim-args (prim) (getf prim :args))
32 (defun prim-in-structure (prim) (getf prim :in-structure))
33
34 (defun find-prim (symb) (find symb *prims* :key #'prim-name))
35
36 ; Make this only as complicated as it needs to be, letting it grow
37 ; as we take on more and more of the language
38 (defun parse (lexed-ast &optional structure)
39  "PARSE LEXED-AST &optional STRUCTURE => AST
40
41 ARGUMENTS AND VALUES:
42
43   LEXED-AST: An ambigious ast
44   STRUCTURE: A boolean
45   AST: An unambigious ast that can be transpiled
46
47 DESCRIPTION:
48
49   PARSE takes a ambigious LEXED-AST and converts it to an unambigious one.
50
51   When STRUCTURE is true, parse is done with the expanded indentifier set used
52   in NetLogo files, as well as pulling out procedure definitions.
53
54   The need for a parser between the lexer and the transpiler is because NetLogo
55   needs two passes to turn into something that can be used.  This is the only entry
56   point into this module, and should probably remain that way.
57
58   There's also a lot of error checking that the LEXED-AST even makes sense, even
59   though the lexer obviously thought it did.
60
61   Examples are too numerous and varied, but by inserting an output between
62   the lexer and this code, a good idea of what goes on can be gotten."
63  (let
64   ; could have defined this using the special variable, but didn't to make the
65   ; function definition simpler, as well as the documentation.
66   ((*in-structure* structure))
67   (parse-internal lexed-ast)))
68
69 (defun parse-internal (lexed-ast)
70  (cond
71   ((not lexed-ast) nil)
72   ((numberp (car lexed-ast)) (cons (coerce (car lexed-ast) 'double-float) (parse-internal (cdr lexed-ast))))
73   ((eql :[ (car lexed-ast)) (parse-block (cdr lexed-ast)))
74   ((eql :to (car lexed-ast)) (parse-procedure lexed-ast))
75   ((and (symbolp (car lexed-ast)) (find-prim (car lexed-ast)))
76    (let
77     ((prim (find-prim (car lexed-ast))))
78     (when (and (not *in-structure*) (prim-in-structure prim))
79      (error "This doesn't make sense here"))
80     (if
81      (and (= (prim-num-args prim) 1) (eql :unevaluated-list (car (prim-args prim))))
82      (parse-prim-with-unevaluated-list prim lexed-ast)
83      (parse-prim-normally prim lexed-ast))))
84   (t (error "Couldn't parse ~S" lexed-ast))))
85
86 ; This is a special case but left with a little wiggle room for future
87 ; enhancements, like code blocks
88 (defun parse-prim-with-unevaluated-list (prim lexed-ast)
89  (when (not (eql :[ (cadr lexed-ast)))
90   (error "Expected list literal here"))
91  (multiple-value-bind (in-list after-list) (find-closing-bracket (cddr lexed-ast))
92   (cons
93    (list (prim-name prim) (cons :list-literal in-list))
94    (parse-internal after-list))))
95
96 (defun parse-prim-normally (prim lexed-ast)
97  (let
98   ((num-args (prim-num-args prim))
99    (parsed-remainder (parse-internal (cdr lexed-ast))))
100   (cons
101    (cons
102     (prim-name prim)
103     (mapcar
104      #'help-arg
105      (prim-args prim)
106      (butlast parsed-remainder (- (length parsed-remainder) num-args))))
107    (nthcdr num-args parsed-remainder))))
108
109 (defun help-arg (arg-type arg)
110  (case arg-type
111   (:command-block
112    (if (not (and (consp arg) (eql 'block (car arg))))
113     (error "Required a block, but found a ~A" arg)
114     (cons :command-block (cdr arg))))
115   (:list
116    (if (and (consp arg) (eql 'block (car arg)))
117     (cons :list-literal (cdr arg))
118     arg))
119   (t arg)))
120
121 (defun parse-block (tokens)
122  (multiple-value-bind (in-block after-block) (find-closing-bracket tokens)
123   (cons
124    (cons
125     'block
126     (parse-internal in-block))
127    (parse-internal after-block))))
128
129 (defun find-closing-bracket (tokens &optional (depth 0))
130  (cond
131   ((not tokens) (error "Failed to find a matching closing bracket"))
132   ((and (eql :] (car tokens)) (= depth 0)) (values nil (cdr tokens)))
133   (t (multiple-value-bind
134       (in-block after-block)
135       (find-closing-bracket (cdr tokens) (case (car tokens) (:[ (1+ depth)) (:] (1- depth)) (t depth)))
136       (values (cons (car tokens) in-block) after-block)))))
137
138 ; Due to the non expression style syntax of procedures, this must be special cased
139 (defun parse-procedure (tokens)
140  (when (not *in-structure*) (error "This doesn't make sense here"))
141  (multiple-value-bind (in-block after-block) (find-end tokens)
142   (declare (ignore in-block))
143   (cons
144    (cons
145     (car tokens)
146     nil) ; Update this to parsing the internal of the inblock)
147    (parse-internal after-block))))
148
149 (defun find-end (tokens)
150  (cond
151   ((not tokens) (error "Failed to find end"))
152   ((eql :end (car tokens)) (values nil (cdr tokens)))
153   (t (multiple-value-bind (in-block after-block) (find-end (cdr tokens))
154       (values (cons (car tokens) in-block) after-block)))))
155
156 ; Used to populate dynamic-prims
157 (defun determine-procedure-definition (tokens)
158  (declare (ignore tokens)))
159
160 (defmacro defprim (name args)
161  `(push
162    (list :name ,name :args ',args)
163    *prims*))
164
165 (defmacro defstructureprim (name args)
166  `(push
167    (list :name ,name :args ',args :in-structure t)
168    *prims*))
169
170 ; This list of prims will get combined with the mapping to actual code later
171 ; Current list of argument types we accept:
172 ; - :number
173 ; - :agentset
174 ; - :command-block
175 ; - t - any type
176 (defprim :ask (:agentset :command-block))
177 (defprim :crt (:number))
178 (defprim :fd (:number))
179 (defprim :random-float (:number))
180 (defprim :show (t))
181 (defprim :turtles ())
182
183 (defstructureprim :globals (:unevaluated-list))
184 (defstructureprim :breed (:unevaluated-list))
185 (defstructureprim :turtles-own (:unevaluated-list))
186 (defstructureprim :patches-own (:unevaluated-list))