1
0
forked from alex-eg/sex

10 Commits

Author SHA1 Message Date
Pavel Kulyov
b1ab18b9af dev: require make deps manually for dev & CI 2026-09-24 01:10:59 +03:00
e0987c1836 read the feature flags in sextest in the way sexc does
sextest resolves #+ and #- itself -- it reads the program and prints
what survives to sexc -- so a flag spelling it does not recognise
decides which branch gets compiled
2026-09-23 14:35:56 +03:00
ee053e35c9 look past comments in a public form's header 2026-09-23 14:35:55 +03:00
534e9ef56b say what is wrong with a pointer type, and stop mangling fn ones
The guard against nested pointer chains searched every sublist for a
`*', including the ones that hold a type of their own.

A function type with a pointer parameter, while the same type with an int
parameter passed. (* [char 4]) was let through as well and came out
as `vector-ref char 4 * p'.

A `*' inside an array or a function type belongs to that type, so the
search stops there. What the flat conversion cannot express is now
named: a fn type is a function pointer already, and a pointer to an
array is not supported.

Parameters of a function type are walked without their names. C writes
a parameter name into a declarator and a type has none, so fmt-c prints
whatever it is handed there as a type: `(s (* char))' came out as
`char(*) s'.

fmt-c printed a nameless array declarator's #f into the C, which showed
up in these parameters.
2026-09-23 14:31:41 +03:00
381ad21d8b separate tags and typedefs
C has two namespaces and the database had one. A typedef of a tag's
name wiped it.

Tags, i.e. struct, enum and union, share one namespace; they move to a
table of their own. A name resolves the way C does: the ordinary
identifier first, the tag when that leads nowhere.

While resolving a typedef, the target was taken apart with cadr
whatever it was, so (typedef points (¤ point 4)) reported point's
fields as its own -- an array of four points claiming to be a point,
and a macro reaching through it with v->x. Only a name and
(struct|union|enum NAME) name a type now.
2026-09-23 14:31:41 +03:00
e0a228c66e make feature guard produce nothing on eof/closing bracket/paren
A dropped datum is replaced by whatever follows it, but what follows
may be the end of the file or the paren closing the list we are
in. Hand the token back to the caller instead: read-list closes its
list with it and the toplevel loop stops.

A `;' comment between a guard and the form it guards was also taken for
the guarded datum, so the form stayed unconditional and the guard did
nothing. Skip comments when reading the guard.

comment-form? was defined three times over; it moves to utils.
2026-09-23 14:31:38 +03:00
Pavel Kulyov
d0e2c699e3 semen: fix premature negative cases merge hehe 2026-09-22 23:40:16 +03:00
Pavel Kulyov
434086ade8 tests: add docstring cases 2026-09-21 22:24:47 +03:00
Pavel Kulyov
1685b0c31f semen: process fn docstrings 2026-09-21 22:24:04 +03:00
Pavel Kulyov
720bff3437 semen: add and use fn core prototype getter 2026-09-20 15:40:29 +03:00
23 changed files with 514 additions and 164 deletions

View File

@@ -57,7 +57,9 @@ jobs:
hash -r
csc -version
# Eggs are pinned in eggs.lock and installed by the Makefile into .eggs/.
- name: Install dependencies
run: make deps
- name: Build sexc
run: make && ./sexc --help

View File

