473 lines
15 KiB
Scheme
Executable File
473 lines
15 KiB
Scheme
Executable File
#!/usr/bin/env -S guile -e "main" -s
|
|
!#
|
|
|
|
;; -*- geiser-scheme-implementation: guile -*-
|
|
|
|
(import (srfi srfi-1)
|
|
(srfi srfi-11)
|
|
(srfi srfi-26)
|
|
(srfi srfi-28)
|
|
(rnrs bytevectors)
|
|
(web server)
|
|
(web request)
|
|
(web response)
|
|
(web uri)
|
|
(sxml simple)
|
|
(ice-9 regex)
|
|
(ice-9 binary-ports)
|
|
(ice-9 textual-ports)
|
|
(ice-9 popen))
|
|
|
|
(import (embddr common)
|
|
(embddr optargs))
|
|
|
|
(define INDEX-FILE "index.html")
|
|
(define LOGGING #t)
|
|
(define DELETE-WORK-DIR #t)
|
|
|
|
(define DEFAULT-CODE
|
|
(string-append
|
|
"`timescale 10ps/10ps\n"
|
|
"module test;\n"
|
|
" initial begin\n"
|
|
" $display(\"Hello world!\");\n"
|
|
" $finish();\n"
|
|
" end\n"
|
|
"endmodule\n"))
|
|
|
|
(define (multistring . strings)
|
|
(apply string-append
|
|
(insert-between strings "\n")))
|
|
|
|
(define (not-found request)
|
|
(values (build-response #:code 404)
|
|
(string-append "Resource not found: "
|
|
(uri->string (request-uri request)))))
|
|
|
|
(define* (make-response str #:key (type 'text/html))
|
|
(values (build-response
|
|
#:headers `((content-type . (,type (charset . "utf-8"))))
|
|
#:code 200)
|
|
str))
|
|
|
|
(define* (file-reader file-name #:key (max-read-length 512))
|
|
(lambda (port)
|
|
(with-input-from-file file-name
|
|
(lambda ()
|
|
(let loop ()
|
|
(let ((data (get-bytevector-n (current-input-port)
|
|
max-read-length)))
|
|
(when (not (eof-object? data))
|
|
(put-bytevector port data)
|
|
(loop))))))))
|
|
|
|
(define* (file-response file #:key (type 'text/html))
|
|
(make-response (file-reader file) #:type type))
|
|
|
|
(define-syntax guard
|
|
(syntax-rules ()
|
|
((_ default code...)
|
|
(with-exception-handler (lambda (e) default)
|
|
(lambda () code...)
|
|
#:unwind? #t))))
|
|
|
|
(define (print . args)
|
|
(display (apply format args)))
|
|
|
|
(define (println . args)
|
|
(display (apply format args))
|
|
(newline))
|
|
|
|
(define (printlog . args)
|
|
(when LOGGING
|
|
(display (apply format args))
|
|
(newline)))
|
|
|
|
;;;
|
|
;;; Return directory list
|
|
;;;
|
|
(define (list-dir path)
|
|
(if (file-exists? path)
|
|
(let ((dir (opendir path)))
|
|
(let loop ((ls '()))
|
|
(let ((item (readdir dir)))
|
|
(if (eof-object? item)
|
|
(begin
|
|
(closedir dir)
|
|
ls)
|
|
(if (or (string=? item ".")
|
|
(string=? item ".."))
|
|
(loop ls)
|
|
(loop (cons (string-append path "/" item) ls)))))))
|
|
'()))
|
|
|
|
;;;
|
|
;;; Recursive delete directory
|
|
;;;
|
|
(define (delete-recursive path)
|
|
(let ((path (canonicalize-path path)))
|
|
(if (eq? 'directory (stat:type (stat path)))
|
|
(begin
|
|
(for-each delete-recursive (list-dir path))
|
|
(rmdir path))
|
|
(delete-file path))))
|
|
|
|
;;;
|
|
;;; Trim list
|
|
;;;
|
|
(define (list-trim l pred)
|
|
(cond
|
|
((null? l) '())
|
|
((pred (car l)) (list-trim (cdr l) pred))
|
|
(else
|
|
(let ((lr (reverse l)))
|
|
(if (pred (car lr))
|
|
(reverse (list-trim (cdr lr) pred))
|
|
l)))))
|
|
|
|
;;;
|
|
;;; Read template to string
|
|
;;;
|
|
(define (read-template-text file subst)
|
|
(let ((lines (read-template file "%~a%" subst)))
|
|
(apply string-append
|
|
(append-map (cut list <> "\n") lines))))
|
|
|
|
;;;
|
|
;;; Execute system command and capture stdout and stderr to string list
|
|
;;;
|
|
(define* (system-to-string cmd #:key (pwd #f))
|
|
(let* ((cmd (string-append cmd " 2>&1"))
|
|
(cmd (if pwd (format "cd ~a; ~a" pwd cmd) cmd))
|
|
(p (open-input-pipe cmd))
|
|
(out (get-string-all p)))
|
|
(values (close-pipe p) out)))
|
|
|
|
;;;
|
|
;;; Unused
|
|
;;;
|
|
;; (define* (system-to-string-list cmd #:key (pwd #f))
|
|
;; (let-values (((status out)
|
|
;; (system-to-string cmd #:pwd pwd)))
|
|
;; (values
|
|
;; status
|
|
;; (list-trim (string-split out #\newline) string-null?))))
|
|
|
|
;;;
|
|
;;; Make pretty log from executable output
|
|
;;;
|
|
(define (exe-log-pretty cmdline status out)
|
|
(string-append
|
|
(format "$ ~a\n" cmdline)
|
|
(format "Return code: ~a\n" status)
|
|
(if (string-null? out)
|
|
"\n"
|
|
(format "--\n~a\n" out))))
|
|
|
|
;;;
|
|
;;; Trivial sanitize verilog code
|
|
;;;
|
|
(define (sanitize-verilog code)
|
|
(let* (;; $f* functions but not $finish
|
|
(code (regexp-substitute/global #f "\\$f[a-hj-z][a-z]+" code 'pre "$error" 'post))
|
|
;; $scanf
|
|
(code (regexp-substitute/global #f "\\$.?scanf" code 'pre "$error" 'post))
|
|
;; $readmem
|
|
(code (regexp-substitute/global #f "\\$readmem[bh]" code 'pre "$error" 'post))
|
|
;; $dump*
|
|
(code (regexp-substitute/global #f "\\$dump[a-z]*" code 'pre "$error" 'post)))
|
|
code))
|
|
|
|
;;;
|
|
;;; Get module name
|
|
;;;
|
|
(define (module-name code)
|
|
(let ((rx (make-regexp
|
|
"(^|\\s)module\\s*(\\s|(#\\(.*\\)\\s*))[a-zA-Z0-9_]+")))
|
|
(let loop ((pos 0)
|
|
(modname #f))
|
|
(let ((m (regexp-exec rx code pos)))
|
|
(if m
|
|
(loop (match:end m) (match:substring m))
|
|
(if modname
|
|
(match:substring (string-match "[a-zA-Z0-9_]+$" modname))
|
|
#f))))))
|
|
|
|
;;;
|
|
;;; Create dump module
|
|
;;;
|
|
(define (create-dump-module path modname top)
|
|
(define (* . fmt) (display (apply format fmt)) (newline))
|
|
(let ((filename (format "~a/~a.v" path modname)))
|
|
(with-output-to-file filename
|
|
(lambda ()
|
|
(* "`timescale 1ps/1ps")
|
|
(* "module ~a();" modname)
|
|
(* " initial begin")
|
|
(* " $dumpfile(\"~a/~a.vcd\");" path modname)
|
|
(* " $dumpvars(0, ~a);" top)
|
|
(* " end")
|
|
(* "endmodule")))
|
|
filename))
|
|
|
|
;;;
|
|
;;; Make workdir with sources and command file
|
|
;;; Returns work directory path string
|
|
;;;
|
|
(define (make-sim-workdir simulator code top)
|
|
(let* ((work-dir (mkdtemp (format "work-~a-XXXXXX" (current-time))))
|
|
(verilog-file (format "~a/testbench.sv" work-dir))
|
|
(command-file (format "~a/testbench.vc" work-dir))
|
|
(dump-file (create-dump-module work-dir "dump" top)))
|
|
(with-output-to-file verilog-file (cut display code))
|
|
(with-output-to-file command-file
|
|
(lambda ()
|
|
(cond
|
|
((eq? simulator 'iverilog)
|
|
(println "~a" verilog-file)
|
|
(println "~a" dump-file)
|
|
(println "+define+TESTBENCH")
|
|
(println "+timescale+1ps/1ps")))))
|
|
work-dir))
|
|
|
|
;;;
|
|
;;; Compile sources and execute simulation with Icarus Verilog
|
|
;;; Returns (values status log)
|
|
;;;
|
|
(define (exec-sim-iverilog work-dir vvp-exe iverilog-exe)
|
|
;; Compile
|
|
(let ((cmdline (format "~a -g2012 -o ~a/testbench.vvp -c~a/testbench.vc"
|
|
iverilog-exe work-dir work-dir)))
|
|
(let-values (((status out)
|
|
(system-to-string cmdline)))
|
|
(let ((compile-log
|
|
(exe-log-pretty cmdline status out)))
|
|
(if (not (zero? status))
|
|
(values status compile-log)
|
|
|
|
;; Execute
|
|
(let ((cmdline (format "~a ~a/testbench.vvp" vvp-exe work-dir)))
|
|
(let-values (((status out)
|
|
(system-to-string cmdline)))
|
|
(let ((execution-log
|
|
(exe-log-pretty cmdline status out)))
|
|
(values status (string-append compile-log execution-log))))))))))
|
|
|
|
;;;
|
|
;;; Execute simulation
|
|
;;;
|
|
(define* (exec-sim simulator code
|
|
#:key
|
|
(vvp-exe "vvp")
|
|
(iverilog-exe "iverilog"))
|
|
(let ((top (module-name code)))
|
|
(if (not top)
|
|
"Error: No module declaration\n"
|
|
(let ((work-dir (make-sim-workdir simulator code top)))
|
|
(let-values
|
|
(((status log)
|
|
(cond
|
|
((eq? simulator 'iverilog)
|
|
(exec-sim-iverilog work-dir vvp-exe iverilog-exe))
|
|
|
|
(else
|
|
(values -1 "No simulator found!\n")))))
|
|
|
|
;; Delete work dir
|
|
(when DELETE-WORK-DIR
|
|
(delete-recursive work-dir))
|
|
|
|
;; Return log
|
|
(string-append
|
|
log
|
|
(format "-----------------\nSimulation complete~a\n"
|
|
(if (zero? status)
|
|
" succesfully"
|
|
" with errors"))))))))
|
|
|
|
;;;
|
|
;;; Get storage path from URI
|
|
;;;
|
|
(define (get-storage-path uri root-path)
|
|
(string-trim
|
|
(substring (uri-path uri)
|
|
(string-length root-path))
|
|
#\/))
|
|
|
|
;;;
|
|
;;; Check storage path validity
|
|
;;;
|
|
(define (valid-storage-path path)
|
|
(if (or (< (string-length path) 1)
|
|
(> (string-length path) 32))
|
|
#f
|
|
(string-fold
|
|
(lambda (c valid)
|
|
(if (or (char-alphabetic? c)
|
|
(char-numeric? c)
|
|
(char=? c #\-))
|
|
valid #f))
|
|
#t path)))
|
|
|
|
;;;
|
|
;;; Check storage exists
|
|
;;;
|
|
(define (storage-exists path)
|
|
(let ((dir-stat (stat path #f))
|
|
(file-stat (stat (format "~a/code.v" path) #f)))
|
|
(and dir-stat file-stat
|
|
(eq? (stat:type dir-stat) 'directory)
|
|
(eq? (stat:type file-stat) 'regular)
|
|
(not (zero? (logand #o444 (stat:perms file-stat))))
|
|
(not (zero? (logand #o222 (stat:perms file-stat)))))))
|
|
|
|
;;;
|
|
;;; Save code to storage
|
|
;;;
|
|
(define (save-to-storage path code)
|
|
(with-output-to-file (format "~a/code.v" path)
|
|
(cut display code)))
|
|
|
|
;;;
|
|
;;; Read from storage
|
|
;;;
|
|
(define (read-from-storage path)
|
|
(call-with-input-file (format "~a/code.v" path)
|
|
get-string-all))
|
|
|
|
;;;
|
|
;;; Web page handler
|
|
;;;
|
|
(define (make-page-handler host root index-file
|
|
vvp-exe iverilog-exe)
|
|
(let* ((root-path (split-and-decode-uri-path root))
|
|
(root (encode-and-join-uri-path root-path))
|
|
(iverilog-path (append root-path '("iverilog")))
|
|
(savecode-path (append root-path '("save")))
|
|
(iverilog-post-uri (string-append host "/" (encode-and-join-uri-path iverilog-path)))
|
|
(savecode-post-uri (string-append host "/" (encode-and-join-uri-path savecode-path)))
|
|
(index-html (read-template-text index-file `(("IVERILOGPOSTURI" ,iverilog-post-uri)
|
|
("SAVECODEURI" ,savecode-post-uri)))))
|
|
|
|
(lambda (request request-body)
|
|
(let* ((path (split-and-decode-uri-path
|
|
(uri-path
|
|
(request-uri request))))
|
|
(ref (assoc 'referer (request-headers request)))
|
|
(ref-stor (if ref (get-storage-path (cdr ref) root) ""))
|
|
(ref-stor (if (and (valid-storage-path ref-stor)
|
|
(storage-exists ref-stor))
|
|
ref-stor #f)))
|
|
(printlog "-- path: ~a" path)
|
|
(printlog "-- ref-stor: ~a" ref-stor)
|
|
|
|
(cond
|
|
;; Iverilog
|
|
((equal? path iverilog-path)
|
|
(printlog "-- Request simulate")
|
|
|
|
(let ((code (utf8->string request-body)))
|
|
(when ref-stor
|
|
(save-to-storage ref-stor code))
|
|
|
|
(make-response
|
|
(exec-sim 'iverilog
|
|
(sanitize-verilog code)
|
|
#:vvp-exe vvp-exe
|
|
#:iverilog-exe iverilog-exe)
|
|
#:type 'text/plain)))
|
|
|
|
;; Save code
|
|
((equal? path savecode-path)
|
|
(printlog "-- Request save code")
|
|
(let ((storage (or ref-stor
|
|
(mkdtemp (format "~a-XXXXXX" (current-time))))))
|
|
(save-to-storage storage (utf8->string request-body))
|
|
(make-response
|
|
(encode-and-join-uri-path
|
|
(append root-path `(,storage))))))
|
|
|
|
;; Index page
|
|
((equal? path root-path)
|
|
(printlog "-- Request index page")
|
|
(make-response
|
|
(substitute index-html "@~a@" `((CODE ,DEFAULT-CODE)))))
|
|
|
|
;; Storage
|
|
((and (> (length path)
|
|
(length root-path))
|
|
(every equal? path root-path))
|
|
(let ((storage (if (null? path) "" (last path))))
|
|
(printlog "-- Request code from storage ~a" storage)
|
|
(if (and (valid-storage-path storage)
|
|
(storage-exists storage))
|
|
(let ((code (read-from-storage storage)))
|
|
(make-response
|
|
(substitute index-html "@~a@" `((CODE ,code)))))
|
|
(make-response
|
|
(substitute index-html "@~a@" `((CODE ,DEFAULT-CODE)))))))
|
|
|
|
;; Wrong request
|
|
(else
|
|
(printlog "-- Request wrong path")
|
|
(not-found request)))))))
|
|
|
|
;;;
|
|
;;; ----------------------------------------------------------------------
|
|
;;; ------------------------------- MAIN ---------------------------------
|
|
;;; ----------------------------------------------------------------------
|
|
;;;
|
|
|
|
(define (print-help app-name)
|
|
(define (-> . args)
|
|
(display (apply format args) (current-error-port))
|
|
(newline (current-error-port)))
|
|
(let ((app-name (basename app-name)))
|
|
(with-output-to-port (current-error-port)
|
|
(lambda ()
|
|
(-> "Usage: ~a [OPTION]..." app-name)
|
|
(-> "Start Verilog playground WEB server")
|
|
(-> "")
|
|
(-> "Options:")
|
|
(-> " -a, --addr ADDR Listen on ADDR address. Default: 127.0.0.1")
|
|
(-> " -p, --port PORT Listen on PORT port. Default: 8080")
|
|
(-> " -s, --host URL Run on URL hostname. Default: http://127.0.0.1:8080")
|
|
(-> " -r, --root URN Service location root. Default: ''")
|
|
(-> " --ivverilog-exe PATH Set Icarus Verilog compiler executable. Default: iverilog")
|
|
(-> " --vvp-exe PATH Set Icarus Verilog interpreter executable. Default: vvp")
|
|
(-> " -h, --help Print this message and exit")
|
|
(-> "")
|
|
(-> "Source code and issue tracker: <https://github.com/punzik/>")))))
|
|
|
|
(define (main args)
|
|
(debug-disable 'backtrace)
|
|
(let-values
|
|
(((opts rest err)
|
|
(parse-opts (cdr args)
|
|
'(("addr" #\a) required)
|
|
'(("port" #\p) required)
|
|
'(("host" #\s) required)
|
|
'(("root" #\r) required)
|
|
'(("vvp-exe") required)
|
|
'(("iverilog-exe") required)
|
|
'(("help" #\h) none))))
|
|
|
|
(let ((addr (string-trim (or (option-get opts "addr") "127.0.0.1")))
|
|
(port (string->number (string-trim (or (option-get opts "port") "8080"))))
|
|
(host (string-trim (or (option-get opts "host") "http://127.0.0.1:8080")))
|
|
(root (string-trim (or (option-get opts "root") "")))
|
|
(vvp (string-trim (or (option-get opts "vvp-exe") "vvp")))
|
|
(iverilog (string-trim (or (option-get opts "iverilog-exe") "iverilog"))))
|
|
|
|
(if (option-get opts "help")
|
|
(begin
|
|
(print-help (car args))
|
|
(exit -1))
|
|
(begin
|
|
(printlog "Listen on '~a' port '~a'" addr port)
|
|
(printlog "Server URL: '~a/~a'" host root)
|
|
|
|
(run-server
|
|
(make-page-handler host root INDEX-FILE vvp iverilog)
|
|
'http `(#:host ,addr #:port ,port)))))))
|