mirror of
https://github.com/justinethier/cyclone.git
synced 2025-05-20 14:19:17 +02:00
564 lines
21 KiB
Scheme
564 lines
21 KiB
Scheme
;;;; Cyclone Scheme
|
|
;;;; https://github.com/justinethier/cyclone
|
|
;;;;
|
|
;;;; Copyright (c) 2014-2016, Justin Ethier
|
|
;;;; All rights reserved.
|
|
;;;;
|
|
;;;; This module contains a front-end for the compiler itself.
|
|
;;;;
|
|
(import (scheme base)
|
|
(scheme case-lambda)
|
|
(scheme eval)
|
|
(scheme file)
|
|
(scheme lazy)
|
|
(scheme read)
|
|
(scheme write)
|
|
(scheme cyclone ast)
|
|
(scheme cyclone common)
|
|
(scheme cyclone util)
|
|
(scheme cyclone cgen)
|
|
(scheme cyclone transforms)
|
|
(scheme cyclone cps-optimizations)
|
|
(scheme cyclone macros)
|
|
(scheme cyclone libraries))
|
|
|
|
(define *optimization-level* 2) ;; Default level
|
|
|
|
;; Code emission.
|
|
|
|
; c-compile-and-emit : (string -> A) exp -> void
|
|
(define (c-compile-and-emit input-program program:imports/code
|
|
lib-deps src-file append-dirs prepend-dirs)
|
|
(call/cc
|
|
(lambda (return)
|
|
(define globals '())
|
|
(define module-globals '()) ;; Globals defined by this module
|
|
(define program? #t) ;; Are we building a program or a library?
|
|
(define imports '())
|
|
(define imported-vars '())
|
|
(define lib-name '())
|
|
(define lib-exports '())
|
|
(define lib-renamed-exports '())
|
|
(define lib-pass-thru-exports '())
|
|
(define c-headers '())
|
|
(define rename-env (env:extend-environment '() '() '()))
|
|
|
|
(emit *c-file-header-comment*) ; Guarantee placement at top of C file
|
|
|
|
(trace:info "---------------- input program:")
|
|
(trace:info input-program) ;pretty-print
|
|
|
|
(cond
|
|
((library? (car input-program))
|
|
(let ((includes (lib:includes (car input-program))))
|
|
(set! program? #f)
|
|
(set! lib-name (lib:name (car input-program)))
|
|
(set! c-headers (lib:include-c-headers (car input-program)))
|
|
(set! lib-exports
|
|
(cons
|
|
(lib:name->symbol lib-name)
|
|
(lib:exports (car input-program))))
|
|
(set! lib-pass-thru-exports lib-exports)
|
|
(set! lib-renamed-exports
|
|
(lib:rename-exports (car input-program)))
|
|
(set! imports (lib:imports (car input-program)))
|
|
(set! input-program (lib:body (car input-program)))
|
|
;; Add any renamed exports to the begin section
|
|
(set! input-program
|
|
(append
|
|
(map
|
|
(lambda (r)
|
|
`(define ,(caddr r) ,(cadr r)))
|
|
lib-renamed-exports)
|
|
input-program))
|
|
;; Prepend any included files into the begin section
|
|
(if (not (null? includes))
|
|
(for-each
|
|
(lambda (include)
|
|
(set! input-program
|
|
(append (read-file ;(string-append
|
|
(lib:import->path lib-name append-dirs prepend-dirs include)
|
|
;include)
|
|
)
|
|
input-program)))
|
|
(reverse includes))))) ;; Append code in same order as the library's includes
|
|
(else
|
|
;; Handle imports, if present
|
|
(let ((reduction program:imports/code))
|
|
(set! imports (car reduction))
|
|
(set! input-program (cdr reduction)))
|
|
|
|
;; Handle any C headers
|
|
(let ((headers (lib:include-c-headers `(dummy dummy ,@input-program))))
|
|
(cond
|
|
((not (null? headers))
|
|
(set! c-headers headers)
|
|
(set! input-program
|
|
(filter
|
|
(lambda (expr)
|
|
(not (tagged-list? 'include-c-header expr)))
|
|
input-program)))))
|
|
))
|
|
|
|
;; Process library imports
|
|
(trace:info "imports:")
|
|
(trace:info imports)
|
|
(set! imported-vars (lib:imports->idb imports append-dirs prepend-dirs))
|
|
(trace:info "resolved imports:")
|
|
(trace:info imported-vars)
|
|
(let ((meta (lib:resolve-meta imports append-dirs prepend-dirs)))
|
|
(set! *defined-macros* (append meta *defined-macros*))
|
|
(trace:info "resolved macros:")
|
|
(trace:info meta))
|
|
|
|
;; TODO: how to handle stdlib when compiling a library??
|
|
;; either need to keep track of what was actually used,
|
|
;; or just assume all imports were used and include them
|
|
;; in final compiled program
|
|
;(set! input-program (add-libs input-program))
|
|
|
|
;; Load macros for expansion phase
|
|
(let ((macros (filter
|
|
(lambda (v)
|
|
(Cyc-macro? (Cyc-get-cvar (cdr v))))
|
|
(Cyc-global-vars))))
|
|
(set! *defined-macros*
|
|
(append
|
|
macros
|
|
*defined-macros*)))
|
|
(macro:load-env! *defined-macros* (create-environment '() '()))
|
|
|
|
;; Expand macros
|
|
;; In each case, the input is expanded in a way that ensures
|
|
;; defines from any top-level begins are spliced correctly.
|
|
(set! input-program
|
|
(cond
|
|
(program?
|
|
(expand-lambda-body input-program (macro:get-env) rename-env))
|
|
(else
|
|
(let ((expanded (expand `(begin ,@input-program)
|
|
(macro:get-env)
|
|
rename-env)))
|
|
(cond
|
|
((and (pair? expanded)
|
|
(tagged-list? 'lambda (car expanded)))
|
|
(lambda->exp (car expanded)))
|
|
((tagged-list? 'define expanded)
|
|
(list expanded))
|
|
((boolean? expanded)
|
|
(list expanded))
|
|
(else
|
|
(error `(Unhandled expansion ,expanded))))))))
|
|
(trace:info "---------------- after macro expansion:")
|
|
(trace:info input-program) ;pretty-print
|
|
; TODO:
|
|
(set! input-program (macro:cleanup input-program rename-env))
|
|
(trace:info "---------------- after macro expansion cleanup:")
|
|
(trace:info input-program) ;pretty-print
|
|
|
|
;; Separate global definitions from the rest of the top-level code
|
|
(set! input-program
|
|
(isolate-globals input-program program? lib-name rename-env))
|
|
|
|
;; Optimize-out unused global variables
|
|
;; For now, do not do this if eval is used.
|
|
;; TODO: do not have to be so aggressive, unless (eval (read)) or such
|
|
(if (not (has-global? input-program 'eval))
|
|
(set! input-program
|
|
(filter-unused-variables input-program lib-exports)))
|
|
|
|
(trace:info "---------------- after processing globals")
|
|
(trace:info input-program) ;pretty-print
|
|
|
|
;; Identify global variables
|
|
(set! module-globals (global-vars input-program))
|
|
(set! globals (append (lib:idb:ids imported-vars) module-globals))
|
|
|
|
;; Trim down the export list to any exports that are just "pass throughs"
|
|
;; from imported libraries. That is, they are not actually defined in
|
|
;; the library being compiled
|
|
(set! lib-pass-thru-exports
|
|
(filter
|
|
(lambda (e)
|
|
(let ((module-global? (member e module-globals))
|
|
(imported-var? (assoc e imported-vars)))
|
|
(cond
|
|
((eq? e 'call/cc) #f) ;; Special case
|
|
((and (not module-global?)
|
|
(not imported-var?))
|
|
(error "Identifier is exported but not defined" e))
|
|
(else
|
|
;; Pass throughs are not defined in this module,
|
|
;; but by definition must be defined in an imported lib
|
|
(and (not module-global?) imported-var?)))))
|
|
lib-pass-thru-exports))
|
|
(trace:info "pass thru exports:")
|
|
(trace:info lib-pass-thru-exports)
|
|
|
|
; Note alpha-conversion is overloaded to convert internal defines to
|
|
; set!'s below, since all remaining phases operate on set!, not define.
|
|
;
|
|
; TODO: consider moving some of this alpha-conv logic below back into trans?
|
|
(set! input-program
|
|
(map
|
|
(lambda (expr)
|
|
(alpha-convert expr globals return))
|
|
input-program))
|
|
(trace:info "---------------- after alpha conversion:")
|
|
(trace:info input-program) ;pretty-print
|
|
|
|
;; Convert some function calls to primitives, if possible
|
|
(set! input-program
|
|
(map
|
|
(lambda (expr)
|
|
(prim-convert expr))
|
|
input-program))
|
|
(trace:info "---------------- after func->primitive conversion:")
|
|
(trace:info input-program) ;pretty-print
|
|
|
|
(let ((cps (map
|
|
(lambda (expr)
|
|
(cps-convert expr))
|
|
input-program)))
|
|
(cond
|
|
((and library? (equal? lib-name '(scheme base)))
|
|
(set! globals (append '(call/cc) globals))
|
|
(set! module-globals (append '(call/cc) module-globals))
|
|
(set! input-program
|
|
;(cons
|
|
; ;; Experimental version of call-with-values,
|
|
; ;; seems OK in compiler but not in eval.
|
|
; '(define call-with-values
|
|
; (lambda (k producer consumer)
|
|
; (let ((x (producer)))
|
|
; (if (and (pair? x) (equal? '(multiple values) (car x)))
|
|
; (apply consumer (cdr x))
|
|
; (consumer k x))))
|
|
; ; (producer
|
|
; ; (lambda (result)
|
|
; ; (consumer k result))))
|
|
; )
|
|
;; multiple args requires more than just this.
|
|
;; may want to look at:
|
|
;; http://stackoverflow.com/questions/16674214/how-to-implement-call-with-values-to-match-the-values-example-in-r5rs
|
|
;; (lambda vals
|
|
;; (apply k consumer vals)))))
|
|
(cons
|
|
;; call/cc must be written in CPS form, so it is added here
|
|
;; TODO: prevents this from being optimized-out
|
|
;; TODO: will this cause issues if another var is assigned to call/cc?
|
|
`(define call/cc
|
|
,(ast:make-lambda
|
|
'(k f)
|
|
(list
|
|
(list 'f 'k
|
|
(ast:make-lambda '(_ result)
|
|
(list '(k result)))))))
|
|
;(lambda (k f) (f k (lambda (_ result) (k result)))))
|
|
cps)));)
|
|
(else
|
|
;; No need for call/cc yet
|
|
(set! input-program cps))))
|
|
(trace:info "---------------- after CPS:")
|
|
(trace:info input-program) ;pretty-print
|
|
|
|
(when (> *optimization-level* 0)
|
|
(set! input-program
|
|
(optimize-cps input-program))
|
|
(trace:info "---------------- after cps optimizations:")
|
|
(trace:info input-program))
|
|
|
|
(set! input-program
|
|
(map
|
|
(lambda (expr)
|
|
(clear-mutables)
|
|
(analyze-mutable-variables expr)
|
|
(wrap-mutables expr globals))
|
|
input-program))
|
|
(trace:info "---------------- after wrap-mutables:")
|
|
(trace:info input-program) ;pretty-print
|
|
|
|
(set! input-program
|
|
(map
|
|
(lambda (expr)
|
|
(cond
|
|
((define? expr)
|
|
;; Global
|
|
`(define ,(define->var expr)
|
|
,@(caddr (closure-convert (define->exp expr) globals))))
|
|
((define-c? expr)
|
|
expr)
|
|
(else
|
|
(caddr ;; Strip off superfluous lambda
|
|
(closure-convert expr globals)))))
|
|
input-program))
|
|
; (caddr ;; Strip off superfluous lambda
|
|
; (closure-convert input-program)))
|
|
(trace:info "---------------- after closure-convert:")
|
|
(trace:info input-program) ;pretty-print
|
|
|
|
(when (not *do-code-gen*)
|
|
(trace:error "DEBUG, existing program")
|
|
(exit 0))
|
|
|
|
(trace:info "---------------- C headers: ")
|
|
(trace:info c-headers)
|
|
|
|
(trace:info "---------------- C code:")
|
|
(mta:code-gen input-program
|
|
program?
|
|
lib-name
|
|
lib-pass-thru-exports
|
|
imported-vars
|
|
module-globals
|
|
c-headers
|
|
lib-deps
|
|
src-file)
|
|
(return '())))) ;; No codes to return
|
|
|
|
;; Read top-level imports from a program and return a cons of:
|
|
;; - imports
|
|
;; - remaining program
|
|
(define (import-reduction expr expander)
|
|
(let ((results
|
|
(foldl
|
|
(lambda (ex accum)
|
|
(define (process e)
|
|
(cond
|
|
((tagged-list? 'import e)
|
|
(cons (cons (cdr e) (car accum)) (cdr accum)))
|
|
(else
|
|
(cons (car accum) (cons e (cdr accum))))))
|
|
(cond
|
|
((tagged-list? 'cond-expand ex)
|
|
(let ((ex* (expander ex))) ;(expand ex (macro:get-env) rename-env)))
|
|
;(trace:info `(DEBUG ,ex* ,ex))
|
|
(if (tagged-list? 'import ex*)
|
|
(process ex*)
|
|
(process ex))))
|
|
(else
|
|
(process ex))))
|
|
(cons '() '())
|
|
expr)))
|
|
(cons
|
|
(apply append (reverse (car results)))
|
|
(reverse (cdr results)))))
|
|
|
|
;; Return a function to expand any built-in macros
|
|
;; NOTE: since this uses a global macro env, it will be overridden later on when
|
|
;; macros are loaded from dependent libraries.
|
|
(define (base-expander)
|
|
(let ((rename-env (env:extend-environment '() '() '()))
|
|
(macros (filter
|
|
(lambda (v)
|
|
(Cyc-macro? (Cyc-get-cvar (cdr v))))
|
|
(Cyc-global-vars))))
|
|
(macro:load-env! macros (create-environment '() '()))
|
|
(lambda (ex)
|
|
(expand ex (macro:get-env) rename-env))))
|
|
|
|
;; TODO: longer-term, will be used to find where cyclone's data is installed
|
|
(define (get-data-path)
|
|
".")
|
|
|
|
(define (get-lib filename)
|
|
(string-append (get-data-path) "/" filename))
|
|
|
|
(define (read-file filename)
|
|
(call-with-input-file filename
|
|
(lambda (port)
|
|
(read-all port))))
|
|
|
|
;; Compile and emit:
|
|
(define (run-compiler args cc? cc-prog cc-exec cc-lib cc-so append-dirs prepend-dirs)
|
|
(let* ((in-file (car args))
|
|
(expander (base-expander))
|
|
(in-prog-raw (read-file in-file))
|
|
(program? (not (library? (car in-prog-raw))))
|
|
(in-prog
|
|
(if program?
|
|
in-prog-raw
|
|
;; Account for any cond-expand declarations in the library
|
|
(list (lib:cond-expand (car in-prog-raw) expander))))
|
|
;; TODO: expand in-prog, if a library, using lib:cond-expand. (OK, this works now)
|
|
;; TODO: will also need to do below in lib:get-all-import-deps, after reading each library
|
|
(program:imports/code (if program? (import-reduction in-prog expander) '()))
|
|
(lib-deps
|
|
(if (and program?
|
|
(not (null? (car program:imports/code))))
|
|
(lib:get-all-import-deps (car program:imports/code) append-dirs prepend-dirs)
|
|
'()))
|
|
(exec-file (basename in-file))
|
|
(src-file (string-append exec-file ".c"))
|
|
(meta-file (string-append exec-file ".meta"))
|
|
(get-comp-env
|
|
(lambda (sym str)
|
|
(if (> (string-length str) 0)
|
|
str
|
|
(Cyc-compilation-environment sym))))
|
|
(create-c-file
|
|
(lambda (program)
|
|
(with-output-to-file
|
|
src-file
|
|
(lambda ()
|
|
(c-compile-and-emit program program:imports/code
|
|
lib-deps in-file append-dirs prepend-dirs)))))
|
|
(result (create-c-file in-prog)))
|
|
|
|
;; Compile the generated C file
|
|
(cond
|
|
(program?
|
|
(letrec ((objs-str
|
|
(apply
|
|
string-append
|
|
(map
|
|
(lambda (i)
|
|
(string-append " " (lib:import->filename i ".o" append-dirs prepend-dirs) " "))
|
|
lib-deps)))
|
|
(comp-prog-cmd
|
|
(string-replace-all
|
|
(string-replace-all
|
|
;(Cyc-compilation-environment 'cc-prog)
|
|
(get-comp-env 'cc-prog cc-prog)
|
|
"~src-file~" src-file)
|
|
"~exec-file~" exec-file))
|
|
(comp-objs-cmd
|
|
(string-replace-all
|
|
(string-replace-all
|
|
(string-replace-all
|
|
;(Cyc-compilation-environment 'cc-exec)
|
|
(get-comp-env 'cc-exec cc-exec)
|
|
"~exec-file~" exec-file)
|
|
"~obj-files~" objs-str)
|
|
"~exec-file~" exec-file)))
|
|
;(write `(DEBUG all imports ,lib-deps objs ,objs-str))
|
|
;(write `(DEBUG ,(lib:get-all-import-deps (cdar in-prog))))
|
|
(cond
|
|
(cc?
|
|
(if (equal? 0 (system comp-prog-cmd))
|
|
(system comp-objs-cmd)))
|
|
(else
|
|
(display comp-prog-cmd)
|
|
(newline)
|
|
(display comp-objs-cmd)
|
|
(newline)))))
|
|
(else
|
|
;; Emit .meta file
|
|
(with-output-to-file
|
|
meta-file
|
|
(lambda ()
|
|
(display ";; This file was automatically generated by the Cyclone Scheme compiler")
|
|
(newline)
|
|
(write (macro:get-defined-macros))))
|
|
;; Compile library
|
|
(let ((comp-lib-cmd
|
|
(string-replace-all
|
|
(string-replace-all
|
|
(get-comp-env 'cc-lib cc-lib)
|
|
"~src-file~" src-file)
|
|
"~exec-file~" exec-file))
|
|
(comp-so-cmd
|
|
(string-replace-all
|
|
(string-replace-all
|
|
(get-comp-env 'cc-so cc-so)
|
|
"~src-file~" src-file)
|
|
"~exec-file~" exec-file))
|
|
)
|
|
(cond
|
|
(cc?
|
|
(system comp-lib-cmd)
|
|
(system comp-so-cmd)
|
|
)
|
|
(else
|
|
(display comp-lib-cmd)
|
|
(newline)
|
|
(display comp-so-cmd)
|
|
(newline))))))))
|
|
|
|
;; Collect values for the given command line arguments and option.
|
|
;; Will return a list of values for the option.
|
|
;; For example:
|
|
;; ("-a" "1" "2") ==> ("1")
|
|
;; ("-a" "1" -a "2") ==> ("1" "2")
|
|
(define (collect-opt-values args opt)
|
|
(cdr
|
|
(foldl
|
|
(lambda (arg accum)
|
|
(cond
|
|
((equal? arg opt)
|
|
(cons opt (cdr accum)))
|
|
((car accum) ;; we are at an opt value
|
|
(cons #f (cons arg (cdr accum))))
|
|
(else
|
|
(cons #f (cdr accum)))))
|
|
(list #f)
|
|
args)))
|
|
|
|
;; Handle command line arguments
|
|
(let* ((args (command-line-arguments)) ;; TODO: port (command-line-arguments) to husk??
|
|
(non-opts
|
|
(if (null? args)
|
|
'()
|
|
(list (car (reverse args)))))
|
|
; (filter
|
|
; (lambda (arg)
|
|
; (not (and (> (string-length arg) 1)
|
|
; (equal? #\- (string-ref arg 0)))))
|
|
; args))
|
|
(compile? #t)
|
|
(cc-prog (apply string-append (collect-opt-values args "-CP")))
|
|
(cc-exec (apply string-append (collect-opt-values args "-CE")))
|
|
(cc-lib (apply string-append (collect-opt-values args "-CL")))
|
|
(cc-so (apply string-append (collect-opt-values args "-CS")))
|
|
(append-dirs (collect-opt-values args "-A"))
|
|
(prepend-dirs (collect-opt-values args "-I")))
|
|
;; Set optimization level(s)
|
|
(if (member "-O0" args)
|
|
(set! *optimization-level* 0))
|
|
;; TODO: place more optimization reading here as necessary
|
|
;; End optimizations
|
|
(if (member "-t" args)
|
|
(set! *trace-level* 4)) ;; Show all trace output
|
|
(if (member "-d" args)
|
|
(set! compile? #f)) ;; Debug, do not run GCC
|
|
(cond
|
|
((or (member "-h" args)
|
|
(member "--help" args))
|
|
(display "
|
|
-A directory Append directory to the list of directories that are searched
|
|
in order to locate imported libraries.
|
|
-I directory Prepend directory to the list of directories that are searched
|
|
in order to locate imported libraries.
|
|
-CP cc-commands Specify a custom command line for the C compiler to compile
|
|
a program module. See Makefile.config for an example of how
|
|
to construct such a command line.
|
|
-CE cc-commands Specify a custom command line for the C compiler to compile
|
|
an executable.
|
|
-CL cc-commands Specify a custom command line for the C compiler to compile
|
|
a library module.
|
|
-CS cc-commands Specify a custom command line for the C compiler to compile
|
|
a shared object module.
|
|
-Ox Optimization level, higher means more optimizations will
|
|
be used. Set to 0 to disable optimizations.
|
|
-d Only generate intermediate C files, do not compile them
|
|
-t Show intermediate trace output in generated C files
|
|
-h, --help Display usage information
|
|
-v Display version information
|
|
")
|
|
(newline))
|
|
((member "-v" args)
|
|
(display *version-banner*))
|
|
((member "--autogen" args)
|
|
(autogen "autogen.out")
|
|
(newline))
|
|
((member "-v" args)
|
|
(display *version-banner*))
|
|
((member "--autogen" args)
|
|
(autogen "autogen.out"))
|
|
((or (< (length args) 1)
|
|
(null? non-opts))
|
|
(display "cyclone: no input file")
|
|
(newline))
|
|
(else
|
|
(run-compiler non-opts compile? cc-prog cc-exec cc-lib cc-so append-dirs prepend-dirs))))
|
|
|