@@ -34,24 +34,27 @@ DEPSLOCK = eggs.lock
EGGS_DIR := $(abspath .eggs)
# Compiler's own repository (types.db, chicken.*), ignoring ambient venv vars.
SYSTEM_CHICKEN_REPO := $(shell env -u CHICKEN_INSTALL_REPOSITORY -u CHICKEN_REPOSITORY_PATH -u CHICKEN_EGG_CACHE -u CHICKEN_INSTALL_PREFIX $(CHICKEN_INSTALL) -repository 2>/dev/null)
SYSTEM_CHICKEN_REPO := $(shell env \
-u CHICKEN_INSTALL_REPOSITORY -u CHICKEN_REPOSITORY_PATH \
-u CHICKEN_EGG_CACHE -u CHICKEN_INSTALL_PREFIX \
$(CHICKEN_INSTALL) -repository 2>/dev/null)
CHICKEN_ABI := $(notdir $(SYSTEM_CHICKEN_REPO))
EGGS_STAMP := $(EGGS_DIR)/.installed-$(CHICKEN_ABI)
export CHICKEN_EGG_CACHE := $(EGGS_DIR)/cache
export CHICKEN_INSTALL_REPOSITORY := $(EGGS_DIR)
export CHICKEN_INSTALL_PREFIX := $(EGGS_DIR)
# Try project-local chicken repository first. In case it doesn't exist (packaging for distros),
# system-wide repository will be used.
unexport CHICKEN_INSTALL_REPOSITORY
unexport CHICKEN_EGG_CACHE
unexport CHICKEN_INSTALL_PREFIX
export CHICKEN_REPOSITORY_PATH := $(EGGS_DIR):$(SYSTEM_CHICKEN_REPO)
all: sexc
sexc: $(EGGS_STAMP) $(OBJ) main.scm
sexc: $(OBJ) main.scm
$(CHICKEN_C) $(CSC_FLAGS) main.scm -o sexc-tmp -link sexc
# otherwise csc hangs, probably because it tries to compile to sexc.o first
mv sexc-tmp sexc
$(OBJ): $(EGGS_STAMP)
#------------------------------------------------------------------
utils.o: utils.module.scm utils.scm
@@ -82,15 +85,16 @@ sexc.o: sexc.module.scm types.o sexc.scm fmt-c-writer.o sex-macros.o sex-modules
$(CHICKEN_C) $(CSC_FLAGS) $(MODULE_FLAGS) sexc.module.scm -o sexc.o -unit sexc -link fmt-c-writer,sex-macros,sex-modules,reader,semen,types,utils
# Unit testing
sex-tests: $(EGGS_STAMP)
sex-tests:
$(MAKE) -C ./tests sex-tests
cp ./tests/sex-tests ./
sextest: $(EGGS_STAMP)
sextest:
$(MAKE) -C ./tools/sextest sextest
cp ./tools/sextest/sextest .
SEX_TEST_PROGRAMS = hello-world lists comments unicode serialize features
SEX_TEST_PROGRAMS = hello-world lists comments unicode serialize features \
feature-flags
# Multi-module linking is checked end to end; see tests/modules/Makefile.
check-modules: sexc
@@ -115,9 +119,11 @@ installdirs:
uninstall:
rm -f $(DESTDIR)$(bindir)/sexc
# eggs.lock is the pin file (chicken-status -list). Install from it;
# do not float versions on a normal build. Regenerating the lock:
# make deps-update
$(EGGS_STAMP) deps-update: export CHICKEN_EGG_CACHE := $(EGGS_DIR)/cache
$(EGGS_STAMP) deps-update: export CHICKEN_INSTALL_REPOSITORY := $(EGGS_DIR)
$(EGGS_STAMP) deps-update: export CHICKEN_INSTALL_PREFIX := $(EGGS_DIR)
$(EGGS_STAMP) deps-update: export CHICKEN_REPOSITORY_PATH := $(EGGS_DIR):$(SYSTEM_CHICKEN_REPO)
$(EGGS_STAMP): $(DEPSLOCK)
mkdir -p $(CHICKEN_EGG_CACHE)
$(CHICKEN_INSTALL) -force -from-list $(DEPSLOCK)
@@ -125,10 +131,10 @@ $(EGGS_STAMP): $(DEPSLOCK)
deps: $(EGGS_STAMP)
deps-update: $(DEPSFILE)
rm -rf $(EGGS_DIR)
deps-update: $(DEPSFILE) deps-clean
mkdir -p $(CHICKEN_EGG_CACHE)
$(CHICKEN_INSTALL) -force $$(cat $(DEPSFILE))
# Local repo only: a system path here would leak distro eggs into the lock.
CHICKEN_REPOSITORY_PATH=$(EGGS_DIR) $(CHICKEN_STATUS) -list | sort > $(DEPSLOCK).tmp
mv $(DEPSLOCK).tmp $(DEPSLOCK)
touch $(EGGS_STAMP)

View File

@@ -12,22 +12,25 @@ Sex is statically typed, compiled general purpose language.
First, get yourself a Chicken, then, some Chicken deps. You also will
need a C compiler.
Eggs are installed into a project-local ~.eggs/~ repository; they do not
touch the Chicken system repository.
** Compilation
** Development
#+begin_src sh
make deps
make
#+end_src
That installs pinned eggs from ~eggs.lock~ into ~.eggs/~ if needed, then
builds ~sexc~. ~dependencies.txt~ is the unpinned request list. To
refresh ~eggs.lock~ after changing it:
~make deps~ installs pinned eggs from ~eggs.lock~ into a project-local
~.eggs/~ repository. ~dependencies.txt~ is the unpinned request list.
To refresh ~eggs.lock~ after changing it:
#+begin_src sh
make deps-update
#+end_src
** Packaging for system package managers
- Depend ~sex~ package on Chicken-6 (with ~libchicken.a~) and all eggs from ~dependencies.txt~.
- Compile and install with ~make~ (usually no other arguments required).
- Move resulting ~sexc~ binary to the appropriate place.
** Static compilation
The default ~CSC_FLAGS~ include ~-static~, so ~sexc~ does not need
~.eggs~ at runtime and ~make install~ stays relocatable. Chicken must

View File

