Files
sex/semen.scm
2026-09-29 21:07:16 +03:00

690 lines
25 KiB
Scheme

;;; Sex semantic engine
(import
scheme
(chicken base)
(chicken keyword)
(chicken string)
(chicken module)
fmt
sex-macros
sex-modules
types
matchable ; pattern matching
srfi-1 ; list routines
srfi-69 ; hash tables
utils
)
(export/rename (process semen-process))
(export closure-env-declaration)
;;; for lambda extraction, docstring processing, macro expansion,
;;; injection of module headers, i.e. all things that rearrange code
;;; structurally, add or remove forms
;;;
;;; The algorithm: feed toplevel forms to appropriate handlers, then
;;; append their return to the resulting list. Each handler can return
;;; multiple forms, e.g. lambdas collected from a function may result
;;; in auxiliary structures and functions.
(define (process raw-sex-forms)
(process-rec raw-sex-forms (list)))
(define (process-rec forms acc)
(cond
((null? forms) (reverse acc))
((macro? (car forms))
(process-rec
(macroexpand (car forms) (cdr forms))
acc))
(else
;; A closure type's struct is emitted before the toplevel form that
;; first mentioned it, which is why the new forms are lifted off and
;; the structs slide underneath them
(let* ((processed (match-sex-form (car forms) acc))
(new (take-until processed acc)))
(process-rec (cdr forms)
(append new (flush-closure-structs!) acc))))))
(define (take-until forms tail)
(if (eq? forms tail)
(list)
(cons (car forms) (take-until (cdr forms) tail))))
(define (macroexpand macro-form rest-forms)
;; We want to replace macro with its expansion. The problem is,
;; top-level macro can return either a single form, or a list of
;; forms, when it for example generates some aux
;; structures/functions/typedefs.
;;
;; Single form we just cons to the top of rest-forms, but multiple
;; forms have to be appended to the rest-forms.
(let ((res (apply-macro macro-form))
(src (form-source macro-form)))
;; An expansion is fresh structure with no location of its own. Give
;; it the call site's, the way cpp attributes a macro body to where
;; the macro was used
(if (list? (car res))
(append (map (lambda (f) (stamp-form-source! f src)) res)
rest-forms)
(cons (stamp-form-source! res src) rest-forms))))
(define (match-sex-form sex-form acc)
(match sex-form
((or ('fn . _)
('pub 'fn . _)
('extern 'fn . _)) (process-fn sex-form acc))
((or ('struct . _)
('pub 'struct . _)) (process-struct sex-form acc))
((or ('union . _)
('pub 'union . _)) (process-struct sex-form acc))
((or ('enum . _)
('pub 'enum . _)) (process-struct sex-form acc))
((or ('var . _)
('pub 'var . _)
('extern 'var . _)) (process-global-var sex-form acc))
(('include . includes) (process-includes sex-form includes acc))
((or ('define name . _)
('pub 'define name . _))
(add-define name sex-form)
(cons sex-form acc))
(('comment . _) (cons sex-form acc))
(('import . modules)
(process-imports (get-modules-public-forms modules) acc))
((or ('defmacro . rest)
('pub 'defmacro . rest)) (defmacro rest) acc)
((or ('typedef new-type target)
('pub 'typedef new-type target))
(add-typedef new-type sex-form)
(process-typedef sex-form new-type target acc))
(else (sex-error sex-form "unknown top level form" sex-form))))
(define (process-includes sex-form includes acc)
;; consume (include ...) form and add to acc
;; (include <inc>) for each include
(let process ((includes includes)
(acc acc))
(if (null? includes)
acc
(process (cdr includes)
(cons (copy-form-source! sex-form `(include ,(car includes)))
acc)))))
(define (process-imports module-public-forms acc)
;; consume (import ...) form and process imports so
;; data types end up in types db
(fold match-sex-form acc module-public-forms))
(define (macro-expand form)
"Walk the form recursively and expand all macros, until none is left."
(walk-form
form
(lambda (subform env)
(if (macro? subform)
(cons walk-embed-result (macroexpand subform (list)))
subform))
#f))
;;; walk-form and friends: form walker with various abilities.
;;; By default, replaces walked form with walk-fn result But may
;;; perform additional operations depending of what the walk function
;;; has requested.
;;; For inspiration, see SBCL's walk.lisp and their template
;;; system.
(define walk-embed-result (gensym)
;; For cases when result is a list which must be embedded in the
;; form, e.g. when it returned from a macro
)
(define (walk-form form walk-fn env)
(if (atom? form) form
(let ((new-form (walk-fn form env)))
(cond ((not (eq? form new-form))
(walk-form new-form walk-fn env))
(else
(let ((new-car (walk-form (car new-form) walk-fn env))
(new-cdr (walk-form (cdr new-form) walk-fn env)))
(cond ((and (pair? new-car)
(eq? (car new-car) walk-embed-result))
(append (cdr new-car) new-cdr))
(else
(recons new-form new-car new-cdr)))))))))
;;; Typdef
(define (process-typedef form new-type target acc)
(cons (copy-form-source! form `(typedef ,target ,new-type)) acc))
;;; Fn processing
;;;
;;; A string as the first body form is a docstring. In the generated
;;; C code it will be placed as a C commentary just before the function
;;; definition (actually that works for all blocky things: enum, struct, union as well).
(define (fn-header-length fn-form)
(if (memq (first fn-form) '(pub extern)) 5 4))
(define (fn-core form)
;; The (fn name args rettype . body) list, without pub/extern
(if (memq (first form) '(pub extern))
(cdr form)
form))
(define (take-leading-docstring forms)
;; If FORMS starts with a string, possibly after comment forms, return
;; that string and FORMS without it. Otherwise #f and FORMS unchanged
(let loop ((fs forms) (prefix (list)))
(match fs
(() (values #f forms))
(((and cmt ('comment . _)) . rest)
(loop rest (cons cmt prefix)))
(((? string? doc) . rest)
(values doc (append (reverse prefix) rest)))
(_ (values #f forms)))))
(define (extract-fn-docstring fn-form)
(let ((lift
(lambda (proto body)
(let-values (((doc rest) (take-leading-docstring body)))
(if doc
(values doc (copy-form-source! fn-form (append proto rest)))
(values #f fn-form))))))
(match fn-form
(('pub 'fn name args ret . body)
(lift `(pub fn ,name ,args ,ret) body))
(('extern 'fn name args ret . body)
(lift `(extern fn ,name ,args ,ret) body))
(('fn name args ret . body)
(lift `(fn ,name ,args ,ret) body))
(_ (values #f fn-form)))))
(define (extract-aggregate-docstring form)
;; A string immediately after the name is the docstring; comments
;; between name and fields are not skipped, they already confuse the
;; writer
(match form
(('pub (and kind (or 'struct 'union 'enum))
(? symbol? name) (? string? doc) . rest)
(values doc (copy-form-source! form `(pub ,kind ,name ,@rest))))
(((and kind (or 'struct 'union 'enum))
(? symbol? name) (? string? doc) . rest)
(values doc (copy-form-source! form `(,kind ,name ,@rest))))
(_ (values #f form))))
(define (with-docstring doc form acc)
;; acc is newest-first; FORM is consed last so the final reverse
;; emits the comment immediately before the declaration
(cons form
(if doc
(cons (list 'comment doc) acc)
acc)))
(define (strip-fn-header-comments fn-form)
;; ([pub|extern] fn name arglist rettype). Comments in the body are
;; left in place as ordinary statements and preserved into the
;; generated C.
(strip-header-comments fn-form (fn-header-length fn-form)))
(define (process-fn sex-fn-raw acc)
(let-values (((doc sex-fn)
(extract-fn-docstring (strip-fn-header-comments sex-fn-raw))))
(let* ((expanded (macro-expand sex-fn))
(env (make-hash-table))
(lifted
(walk-form
expanded
fn-walker
(begin
(set! (hash-table-ref env :fn-name) (sex-fn-name expanded))
(set! (hash-table-ref env :lambda-counter) 0)
(set! (hash-table-ref env :lambda-aux-code) (list))
(set! (hash-table-ref env :var-types)
(declared-types (sex-fn-arglist expanded)))
env)))
(processed (rewrite-closure-calls-in-body lifted env)))
(with-docstring doc processed
(append (hash-table-ref env :lambda-aux-code) acc)))))
(define (declared-types arglist)
(let ((types (make-hash-table)))
(for-each (lambda (param)
(when (and (pair? param) (pair? (cdr param)))
(hash-table-set! types (first param) (second param))))
arglist)
types))
(define (aux-name! env make)
(let ((counter (hash-table-ref env :lambda-counter)))
(set! (hash-table-ref env :lambda-counter) (+ counter 1))
(make (hash-table-ref env :fn-name) counter)))
(define (add-aux-code! env forms)
(set! (hash-table-ref env :lambda-aux-code)
(append forms (hash-table-ref env :lambda-aux-code))))
(define (fn-walker form env)
(let ((head (car form)))
(cond
((eq? 'lambda head)
(let ((name (aux-name! env make-lambda-name)))
(add-aux-code! env (lift-lambda name form))
name))
;; a type, not an expression -- becomes the struct for its signature
((closure-type? form)
(copy-form-source! form `(struct ,(register-closure-type! form form))))
((eq? 'closure head)
(let ((base (aux-name! env make-closure-name)))
(let-values (((construct forms) (lift-closure base form env)))
(add-aux-code! env (fold match-sex-form (list) forms))
(copy-form-source! form `(,construct ,@(fourth form))))))
;; every binding site is written, so tracking the declarations is
;; enough to know a receiver's type without inference
((and (eq? 'var head) (>= (length form) 3))
(hash-table-set! (hash-table-ref env :var-types) (second form) (third form))
form)
(else form))))
(define (rewrite-closure-calls-in-body fn-form env)
(let ((header (fn-header-length fn-form)))
(append (take fn-form header)
(map (lambda (form) (rewrite-closure-calls form env))
(drop fn-form header)))))
(define (rewrite-closure-calls form env)
(if (not (list? form))
form
(let ((type (and (pair? form) (receiver-closure-type (car form) env)))
(rewrite (lambda (sub) (rewrite-closure-calls sub env))))
(copy-form-source!
form
(if type
`(,(register-closure-call! type form)
,(rewrite (car form))
,@(map rewrite (cdr form)))
(map rewrite form))))))
;;; A closure type has two spellings: `(closure ...)' as written, and
;;; `(struct ƛ...)' once resolved -- which is what a struct
;;; field holds, since the type database is populated after resolution.
;;; Both name the same thing, so a receiver is recognised either way.
(define (as-closure-type type)
(cond
((closure-type? type) type)
((and (list? type) (= 2 (length type)) (eq? 'struct (car type)))
(hash-table-ref/default +closure-structs+ (second type) #f))
(else #f)))
(define (receiver-closure-type expr env)
(as-closure-type (expression-type expr env)))
;;; The type of an lvalue path, from known declarations -- a name, and
;;; what can be reached from one by subscripting, dereferencing and
;;; member access.
(define (expression-type expr env)
(cond
((symbol? expr)
(hash-table-ref/default (hash-table-ref env :var-types) expr #f))
((not (and (list? expr) (>= (length expr) 2))) #f)
(else
(case (car expr)
((¤) (let ((base (expression-type (second expr) env)))
(and (list? base) (>= (length base) 2) (eq? '¤ (car base))
(second base))))
((*) (and (= 2 (length expr))
(let ((base (expression-type (second expr) env)))
(and (list? base) (= 2 (length base)) (eq? '* (car base))
(second base)))))
((dot-access) (member-path-type (expression-type (second expr) env)
(cddr expr)))
((->) (let ((base (expression-type (second expr) env)))
(and (list? base) (= 2 (length base)) (eq? '* (car base))
(member-path-type (second base) (cddr expr)))))
(else #f)))))
(define (member-path-type type fields)
(if (null? fields)
type
(member-path-type (field-type type (car fields)) (cdr fields))))
(define (field-type type field)
(let ((name (cond ((symbol? type) type)
((and (list? type)
(= 2 (length type))
(memq (car type) '(struct union)))
(second type))
(else #f))))
(and name
(let* ((fields (get-fields name))
(entry (and fields (assq field fields))))
(and entry (second entry))))))
(define (make-lambda-name enclosing-fn-name counter)
(string->symbol
(fmt #f "λ" counter "_" enclosing-fn-name)))
(define (lift-lambda name form)
(match form
(('lambda arglist ret-type . body)
(process-fn (copy-form-source! form `(fn ,name ,arglist ,ret-type ,@body))
(list)))
(else (sex-error form "malformed lambda" form))))
;;; Closures
;;;
;;; A closure is a function pointer and an inline environment, so the
;;; value owns its captures and nothing is allocated. The type
;;; `(closure ((int)) int)' becomes one struct per signature, shared by
;;; every closure with that signature. The captures live in `env' as a record
;;; only the lifted body knows the shape of, which is why `env' is
;;; max_align_t rather than char -- it has to be aligned for whatever
;;; ends up in it.
;;;
;;; The expression becomes three hoisted definitions -- the capture
;;; struct, the lifted body, and a constructor -- and is replaced by a
;;; call to the constructor, so the captures are evaluated as ordinary
;;; arguments at the point the closure is written.
;;; How much of a closure is environment, in bytes. Counted in bytes
;;; so a closure that fits where it was written will also fit
;;; elsewhere. Captures are only by value, and never allocated on
;;; heap. Anything that is more than 16 bytes should be stored as a
;;; pointer, and memory management is entirely up to caller
(define +closure-env-bytes+ 16)
;;; +closure-env-bytes+ for maximum capacity, max-align-t for
;;; effectiveness, hence union
(define +closure-env-type+ 'ƛenv)
(define (closure-env-declaration)
`(union ,+closure-env-type+ ((align max-align-t)
(bytes (¤ char ,+closure-env-bytes+)))))
(define +closure-structs+ (make-hash-table))
(define +closure-forwards+ (make-hash-table))
(define *pending-closure-structs* (list))
(define (closure-type? form)
(and (pair? form)
(eq? 'closure (car form))
(= 3 (length form))))
;;; A type spelling becomes an identifier deterministically, so two
;;; translation units have the same signatures for the same closure
;;; types: (* const char) -> p_const_char, (closure ((int)) int) ->
;;; closure_int_int.
(define (mangle-type type)
(cond
((symbol? type) (mangle-word (symbol->string type)))
((number? type) (number->string type))
((null? type) "void")
((pair? type) (string-intersperse (map mangle-type (mangle-head type)) "_"))
(else (sex-error type "cannot mangle type" type))))
(define (mangle-head type)
(case (car type)
((*) (cons 'p (cdr type)))
((¤) (cons 'a (cdr type)))
(else type)))
(define (mangle-word word)
(list->string
(map (lambda (c)
(if (or (char-alphabetic? c) (char-numeric? c)) c #\_))
(string->list word))))
(define (aggregates-in type)
(cond
((not (list? type)) (list))
((and (= 2 (length type))
(memq (car type) '(struct union))
(symbol? (second type)))
(list type))
(else (append-map aggregates-in type))))
;;; Extract aggregate types from the closure's signature to forward
;;; declare them before the closure, so they can be referenced in the
;;; closure. Particularly useful for complex cases like fixed point
;;; combinator, etc.
(define (forward-declare-aggregates! type src-form)
(for-each
(lambda (aggregate)
(let ((name (second aggregate)))
(unless (or (hash-table-exists? +closure-forwards+ name)
(get-tag-info name))
(hash-table-set! +closure-forwards+ name #t)
(set! *pending-closure-structs*
(cons (copy-form-source! src-form aggregate)
*pending-closure-structs*)))))
(delete-duplicates (aggregates-in type))))
(define (closure-struct-name type)
;; the glyph says `closure' already, so the tag is just the signature
(string->symbol (string-append "ƛ"
(mangle-type (second type))
"_"
(mangle-type (third type)))))
;;; The code pointer takes the environment first; everything else is
;;; the closure's own signature.
(define (closure-code-type type)
`(fn (((* void)) ,@(second type)) ,(third type)))
;;; Emitted once per signature, before the toplevel form that first
;;; needed it.
(define (register-closure-type! type src-form)
(let ((name (closure-struct-name type)))
(unless (hash-table-exists? +closure-structs+ name)
(forward-declare-aggregates! type src-form)
(hash-table-set! +closure-structs+ name type)
(let ((form (copy-form-source!
src-form
`(struct ,name ((code ,(closure-code-type type))
(env (union ,+closure-env-type+)))))))
(register-aggregate! form)
(set! *pending-closure-structs*
(cons form *pending-closure-structs*))))
name))
;;; Every closure type in FORM becomes the struct for its signature,
;;; registering it on the way. The walker does this for function bodies
;;; and headers; globals come through here instead.
(define (resolve-closure-types form)
(cond
((closure-type? form)
(copy-form-source! form `(struct ,(register-closure-type! form form))))
((list? form)
(copy-form-source! form (map resolve-closure-types form)))
(else form)))
(define +closure-calls+ (make-hash-table))
;;; The helper a closure call is routed through: `(f 1)' becomes
;;; `ƛint_int_call(f, 1)', which unpacks the receiver into
;;; `ƛc.code(&ƛc.env, ƛa0)' inside. The receiver arrives as an argument,
;;; so it is evaluated once: unpacked at the call site instead,
;;; `([table (++ i)] 10)' would read
;;; `table[++i].code(&table[++i].env, 10)' and bump `i' twice.
(define (register-closure-call! type src-form)
(let* ((closure (closure-struct-name type))
(helper (suffixed closure "_call"))
(returns (third type))
(params (map (lambda (arg index)
(list (string->symbol (fmt #f "ƛa" index))
(unwrap-type arg)))
(second type)
(iota (length (second type))))))
(unless (hash-table-ref/default +closure-calls+ helper #f)
(hash-table-set! +closure-calls+ helper #t)
(let ((call `((dot-access ƛc code)
(& (dot-access ƛc env))
,@(map first params))))
(set! *pending-closure-structs*
(cons (copy-form-source!
src-form
`(fn ,helper ((ƛc (struct ,closure)) ,@params) ,returns
,(if (eq? 'void returns) call `(return ,call))))
*pending-closure-structs*))))
helper))
;;; An argument type is written wrapped: `(int)' in `((int) (float))'
(define (unwrap-type type)
(if (and (list? type) (= 1 (length type)))
(car type)
type))
(define (flush-closure-structs!)
(let ((pending *pending-closure-structs*))
(set! *pending-closure-structs* (list))
pending))
;;; Lowering
(define (make-closure-name enclosing-fn-name counter)
(string->symbol (fmt #f "ƛ" counter "_" enclosing-fn-name)))
(define (suffixed name suffix)
(string->symbol (string-append (symbol->string name) suffix)))
;;; A capture is a plain name, whose type comes from the declarations
;;; the walker has passed. `(name expr)' captures want the type of an
;;; expression, which is inference, and wait for it.
(define (capture-binding capture form env)
(unless (symbol? capture)
(sex-error form "a closure capture must be a plain name for now" capture))
(let ((type (hash-table-ref/default (hash-table-ref env :var-types) capture #f)))
(unless type
(sex-error form "closure captures an undeclared name" capture))
(list capture type)))
(define (lift-closure base form env)
(match form
(('closure arglist ret-type captures . body)
(let* ((caps (map (lambda (c) (capture-binding c form env)) captures))
(record (suffixed base "_captures"))
(code (suffixed base "_code"))
(construct (suffixed base "_make"))
(type `(closure ,(map (lambda (p) (list (second p))) arglist)
,ret-type))
(closure (register-closure-type! type form)))
(values
construct
(map (lambda (f) (copy-form-source! form f))
;; C has no empty struct, and a closure over nothing needs
;; no record to point at
(append
(if (null? caps)
(list)
(list `(struct ,record ,caps)))
(list
`(fn ,code ((ƛe (* void)) ,@arglist) ,ret-type
,@(if (null? caps)
(list)
`((var ƛcaptures (* (struct ,record)) ƛe)
,@(map (lambda (cap)
`(var ,(first cap) ,(second cap)
(-> ƛcaptures ,(first cap))))
caps)))
,@body)
`(fn ,construct ,caps (struct ,closure)
(var ƛc (struct ,closure))
,@(if (null? caps)
(list)
`((static-assert
(<= (sizeof (struct ,record))
(sizeof (dot-access ƛc env)))
"closure captures do not fit the inline environment")))
(= (dot-access ƛc code) ,code)
,@(if (null? caps)
(list)
`((var ƛcaptures (* (struct ,record))
(cast (& (dot-access ƛc env)) (* (struct ,record))))
,@(map (lambda (cap)
`(= (-> ƛcaptures ,(first cap)) ,(first cap)))
caps)))
(return ƛc))))))))
(else (sex-error form "malformed closure" form))))
;;; Structs
;;; Record the named structs, unions and enums in the type database
(define (process-struct sex-struct acc)
(let-values (((doc form) (extract-aggregate-docstring sex-struct)))
;; Resolve before registering: a field of closure type has to reach
;; the type database as the struct it becomes, or member access
;; through it finds nothing
(let ((form (resolve-closure-types form)))
(register-aggregate! form)
(with-docstring doc form acc))))
(define (register-aggregate! form)
(let* ((f (if (eq? (car form) 'pub) (cdr form) form))
(name (and (pair? (cdr f)) (symbol? (cadr f)) (cadr f))))
;; An anonymous aggregate has a field list where the name would be,
;; and nothing can refer to it by name anyway
(when name
(case (car f)
((struct) (add-struct name form))
((union) (add-union name form))
((enum) (add-enum name form))))))
(define (process-global-var sex-var acc)
;; A global is not walked for lambdas, but its type still has to stop
;; saying `closure' before the writer sees it
(cons (resolve-closure-types sex-var) acc))
;;; Utils
(define (non-empty-list? form)
(and (list? form)
(not (null? form))))
(define (sex-fn? form)
"The `form` must be toplevel.
Returns #f if the form is not a function, returns the form otherwise"
(match form
((or ('fn . _)
('pub 'fn . _)
('extern 'fn . _)) form)
(else #f)))
(define (sex-fn-public? fn-form)
(eq? (first fn-form) 'pub))
(define (sex-fn-name fn-form)
(assert (sex-fn? fn-form)
(fmt #f "Form " fn-form " is not a function"))
(second (fn-core fn-form)))
(define (sex-fn-arglist fn-form)
(assert (sex-fn? fn-form)
(fmt #f "Form " fn-form " is not a function"))
(third (fn-core fn-form)))
(define (sex-fn-return-type fn-form)
(assert (sex-fn? fn-form)
(fmt #f "Form " fn-form " is not a function"))
(fourth (fn-core fn-form)))
(define (sex-fn-prototype fn-form)
"Returns all except body"
(assert (sex-fn? fn-form)
(fmt #f "Form " fn-form " is not a function"))
(take fn-form (fn-header-length fn-form)))
(define (sex-fn-body fn-form)
(assert (sex-fn? fn-form)
(fmt #f "Form " fn-form " is not a function"))
(drop fn-form (fn-header-length fn-form)))