How do I make my Racket-designed language run scripts from external files?

Viewed 299

I've got a custom language I'm designing in Racket, let's call it waffle.

Let's say I have

(define (printstr . input)
   (if (string? (car input)) 
       (write (string-join input ""))
       (write input)))

; ... a whole bunch of other definitions

(command-line
 #:multi
 [("-v" "--verbose")    "more verbose"      (set! loglevel (add1 loglevel))]
 [("-q" "--quiet")      "be quiet"          (set! loglevel 0)]
 #:once-any
 [("-i" "--in-place")   "edit in-place"     (set! mode 'in-place)]
 [("-c" "--create-new") "create a new file" (set! mode 'new)]
 [("-n" "--dry-run")    "do nothing"        (set! mode #f)]
 #:once-each
 [("-d" "--directory") dir "work in a given directory" (set! root dir)]
 #:help-labels "operations to perform:"
 #:multi
 [("+l" "++line") "add a line"    (set! ops `(,@ops "add"))]
 [("-l" "--line") "delete a line" (set! ops `(,@ops "delete"))]
 [("-e" "--edit") "edit a line"   (set! ops `(,@ops "edit"))]
 #:args (file)
 (define in (open-input-file file))

 ; This is probably where I'm going wrong with how my language REPL evaluates files passed to it.
 (eval (file->list file) ns))

Then I create an executable from DrRacket using 'Racket [menu] -> Create Executable ... -> [Type] Launcher'. Name is e.g. waffle-test.

I've got a file written in my waffle language, hello.waffle:

(printstr "Hello!")

I expect this to print 'Hello!' on the command line and then exit without errors. But I get a strange error I don't understand, and I get my prompt back without a newline.

$ ./waffle-test hello.waffle 
application: not a procedure;
 expected a procedure that can be applied to arguments
  given: #<void>
  arguments...: [none]
  context...:
   eval-one-top12
   "/home/connie/Desktop/racket-ffgardfghf/waffle": [running body]
   temp37_0
   for-loop
   run-module-instance!125
   perform-require!78
"Hello!" $

I know you're not supposed to use eval but I can't find out how to make my language executable read and run files I pass to it. What is the best approach to doing this?

2 Answers

Just with this simple test I figured out a couple of things:

#!racket
(file->list "test.waffle")

With test.waffle as:

(waffle me)
(waffle it)

The repl printed:

((waffle me)
 (waffle it))

However that is not valid code, even if waffle is a valid procedure. You need it to look like this:

(begin
  (waffle me)
  (waffle it))

Now you can do this by require your language to have it, but you can also just cons a begin to the resulting structure and eval will evaluate each top level form one by one in order.

You will run into problems with using eval. You'll know that soon enough. The correct way to make an interpreter is by creating your own eval that implements your languages syntax and takes en environment with primitives. It has an interface to use the host, but not regardless. With eval waffle programs has access to all internals and you're not really creating an interpreter since you just expose Racket.

I remember someone did the same with Ruby and had a web page and someone just tried to type in a command that deleted all the files on the system and the web service went away.

By fixing a few bugs in my code I managed to solve my problem!

I changed

(eval (file->list file) ns))

to

(define program (cons 'begin (file->list file)))

(eval program ns))

I also changed the 'printstr' function from

 (define (printstr . input)
   (if (string? (car input)) 
       (write (string-join input ""))
       (write input)))

to

 (define (printstr input)
   (displayln input))
Related