@@ -135,9 +135,6 @@ forms, and what remains."
(car type)
type))
(define (comment-form? f)
(and (pair? f) (eq? (car f) 'comment)))
;;; Strip ;s so ;;; Foo becomes /* Foo */ and not /* ;; Foo */
(define (strip-comment-marker text)
(string-trim-both (string-trim text #\;)))
@@ -279,7 +276,7 @@ forms, and what remains."
;; it's a strong semantic cue
`(%array ,(walk-type (maybe-unwrap-type array-type)))))
(('fn arglist ret-type)
`(%fun ,(walk-type ret-type) ,(walk-arglist arglist)))
`(%fun ,(walk-type ret-type) ,(walk-arg-types arglist)))
(('fn . _)
(sex-error form "malformed function type" form))
@@ -291,8 +288,13 @@ forms, and what remains."
(else
(type-convert-to-c form))))
;;; An array or a function type
(define (structured-type? form)
(and (pair? form) (memq (car form) '(¤ fn))))
(define (has-pointer-star? form)
(and (pair? form)
(not (structured-type? form))
(or (memq '* form)
(any has-pointer-star? (filter pair? form)))))
@@ -304,6 +306,10 @@ forms, and what remains."
(and (pair? type)
(any has-pointer-star? (filter pair? type))))
(define (nested-structured-type type)
(and (pair? type)
(find structured-type? (filter pair? type))))
(define (type-convert-to-c type)
;; Our pointers to C pointers
;; int -> int
@@ -311,6 +317,12 @@ forms, and what remains."
;; const * const char -> const char * const
(when (nested-pointer? type)
(sex-error type "pointer chains are written flat, as (* * T), not nested" type))
(let ((inner (nested-structured-type type)))
(when inner
(if (eq? (car inner) 'fn)
;; (fn ...) is spelled as the pointer it already is in C
(sex-error type "a fn type is a function pointer already: write (fn ...), not (* (fn ...))" type)
(sex-error type "a pointer to an array is not supported" type))))
(if (atom? type) (atom-to-fmt-c type)
(flatten
(tree-map atom-to-fmt-c
@@ -334,26 +346,39 @@ forms, and what remains."
((¤ * const volatile struct union) #t)
(else #f)))
;;; Does the parameter name itself?
;;; (f1 float) does
;;; (float), (const char) and (¤ float 4) do not
(define (named-arg? arg)
(and (pair? arg)
(pair? (cdr arg)) ; 1 element args are always type
(not (eq? (car arg) '¤))
(not (is-probably-type arg))))
;;; The type of one parameter
(define (arg-type arg)
(if (named-arg? arg)
(walk-type (maybe-unwrap-type (cdr arg)))
;; A lone type may arrive wrapped in parens of its own, and those
;; are not part of it: ((* const char))
;; Plain names e.g. (int) are left as is
(walk-type (if (and (pair? arg) (null? (cdr arg)) (pair? (car arg)))
(car arg)
arg))))
(define (walk-arglist form)
;; E.g.:
;; ((float) (int) (const char) (* const char) (¤ (* const struct res) 32))
;; ((f1 float) (f2 float) (f3 float) (res (¤ float 4)))
(map (fn
(match x
(('¤ . _) (walk-type x))
;; yeah shitty, but I don't know yet how to determine if the
;; first entry is part of the type and not an argument name
;; :(
((? is-probably-type) (walk-type x))
;; 1 element args are always type
((_) (walk-type x))
((var . type) (append (list (walk-type (maybe-unwrap-type type)))
(list (walk-type var))))))
(map (lambda (arg)
(if (named-arg? arg)
(list (arg-type arg) (walk-type (car arg)))
(arg-type arg)))
(remove comment-form? form)))
(define (walk-arg-types form)
(map arg-type (remove comment-form? form)))
(define (walk-function form)
;; (fn ret-type name arglist body) -> normal function
;; (fn ret-type name arglist) -> prototype

View File

@@ -115,6 +115,21 @@
((eq? tok dot-token) (error "Unexpected ."))
(else tok))))
;;; A token that stands for a datum
(define (datum-token? tok)
(not (or (eof-object? tok)
(eq? tok close-paren)
(eq? tok close-bracket)
(eq? tok dot-token))))
;;; next-token, sans the comments
(define (next-code-token port)
(let loop ()
(let ((tok (next-token port)))
(if (comment-form? tok)
(loop)
tok))))
;;; Read list elements up to close-paren or close-bracket,
;;; honoring dotted-pair notation (a b . c)
(define (read-list port closer)
@@ -208,13 +223,24 @@
(else (error "Malformed feature expression" test))))
;;; The #-/#+ preceded datum is always read -- there is no other way
;;; to know where it ends -- and then either returned or dropped
;;; to know where it ends -- and then either returned or dropped. What
;;; follows a dropped datum is read in its place: `#+x #+y (a) (b)'
;;; with only x is (b).
;;;
;;; That next thing may be nothing: the end of the file, or the
;;; paren closing the list we are in
(define (read-conditional port keep-when)
(let ((keep (eq? keep-when (feature-true? (read-datum port)))))
(if keep
(read-datum port)
(begin (read-datum port)
(next-token port)))))
(let* ((test (next-code-token port))
(keep (begin
(unless (datum-token? test)
(error "Unexpected end of input in feature expression"))
(eq? keep-when (feature-true? test))))
(guarded (next-code-token port)))
(cond
(keep guarded)
;; A datum was dropped, so the next one stands in for it
((datum-token? guarded) (next-token port))
(else guarded))))
;;; #t / #true / #f / #false. `val' is the boolean; consume any
;;; trailing name characters and validate

123
semen.scm
View File

@@ -140,33 +140,79 @@
(cons (copy-form-source! form `(typedef ,target ,new-type)) acc))
;;; Fn processing
(define (comment-form? f)
(and (pair? f) (eq? (car f) 'comment)))
;;;
;;; 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)
;; Remove comment forms from the function header
;; ([pub|extern] fn name arglist rettype) so the positional accessors
;; below are not shifted. Comments in the body are left in place as
;; ordinary statements and preserved into the generated C.
(let ((header-count (fn-header-length fn-form)))
;; This always rebuilds the list, so the location has to be carried
;; over explicitly -- otherwise every function loses it
(copy-form-source!
fn-form
(let loop ((form fn-form) (kept 0) (acc (list)))
(cond
((null? form) (reverse acc))
((= kept header-count) (append (reverse acc) form))
((comment-form? (car form)) (loop (cdr form) kept acc))
(else (loop (cdr form) (+ kept 1) (cons (car form) acc))))))))
;; ([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* ((sex-fn (strip-fn-header-comments sex-fn-raw))
(expanded (macro-expand sex-fn))
(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))
(processed
(walk-form
@@ -177,9 +223,8 @@
(set! (hash-table-ref env :lambda-counter) 0)
(set! (hash-table-ref env :lambda-aux-code) (list))
env))))
(cons processed
(append (hash-table-ref env :lambda-aux-code) acc))))
(with-docstring doc processed
(append (hash-table-ref env :lambda-aux-code) acc)))))
(define (fn-walker form env)
(if (eq? 'lambda (car form))
@@ -210,8 +255,9 @@
;;; Record the named structs, unions and enums in the type database
(define (process-struct sex-struct acc)
(register-aggregate! sex-struct)
(cons sex-struct acc))
(let-values (((doc form) (extract-aggregate-docstring sex-struct)))
(register-aggregate! form)
(with-docstring doc form acc)))
(define (register-aggregate! form)
(let* ((f (if (eq? (car form) 'pub) (cdr form) form))
@@ -236,33 +282,28 @@
"The `form` must be toplevel.
Returns #f if the form is not a function, returns the form otherwise"
(match form
((fn . _) form)
((pub fn . _) form)
((or ('fn . _)
('pub 'fn . _)
('extern 'fn . _)) form)
(else #f)))
(define (sex-fn-public? fn-form)
(eq? (car fn-form) 'pub))
(define (sex-fn-return-type fn-form)
(assert (sex-fn? fn-form)
(fmt #f "Form " fn-form " is not a function"))
(if (sex-fn-public? fn-form)
(third fn-form)
(second 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"))
(if (sex-fn-public? fn-form)
(fourth fn-form)
(third fn-form)))
(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"))
(if (sex-fn-public? fn-form)
(fifth fn-form)
(fourth fn-form)))
(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"

View File

@@ -737,8 +737,11 @@
(cat (c-type (cadr type) #f)
" (*" (or name "") ")("
(fmt-join (lambda (x) (c-type x #f)) (caddr type) ", ") ")"))
;; MODIFIED FROM UPSTREAM fmt-c: a nameless declarator -- an
;; array parameter of a function type, where C has no room
;; for a name -- arrives as #f, and upstream printed it
((%array)
(let ((name (cat name "[" (if (pair? (cddr type))
(let ((name (cat (or name "") "[" (if (pair? (cddr type))
(c-expr (caddr type))
"")
"]")))
@@ -773,8 +776,11 @@
(cat (c-type (cadr type) #f)
" (*" (or name "") ")("
(fmt-join (lambda (x) (c-type x #f)) (caddr type) ", ") ")"))
;; MODIFIED FROM UPSTREAM fmt-c: a nameless declarator -- an
;; array parameter of a function type, where C has no room
;; for a name -- arrives as #f, and upstream printed it
((%array)
(let ((name (cat name "[" (if (pair? (cddr type))
(let ((name (cat (or name "") "[" (if (pair? (cddr type))
(c-expr (caddr type))
"")
"]")))

View File

@@ -26,8 +26,8 @@
(import scheme
(scheme base)
(only sex-macros cat comment)
(only types get-type-info get-fields get-underlying-type
type-match map-fields))
(only types get-type-info get-tag-info get-fields
get-underlying-type type-match map-fields))
,@body)))
(define (get-macro name)

View File

@@ -72,9 +72,10 @@
(list)
raw-forms)))
(define (public-fn-interface form)
(define (public-fn-interface raw-form)
;; (pub fn name args ret . body) -> prototype, keeping a docstring so
;; the importer can emit it above the declaration
(let ((form (strip-header-comments raw-form 5)))
(match form
(('pub 'fn name args ret)
form)
@@ -83,7 +84,7 @@
(('pub 'fn name args ret (? string? doc) . _)
`(pub fn ,name ,args ,ret ,doc))
(('pub 'fn name args ret . _)
`(pub fn ,name ,args ,ret))))
`(pub fn ,name ,args ,ret)))))
;;; TODO: use semen facilities to analyze modules
(define (process-public-interface-form form acc)
@@ -92,8 +93,9 @@
;; with external linkage
(('pub 'fn . _)
(cons (copy-form-source! form (public-fn-interface form)) acc))
(('pub 'var name type . _)
(cons (copy-form-source! form `(extern var ,name ,type)) acc))
(('pub 'var . _)
(match-let ((('pub 'var name type . _) (strip-header-comments form 4)))
(cons (copy-form-source! form `(extern var ,name ,type)) acc)))
(('pub (or 'define 'defmacro 'enum 'import 'include 'struct 'typedef 'union) . _)
(cons (copy-form-source! form (cdr form)) acc))
(('pub . _)

View File

@@ -230,4 +230,33 @@ compiles."
(test-assert "grouping sublist still accepted"
(emits? (in-fn "(var s (* (const struct suc)) 0)") "const struct suc * s"))
(test-assert "flat chain still accepted"
(emits? (in-fn "(var q (* * const char) 0)") "const char * * q"))))
(emits? (in-fn "(var q (* * const char) 0)") "const char * * q")))
;; A string as the first body form (or after the name of a struct,
;; union or enum) is a docstring: it becomes a comment immediately
;; before the declaration, not a statement inside it.
(test-group "docstrings"
(test-assert "appears before the function"
(emits? "(fn greet ((name (* char))) void \"Greet NAME.\" (printf \"hi\" name))"
"/* Greet NAME. */"))
(test-assert "and not inside the body as a statement"
(not (emits? "(fn greet ((name (* char))) void \"Greet NAME.\" (printf \"hi\" name))"
"\"Greet NAME.\"")))
(test-assert "multiline keeps its paragraphs"
(emits? "(pub fn main () int \"Entry point.\n\nARGC and ARGV.\" (return 0))"
"Entry point."))
(test-assert "and the second paragraph too"
(emits? "(pub fn main () int \"Entry point.\n\nARGC and ARGV.\" (return 0))"
"ARGC and ARGV."))
(test-assert "a prototype with only a docstring stays a prototype"
(emits? "(fn helper ((a int)) int \"Forward.\")"
"helper (int a);"))
(test-assert "a string after the first statement is left alone"
(emits? "(fn f () void (g) \"not a docstring\")"
"\"not a docstring\""))
(test-assert "a struct docstring sits above the struct"
(emits? "(struct point \"A 2D point.\" ((x int) (y int)))"
"/* A 2D point. */"))
(test-assert "and an enum docstring too"
(emits? "(enum color \"RGB.\" (red green blue))"
"/* RGB. */"))))

