Intermediate View for Lisp / Clojure DSL

I am developing DSL in Clojure, which is used to control a code generator (in this case, to synthesize a procedural image - clisk ) and am unable to cope with the best representation of intermediate values.

Initially, DSL consisted of functions that returned one or more forms, for example. (Illustrative)

(v+ 1.0 [1.0 'y]) => ['(+ 1.0 1.0) '(+ 1.0 y)] 

These functions can then be composed to create large blocks of code.

It was simple, and the resulting forms could be passed directly to the code generator. However, now I have determined which of these weaknesses can arise with this approach, for example, if you need to transfer some auxiliary data (for example, objects that cannot be encoded in such forms as BufferedImages, metadata useful for optimization, etc. .).

I am sure this is a problem in the Lisp world. What will usually be the best intermediate view for this kind of DSL?

+6
source share
3 answers

Anytime you need an intermediate representation that will be used to generate code, the most obvious thing that comes to my mind is the Abstract Syntax Tree (AST). The presentation of your example is lists that in my experience are not as flexible in form. For any thing other than trivial code generation, I wouldn't beat around the bush and just went with a full-blown AST view. Using lists, you push more work to the generation side to parse information such as types and what the first element means. Moving to the AST view will give you more flexibility and unleash most of the system with more work from the parsing (or more work with the functions that generate the forms). The generation side will do more work, but many of these components can be separated, as their inputs will be more structured.

In terms of what AST should look like, I would either copy Christophe Grand enlive where it used {:tag <tag name> :attrs <map of attrs> :content <some collection>}

or that clojure script uses {:op <some operator> :children <some collection>} .

This makes it pretty general, as you can define arbitrary walkers that look in :children and can cross any structure without knowing exactly what :op or :tag .

Then for atomic components, you can wrap it on a map and provide it with some type information (regarding the semantics of your DSL), which does not depend on the actual type of the object. {:atom <the object> :type :background-image} .

On the code generation side, when you encounter an atom, your code can then send to :type , and then, if you want, further send by the actual type of the object. A generation of collection forms is also easy, send to: op /: tag, and then repeat with the children. For which collection to use for children, I would be more familiar with the discussion of google groups. Their findings were useful to me.

https://groups.google.com/forum/#!topic/clojure-dev/vZLVKmKX0oc/discussion

To summarize, for children, if the meaning of semantic ordering is, for example, in an if statement, then use the map {:conditional z :then y :else x} . If this is just a list of arguments, you can use a vector.

+7
source

I think I do not understand. I would just use lists or structures.

In Lisp, lists can contain, well, anything. I have to say that the CONS cell can point to anything, and thus the list can contain anything. Thus, to a large extent, any other data structure (structures, arrays, maps, etc.) can be used.

Now these structures cannot be visualized, PRINT or visualized into something readable (READ), but this does not mean that they cannot be saved and processed.

Is there any reason why you need to externalize this performance?

+1
source

Not quite the answer, because I don’t know how Clojure works in this regard, but there are reader macros in the CL specially designed for this case: i.e. you define your function to print non-printable objects + a macro reader that reads them the way you printed them. To determine how you print objects, you must define a new print-object method that specializes in the type of object you need and set-macro-character to add a reading function that knows how to read your design object.

There are many things you need to know about, but some that usually act like timer bombs are cases where objects are allowed to refer to themselves recursively, in which case printing must consider previously printed objects.

+1
source

Source: https://habr.com/ru/post/916719/


All Articles