PostScript is homoiconic, and semantically quite Lisp-like, when it's not being syntactically somewhat Forth-like.

https://news.ycombinator.com/item?id=21968842

>[...] PostScript and Lisp are homoiconic, but Forth is not. The PSIBER paper on medium goes into that (but doesn't mention the word homoiconic, just describes how PS data structures are PS code, so a data editor is a code editor too).

https://medium.com/@donhopkins/the-shape-of-psiber-space-oct...

Also, here is a metacircular PostScript interpreter, ps.ps: a PostScript interpreter written in PostScript! Since PostScript is homoiconic and so much like Lisp, it was as easy as writing a metacircular Lisp interpreter (but quite different in how it works, since PostScript and Lisp have very different execution models).

https://donhopkins.com/home/archive/psiber/cyber/ps.ps

The heart of the metacircular interpreter, "iexec", uses some dynamically defined macros like MumbleFrotz, PushExec, Popexec, containing embedded literal references to data (a dictionary representing the interpreter state, and ExecStack, and array representing the execution stack).

% interpretivly execute an object

/iexec { % obj => ...
100 dict begin
% This functions "end"s the interpreter dict, executes an object in the
% context of the interpreted process, and "begin"'s back onto the
% interpreter dict. Note the circularity.
/MumbleFrotz [ % obj => ...
/end load /exec load currentdict /begin load
] cvx def

/ExecStack 32 array def
/ExecSP -1 def

/PushExec [ % obj => -
/ExecSP dup cvx 1 /add load /store load
ExecStack /exch load /ExecSP cvx /exch load /put load
] cvx def

/PopExec [ % obj => -
ExecStack /ExecSP cvx /get load
/ExecSP dup cvx 1 /sub load /store load
] cvx def

/TraceStep {
iexec-step
} def

PushExec

{ ExecSP 0 lt { nullproc exit } if % nothing left to execute? goodbye.

ExecStack 0 ExecSP 1 add getinterval
TraceStep pop

% pop top of exec stack onto the operand stack
PopExec

% is it executable? (else just push literal)
dup xcheck { % obj
% do we know how to execute it?
dup type
//iexec-types 1 index known { % obj type
//iexec-types exch get exec % ...
} { % obj type
% some random type. just push it.
pop % obj
} ifelse
} if % else: obj

} loop % goodbye-proc

currentdict /MumbleFrotz undef % Clean up circular reference
end
exec % whoever exited the above loop left a goodbye proc on the stack.
} def

It also has a "vexec" function that executes arbitrary PostScript code, and prints out another text PostScript program to draw an animated trace diagram of the operand stack depth -vs- execution stack depth. That's another simpler kind of macro that produces text instead of structures. So I was using the metacircular PostScript interpreter to visualize one structural PostScript program's execution, by producing and executing the resulting text PostScript program in another PostScript interpreter!

I don't currently have the PS output or rendered images decoded and online, but here's a readme:

https://donhopkins.com/home/archive/psiber/cyber/twist.readm...

This is a plot of the execution stack (x-axis) and the operand stack (y-axis)
during the execution of the PostScript /quicksort routine. Each successive
picture is more twisted than the last. Twisting is accomplished by rotating
the coordinate system clockwise slightly around the center of each of the
dots as they are drawn. The rotation around each plotted point accumulates
to make the whole drawing curl up. The more twisted away from the original
orientation a point is, the later it occurred in time.
In the first picture, the untwisted version, up corresponds to a deeper
operand stack (pushing things on the stack moves you up), and right
corresponds to a deeper execution stack (procedure calls and control
structures move you right). The lines follow changes in the state of
the stack between steps of the interpreter. (This was made possible by
a PostScript interpreter written in PostScript.)

To see the twist animation, run monochrome NeWS, type "psh" to the shell,
then type "(/wherever/you/put/it/twist.ps)run".
The reason you can't just psh the file directly is that NeWS 1.1 psh
does $1 $2 $3 arg substitution, even on binary data! (X11/NeWS psh should
work, so you can just go "psh twist.ps")

What the file twist.ps contains is a short header defining the function
"c", which reads a canvas and displayed it on the framebuffer. That is
followed by a series of "c"'s each followed by a 1 bit deep 1152x900
sun raster files.

-Don Hopkins
(don@brillig.umd.edu)