View File

@@ -11,6 +11,9 @@
# It also checks what only a second translation unit can check: that an
# imported type reaches the type database, by expanding a macro that
# reads the imported struct's fields.
#
# The public forms carry comments in their headers, which the reduction
# to a prototype and to an extern both have to look past.
SEXC ?= ../../sexc

View File

@@ -7,9 +7,15 @@
(include stdio.h)
(pub var greet-count int 0)
(pub var greet-count ;; a comment in the header of a public form is
;; not part of it: what the importer is given has
;; to be `extern int greet-count', not a form
;; counted off by one
int 0)
(pub struct greeting ((text (* const char)) (times int)))
(pub struct greeting
"A greeting to print."
((text (* const char)) (times int)))
(pub enum mood (cheerful grumpy))
@@ -23,7 +29,9 @@
(lambda (name field-type)
`(printf "%s " ,(symbol->string name))))))
(pub fn greet ((name (* const char))) void
(pub fn greet ;; ...and here the prototype would lose its return type
((name (* const char))) void
"Print a greeting for NAME."
(++ greet-count)
(printf "hello, %s\n" name))

View File

@@ -80,6 +80,22 @@
;; guards nest
(feature-test '((a)) (x y) "#+x #+y (a)")
(feature-test '((b)) (x) "#+x #+y (a) (b)")
;; ...and the inner one may leave nothing behind: the end of the
;; file, or the paren closing the list, is what the outer one then
;; produces, and neither is an error
(feature-test '() (x) "#+x #+y (a)")
(feature-test '((f)) (x) "(f #+x #+y 1)")
(feature-test '((f 2)) (x) "(f #+x #+y 1 2)")
(feature-test '((a)) (x) "(a) #+x #+y (b)")
;; a comment between a guard and the form it guards describes the
;; guard. Taking it for the guarded datum would leave the form itself
;; unconditional
(feature-test '((a)) (x) "#+x ;; why\n (a)")
(feature-test '() (y) "#+x ;; why\n (a)")
(feature-test '((b)) (y) "#+x ;; why\n (a) (b)")
;; a comment after the guarded form is an ordinary form, and stays
(feature-test '((comment "; tail")) (y) "#+x (a) ;; tail")
;; a feature the program was not given is simply absent
(feature-test '() () "#+anything (a)")

View File

@@ -1,30 +1,35 @@
(import srfi-69
semen)
semen
types)
(define print-str-fn
'(fn void print-str ((string s))
'(fn print-str ((s string)) void
(printf "%s" s)))
(define sum-fn
'(pub fn float sum ((int a) (int b))
(return (cast float (+ a b)))))
'(pub fn sum ((a int) (b int)) float
(return (cast (+ a b) float))))
(test-group "semen"
(test-assert (sex-fn? print-str-fn))
(test #f (sex-fn-public? print-str-fn))
(test 'void (sex-fn-return-type print-str-fn))
(test 'print-str (sex-fn-name print-str-fn))
(test '((string s)) (sex-fn-arglist print-str-fn))
(test '(fn void print-str ((string s))) (sex-fn-prototype print-str-fn))
(test '((s string)) (sex-fn-arglist print-str-fn))
(test '(fn print-str ((s string)) void) (sex-fn-prototype print-str-fn))
(test '((printf "%s" s)) (sex-fn-body print-str-fn))
(test-assert (sex-fn? sum-fn))
(test #t (sex-fn-public? sum-fn))
(test 'float (sex-fn-return-type sum-fn))
(test 'sum (sex-fn-name sum-fn))
(test '((int a) (int b)) (sex-fn-arglist sum-fn))
(test '(pub fn float sum ((int a) (int b))) (sex-fn-prototype sum-fn))
(test '((return (cast float (+ a b)))) (sex-fn-body sum-fn))
(test '((a int) (b int)) (sex-fn-arglist sum-fn))
(test '(pub fn sum ((a int) (b int)) float) (sex-fn-prototype sum-fn))
(test '((return (cast (+ a b) float))) (sex-fn-body sum-fn))
(test-assert (sex-fn? '(extern fn foo () void)))
(test 'foo (sex-fn-name '(extern fn foo () void)))
(test #f (sex-fn? '(struct point ((x int)))))
(let ((sex-code
'((defmacro (sum-var name a b c)
@@ -49,9 +54,65 @@
'((defmacro (x10 a)
`(* 10 ,a))
(fn void foo ((int a) (int b))
(fn foo ((a int) (b int)) void
(return (+ a (x10 b)))))))
(test '((fn void foo ((int a) (int b))
(test '((fn foo ((a int) (b int)) void
(return (+ a (* 10 b)))))
(semen-process sex-code-macro))))
(semen-process sex-code-macro)))
;;; Docstrings are lifted out as comment forms sitting before the
;;; declaration. A string later in a body is left alone.
(test '((comment "Greet NAME.")
(fn greet ((name (* char))) void
(printf "Hello %s!\n" name)))
(semen-process
'((fn greet ((name (* char))) void
"Greet NAME."
(printf "Hello %s!\n" name)))))
(test '((comment "Public entry.")
(pub fn main () int
(return 0)))
(semen-process
'((pub fn main () int
"Public entry."
(return 0)))))
;; A prototype whose only "body" is a docstring stays a prototype
(test '((comment "Forward.")
(fn helper ((a int)) int))
(semen-process
'((fn helper ((a int)) int
"Forward."))))
(test '((fn f () void (g) "not a docstring"))
(semen-process
'((fn f () void (g) "not a docstring"))))
;; `;' comments before the string are skipped when looking for it,
;; and stay in the body
(test '((comment "Kept.")
(fn f () void (comment " note") (g)))
(semen-process
'((fn f () void (comment " note") "Kept." (g)))))
(test '((comment "A 2D point.")
(struct t-doc-pt ((x int) (y int))))
(semen-process
'((struct t-doc-pt "A 2D point." ((x int) (y int))))))
(test '((x int) (y int))
(get-fields 't-doc-pt))
(test '((comment "RGB.")
(enum t-doc-color (red green blue)))
(semen-process
'((enum t-doc-color "RGB." (red green blue)))))
(test '((comment "Either.")
(union t-doc-val ((i int) (f float))))
(semen-process
'((union t-doc-val "Either." ((i int) (f float))))))
)

View File

@@ -0,0 +1,25 @@
(compilation "-f alpha -f beta,gamma --features=delta --no-platform-features")
(input)
(output "alpha" "beta" "gamma" "delta" "elsewhere")
(return 0)
;;; The flags naming the features, in every spelling sexc takes. This
;;; is not pedantry about the command line: sextest reads the program
;;; itself and prints the surviving forms to sexc, so a spelling it
;;; does not recognise leaves the guards below resolved against the
;;; wrong set -- quietly, since the test then checks the output of a
;;; program it did not mean to compile.
;;;
;;; --no-platform-features is what makes `#-unix' true wherever this is
;;; compiled, and it has to be honoured on both sides for that to hold.
(include stdio.h)
(pub fn main () int
#+alpha (puts "alpha")
#+beta (puts "beta")
#+gamma (puts "gamma")
#+delta (puts "delta")
#-unix (puts "elsewhere")
#+unix (puts "here")
(return 0))

View File

@@ -9,6 +9,7 @@
map-fields
get-type-info
get-tag-info
get-fields
get-underlying-type)
"../types.scm")

View File

@@ -79,6 +79,38 @@
#f
(get-fields 't-u8))
;; C keeps typedefs and ordinary identifiers apart, and the canonical way
;; to declare a struct uses both names at once. Neither declaration
;; may stand on the other.
(add-struct 't-node '(struct t-node ((next (* t-node)) (v int))))
(add-typedef 't-node '(typedef t-node (struct t-node)))
(test "a typedef of a struct's own name keeps the struct reachable"
'((next (* t-node)) (v int))
(get-fields 't-node))
(test "and the tag is there under its own name"
'(struct t-node ((next (* t-node)) (v int)))
(get-tag-info 't-node))
(add-struct 't-rect '(struct t-rect ((w int) (h int))))
(add-define 't-rect '(define t-rect 3))
(test "a define of a tag's name does not hide the fields"
'((w int) (h int))
(get-fields 't-rect))
;; A type built over a struct is not that struct. Handing back the
;; element's fields would have a macro write v->x for an array
(add-typedef 't-points '(typedef t-points (¤ t-point 4)))
(test "an array of a struct has no fields of its own"
#f
(get-fields 't-points))
(add-typedef 't-point-p '(typedef t-point-p (* t-point)))
(test "nor does a pointer to one"
#f
(get-fields 't-point-p))
(add-typedef 't-cb '(typedef t-cb (fn ((t-point)) void)))
(test "nor a function type over one"
#f
(get-fields 't-cb))
;; A typedef can be written to lead back to itself. Resolving it must
;; stop rather than spin
(add-typedef 't-loop-a '(typedef t-loop-a t-loop-b))

View File

@@ -1,4 +1,5 @@
(import scheme
(scheme base) ; let-values
brev-separate
(chicken base)
(chicken file)
@@ -33,27 +34,45 @@
(cons (list) (list))
contents))
;;; --features from the (compilation ...) form, which we have to honour
;;; ourselves: the program is read here and printed back out for sexc,
;;; so #+ and #- are resolved on this side
;;; The feature flags of the (compilation ...) form, which we have to
;;; honour ourselves: the program is read here and printed back out for
;;; sexc, so #+ and #- are resolved on this side.
;;;
;;; Returns the named features and whether the host's own are in play
(define (compilation-features settings)
(let ((compilation (assoc 'compilation settings)))
(if compilation
(append-map (lambda (flag)
(if (string-prefix? "--features=" flag)
(map string->symbol
(string-split (substring flag 11) ","))
(let loop ((flags (if compilation
(string-split (cadr compilation))
(list)))
(string-split (cadr compilation)))
(list))))
(features (list))
(platform #t))
(define (add names rest)
(loop rest
(append features (map string->symbol (string-split names ",")))
platform))
(cond
((null? flags) (values features platform))
;; past `--' the flags are the C compiler's
((string=? (car flags) "--") (values features platform))
((string=? (car flags) "--no-platform-features")
(loop (cdr flags) features #f))
((and (member (car flags) '("-f" "--features")) (pair? (cdr flags)))
(add (cadr flags) (cddr flags)))
((string-prefix? "--features=" (car flags))
(add (substring (car flags) 11) (cdr flags)))
((string-prefix? "-f" (car flags))
(add (substring (car flags) 2) (cdr flags)))
(else (loop (cdr flags) features platform))))))
(define (process-file target-path)
(let* ((first-pass (split-settings (read-raw-forms target-path)))
(features (compilation-features (car first-pass))))
(if (null? features)
(let ((first-pass (split-settings (read-raw-forms target-path))))
(let-values (((features platform?) (compilation-features (car first-pass))))
(if (and (null? features) platform?)
first-pass
(parameterize ((current-features (append (platform-features) features)))
(split-settings (read-raw-forms target-path))))))
(parameterize ((current-features
(append (if platform? (platform-features) (list))
features)))
(split-settings (read-raw-forms target-path)))))))
(define (compile src compilation sexc)
(let ((compiler (or

View File

@@ -2,6 +2,8 @@
(get-env-var
set-working-directory
to-absolute-pathname
comment-form?
strip-header-comments
list-split
list-join
recons

View File

@@ -9,6 +9,7 @@
map-fields
get-type-info
get-tag-info
get-fields
get-underlying-type)
"types.scm")

View File

@@ -15,7 +15,11 @@
srfi-1
srfi-69)
(define +type-db+ (make-hash-table))
;;; Two namespaces: `struct point' and a `point' typedef are separate
;;; declarations. Tags -- struct, union and enum alike -- share the
;;; second table between them
(define +type-db+ (make-hash-table)) ; typedefs and defines
(define +tag-db+ (make-hash-table)) ; struct, union and enums
(define (strip-pub form)
(if (eq? (car form) 'pub) (cdr form) form))
@@ -44,17 +48,17 @@
(list))))
(define (add-struct name form)
(hash-table-set! +type-db+ name
(hash-table-set! +tag-db+ name
(list 'struct name (normalize-fields (aggregate-fields form)))))
(define (add-union name form)
(hash-table-set! +type-db+ name
(hash-table-set! +tag-db+ name
(list 'union name (normalize-fields (aggregate-fields form)))))
;;; ([pub] enum name (value ...))
(define (add-enum name form)
(let ((f (strip-pub form)))
(hash-table-set! +type-db+ name
(hash-table-set! +tag-db+ name
(list 'enum name (if (and (pair? (cddr f)) (list? (caddr f)))
(caddr f)
(list))))))
@@ -70,8 +74,14 @@
(hash-table-set! +type-db+ name
(list 'define name (cddr (strip-pub form)))))
(define (get-tag-info name)
(hash-table-ref/default +tag-db+ name #f))
;;; First look up the ordinary identifier, then tag of that id when
;;; no ordinary one was declared, as C does
(define (get-type-info name)
(hash-table-ref/default +type-db+ name #f))
(or (hash-table-ref/default +type-db+ name #f)
(get-tag-info name)))
;;; ((name type) ...) for a struct or union, #f for anything else --
;;; including a name that was never declared. Callers give the better
@@ -100,19 +110,31 @@
;;; The declaration NAME ultimately names. For a typedef that is the
;;; entry of whatever it stands for, and for anything else it is
;;; NAME's own info.
(define (resolve-type-info name)
(let ((info (get-type-info name)))
(and info
(if (eq? (car info) 'typedef)
(let* ((target (get-underlying-type name))
(tag (cond ((symbol? target) target)
;;; NAME's own info
(define (target-tag target)
(cond
((symbol? target) target)
((and (pair? target)
(memq (car target) '(struct union enum))
(pair? (cdr target))
(symbol? (cadr target)))
(cadr target))
(else #f))))
(and tag (get-type-info tag)))
(else #f)))
(define (resolve-type-info name)
(let ((info (resolve-ordinary-type-info name)))
(if (and info (memq (car info) '(struct union enum)))
info
(or (get-tag-info name) info))))
(define (resolve-ordinary-type-info name)
(let ((info (get-type-info name)))
(and info
(if (eq? (car info) 'typedef)
(let ((tag (target-tag (get-underlying-type name))))
;; A typedef target is written in type position, where
;; `struct point' means the tag
(and tag (or (get-tag-info tag) (get-type-info tag))))
info))))
;;; Type matcher macro

View File

@@ -2,6 +2,8 @@
(get-env-var
set-working-directory
to-absolute-pathname
comment-form?
strip-header-comments
list-split
list-join
recons

View File

@@ -42,6 +42,24 @@
(current-directory)
pathname)))
(define (comment-form? form)
(and (pair? form) (eq? (car form) 'comment)))
;;; Remove the comment forms from the first COUNT elements of FORM --
;;; its header -- so that the positional accessors reading it are not
;;; shifted by one
(define (strip-header-comments form count)
;; This always rebuilds the list, so the location has to be carried
;; over explicitly -- otherwise every form loses it
(copy-form-source!
form
(let loop ((rest form) (kept 0) (acc (list)))
(cond
((null? rest) (reverse acc))
((= kept count) (append (reverse acc) rest))
((comment-form? (car rest)) (loop (cdr rest) kept acc))
(else (loop (cdr rest) (+ kept 1) (cons (car rest) acc)))))))
(define (list-split src-list split-elt)
;; split '(1 2 / 3 4 / 5 6) by '/ -> '((1 2) (3 4) (5 6))
(fold (lambda (elt acc)