#!/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. Code size should not exceed 10000 characters." "3. 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: