#!/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)
(embddr vcd))
(define INDEX-FILE "index.html")
(define DELETE-WORK-DIR #t)
(define TOP-MODULE "testbench")
(define SNIPPET-FILE "code.sv")
(define IVERILOG-METATOP-FILE "top_iverilog.sv")
(define VERILATOR-CPP-FILE "top_verilator.cpp")
(define USE-TIME-IN-SAVE-URL #f)
(define IVERILOG-EXE "iverilog")
(define VVP-EXE "vvp")
(define VERILATOR-EXE "verilator")
(define URI-IVERILOG "iverilog")
(define URI-VERILATOR "verilator")
(define URI-SAVE-CODE "save")
(define LOG-DBG 3)
(define LOG-VERBOSE 2)
(define LOG-INFO 1)
(define LOG-ERROR 0)
(define LOG-LEVEL LOG-VERBOSE)
(define DEFAULT-CODE
(string-append
"`timescale 1ps/1ps\n\n"
;; (format "module ~a (input clock);\n" TOP-MODULE)
(format "module ~a;\n" TOP-MODULE)
" logic clock = 1'b0;\n"
" initial forever #(5ns) clock = ~clock;\n"
"\n"
" initial begin\n"
" $display(\"Hello world!\");\n"
" repeat(10) @(posedge clock);\n"
" $finish();\n"
" end\n"
"endmodule\n"))
(define DEFAULT-CANVAS-WIDTH 800)
(define (multistring . strings)
(apply string-append
(insert-between strings "\n")))
(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))
;;;
;;; Logger
;;;
(define (logger . args)
(unless (null? args)
(let ((prefix
(format "~a | "
(strftime "%c" (localtime (current-time))))))
(if (number? (car args))
(when (<= (car args) LOG-LEVEL)
(display prefix)
(display (apply format (cdr args)))
(newline))
(begin
(display prefix)
(display (apply format args))
(newline))))
(force-output (current-output-port))))
;;;
;;; 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))))
;;;
;;; 404 response
;;;
(define (not-found request)
(values (build-response #:code 404)
(string-append "Resource not found: "
(uri->string (request-uri request)))))
;;;
;;; Common text/html response
;;;
(define* (make-response str #:key
(content-type 'text/html)
(content-type-params '((charset . "utf-8"))))
(values (build-response
#:headers `((content-type . (,content-type ,@content-type-params)))
;; #:headers `((content-type . (,(if (null? encoding)
;; type
;; (cons type encoding)))))
#:code 200)
str))
;;;
;;; File reader
;;;
(define* (file-reader file-name #:key
(max-read-length 512)
(max-file-size #f))
(lambda (port)
(guard ""
(call-with-input-file file-name
(lambda (in)
(let loop ((readed 0))
(when (or (not max-file-size)
(< readed max-file-size))
(let ((data (get-bytevector-n in max-read-length)))
(unless (eof-object? data)
(put-bytevector port data)
(loop (+ readed (bytevector-length data))))))))
#:binary #t))))
;;;
;;; File response
;;;
(define* (file-response file #:key
(content-type 'application/octet-stream)
(content-type-params '((charset . "")))
(max-file-size #f))
(make-response
(file-reader file #:max-file-size max-file-size)
#:content-type content-type
#:content-type-params content-type-params))
;;;
;;; Simple format with convert input number to inexact numbers
;;;
(define (format-inex . args)
(apply format
(map (lambda (arg)
(if (number? arg)
(exact->inexact
(/ (round (* arg 100)) 100))
arg))
args)))
;;;
;;; Make SVG drawing of VCD signals
;;;
(define* (vcd-signal->svg signal tstart tend x y width height text-position
#:key (id #f) (data-hw 3))
(let* ((time-per-pixel (/ (- tend tstart) width))
(sig-width (vcd-signal-size signal))
(sig-type (vcd-signal-type signal))
(y0 y)
(y1 (+ y height))
(yz (+ y (/ height 2)))
(half-dy (/ (- y1 y0) 2))
(id (if id (format " id=\"~a\"" id) "")))
(let next-sample ((samples (vcd-signal-get signal))
(value (if (eq? sig-type 'real)
0
(make-string sig-width #\x)))
(time tstart)
(svg '()))
(if (null? samples)
svg
(let ((sample-time (car (car samples)))
(sample-value (cdr (car samples))))
(if (and (< sample-time tend)
(or (< (- sample-time time) time-per-pixel)
(and (equal? value sample-value)
(not (eq? sig-type 'event)))))
(next-sample
(cdr samples)
(if (<= sample-time tstart)
sample-value
value)
time svg)
(next-sample
(cdr samples) sample-value sample-time
(cons
(cond
((or (eq? sig-type 'bits)
(eq? sig-type 'real))
(if (and (= sig-width 1)
(not (eq? sig-type 'real))
(not (char-ci=? (string-ref value 0) #\x)))
;; Scalar
(let ((x0 (+ x (/ (- time tstart) time-per-pixel)))
(x1 (+ x (/ (- sample-time tstart) time-per-pixel))))
(string-append
(format-inex "
\n
~a\n" (string-concatenate (vcd->svg vcd (if (< need-width canvas-width) canvas-width (if (> need-width maximum-canvas-width) maximum-canvas-width need-width)))) log)) (format "
~a\n" log))) ;;; ;;; Web page handler ;;; (define (make-page-handler host root index-file work-base stor-base max-code-size sanitize iverilog-wrap vvp-wrap verilator-wrap verilator-sim-wrap verilator-build-jobs) (let* ((root-path (split-and-decode-uri-path root)) (root (encode-and-join-uri-path root-path)) (iverilog-path (append root-path `(,URI-IVERILOG))) (verilator-path (append root-path `(,URI-VERILATOR))) (savecode-path (append root-path `(,URI-SAVE-CODE))) (index-html (read-template-text index-file `(("IVERILOGPOSTURI" ,(encode-and-join-uri-path iverilog-path)) ("VERILATORPOSTURI" ,(encode-and-join-uri-path verilator-path)) ("SAVECODEURI" ,(encode-and-join-uri-path savecode-path)) ("HELPSTRING", (string-concatenate (insert-between `("Verilog Playground by Punzik (c) 2022" "" ,(format "Icarus: ~a" (app-version (wrap-exe IVERILOG-EXE iverilog-wrap) "-V")) ,(format "Verilator: ~a" (app-version (wrap-exe VERILATOR-EXE verilator-wrap))) "" "Rules:" "0. Don't fool around ;)" "1. The top module must be named 'testbench'." "2. The top module must have an input clock signal." "3. Code size should not exceed 10000 characters." "4. Code execution time no longer than 5 seconds.") "\\n")))))) (iverilog-metatop (call-with-input-file IVERILOG-METATOP-FILE get-string-all)) (verilator-cpp (call-with-input-file VERILATOR-CPP-FILE get-string-all))) (lambda (request request-body) (let (;; Requested resource path (path (split-and-decode-uri-path (uri-path (request-uri request)))) ;; Snippet dir path relative to stor-base (ref-stor-dir (let ((ref (assoc 'referer (request-headers request)))) (and ref (let ((p (get-storage-dir (cdr ref) root))) (and (storage-dir-valid? p) (storage-path-exists? (path+ stor-base p)) p))))) ;; Body of the POST request (code (if request-body (let ((code (utf8->string request-body))) (if (or (zero? max-code-size) (<= (string-length code) max-code-size)) code (substring code 0 max-code-size))) "")) ;; Request query (query (let ((q (uri-query (request-uri request)))) (if q (map (lambda (qstr) (string-split q #\=)) (string-split q #\;)) '())))) (logger LOG-VERBOSE "Request ~a:~a" (request-method request) path) (logger LOG-VERBOSE "Request query:~a" query) (logger LOG-DBG " stor:'~a' len:~a/~a" ref-stor-dir (request-content-length request) (string-length code)) (cond ;; ;; ---- GET requests ;; ((eq? 'GET (request-method request)) (cond ;; Index page ((equal? path root-path) (logger LOG-DBG "Request index page") (make-response (substitute index-html "@~a@" `((CODE ,DEFAULT-CODE))))) ;; Site favicon ((equal? path (append root-path '("favicon.ico"))) (logger LOG-DBG "Request favicon.ico") (file-response "favicon.png" #:content-type 'image/png #:max-file-size 10000)) ;; Get saved snippet ((and (= (length path) (+ (length root-path) 1)) (every equal? path root-path)) (logger LOG-DBG "Request code from storage") (let ((code (if (null? path) DEFAULT-CODE (let* ((stor-dir (last path)) (stor-path (path+ stor-base stor-dir))) (if (and (storage-dir-valid? stor-dir) (storage-path-exists? stor-path)) (read-from-storage stor-path) DEFAULT-CODE))))) (make-response (substitute index-html "@~a@" `((CODE ,code)))))) ;; Wrong GET request (else (logger LOG-DBG "Wrong GET request") (not-found request)))) ;; ;; ---- POST requests ;; ((eq? 'POST (request-method request)) (cond ;; Run simulation ((or (equal? path iverilog-path) (equal? path verilator-path)) (let ((simulator (if (equal? path iverilog-path) 'iverilog 'verilator))) (logger LOG-DBG "Request ~a simulation" (symbol->string simulator)) (let-values (((log vcd) (exec-sim simulator (if sanitize (sanitize-verilog code) code) work-base TOP-MODULE #:metatop iverilog-metatop #:vvp-wrap vvp-wrap #:iverilog-wrap iverilog-wrap #:verilator-wrap verilator-wrap #:verilator-sim-wrap verilator-sim-wrap #:verilator-cpp verilator-cpp #:verilator-build-jobs verilator-build-jobs))) (let ((canvas-width (let ((v (assoc "width" query))) (or (and v (string->number (cadr v))) DEFAULT-CANVAS-WIDTH)))) (make-response (make-log-html log vcd canvas-width) #:content-type 'text/plain))))) ;; Save snippet ((or (equal? path savecode-path)) (logger LOG-DBG "Request code saving") (let ((old-code (if ref-stor-dir (read-from-storage (path+ stor-base ref-stor-dir)) DEFAULT-CODE))) (if (equal? code old-code) ;; If code is not changed do nothing (make-response (encode-and-join-uri-path (append root-path `(,ref-stor-dir))) #:content-type 'text/plain) ;; New code save to new location (let ((stor-dir (basename (mkdtemp (path+ stor-base (if USE-TIME-IN-SAVE-URL (format "~a-XXXXXX" (current-time)) "XXXXXX")))))) (save-to-storage (path+ stor-base stor-dir) code) (make-response (encode-and-join-uri-path (append root-path `(,stor-dir))) #:content-type 'text/plain))))) ;; Wrong POST request (else (logger LOG-DBG "Wrong POST request") (not-found request)))) ;; ;; ---- Unknown requests type ;; (else (logger LOG-DBG "Wrong request method") (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: ''") (-> " --iverilog-wrap PATH Icarus compiler wrapper.") (-> " --vvp-wrap PATH Icarus Verilog interpreter wrapper.") (-> " --verilator-wrap PATH Verilator compiler wrapper.") (-> " --verilator-sim-wrap PATH Verilator simulation executable wrapper.") (-> " --verilator-build-jobs N Verilator parallel build.") (-> " --max-len LEN Set maximum code size in symbols. Default: 0 (infinite)") (-> " --dont-sanitize Do not sanitize verilog code (dangerous)") (-> " --work-base PATH Set work base path. Default: ./") (-> " --stor-base PATH Set snippets storage path. Default: ./") (-> " --log-level LEVEL Set log level from 0 (quiet) to 10 (verbose). Default: 1./") (-> " -h, --help Print this message and exit") (-> "") (-> "Source code and issue tracker: