33 Commits

Author SHA1 Message Date
a860da6d7e show the cases the comments were describing
All checks were successful
Sex CI / build-linux (pull_request) Successful in 5m18s
2026-09-30 23:51:55 +03:00
fe72a109bf read a var's type as a type, not as a call
`walk-parts' walked the whole `var' form, and a `fn' type's parameter
list is shaped like a call: `(fn ((c int)) int)' came out as
`int (*fp)(c int)'. A closure literal had no type, so calling one in
place missed the rewrite; sextest fed sexc symbols Sex cannot read.
2026-09-30 23:51:55 +03:00
e87463be87 type the operators the walk had no rule for
`&&', the bitwise operators, the shifts and `++' each stopped with
`cannot infer'; an array operand came back as the array; a rank tie
went to whichever operand came first; and a toplevel `_' reached the
writer unsolved.
2026-09-30 23:51:55 +03:00
f5eee71eb7 declare the closure environment in c99
`max_align_t' is C11 and the union goes into every unit, so a program
with no closure in it stopped building under -std=c99. `unify' also
bound a rigid variable one way round only.
2026-09-30 23:51:55 +03:00
024e97553b keep closure signatures apart when mangled
The argument list was flattened with one separator throughout, so
`((long long))' and `((long) (long))' named one struct. A capture
borrowing a name looked only at the local scope chain.
2026-09-30 23:51:55 +03:00
578feb6b73 scope and promote the way C does
A `while' or `switch' body is a block with no `do' around it, and its
declarations landed in the enclosing frame. Two operands of one narrow
type skipped the conversions, so `(+ c c)' answered `char'.
2026-09-30 23:51:55 +03:00
2da5b5005b give fmt-c a name for every parameter
fmt-c takes a parameter's name with `cadr', so an unnamed one handed
over bare lost its second word to it: `(* const char)' dropped its
star, and `(int)' had no second word at all.
2026-09-30 23:51:55 +03:00
2b73fcf1c4 answer where a written type ends in one place
An arglist entry, an array's bound and its element type ask one
question, and disagreed: `(unsigned int)' was a name plus a type, a
trailing typedef a bound, a subscript the type's second word.
2026-09-30 23:51:55 +03:00
9daaa42d6a ignore the attic directory
All checks were successful
Sex CI / build-linux (pull_request) Successful in 5m12s
2026-09-30 00:10:06 +03:00
1f445f0f9b implement type inference
Two things out of one mechanism. `_' as a type means "work it out from
the initializer", so (var n _ (strlen s)) stops needing size-t spelled
out; `type-of' hands a macro the type of an expression, so a macro can
dispatch on what it was handed rather than on what was declared. Both
read the same answers from two sides.

Algorithm W's core, intra-procedural, with the extensions C forces:

  - an unknown type, since (include stdio.h) brings in names we never
    parsed. Unification is consistency rather than equality, so
    anything touching an unparsed declaration stops constraining
    instead of rejecting a program that compiled yesterday;
  - the usual arithmetic conversions, since `+' is not a function of
    one type;
  - checking mode for initializers, since #(0 0) has no type of its own
    and takes one from its context. #(T : ...) is the way out of that.

What it wanted on the way:

  - what type a *name* has, which neither the typedef nor the tag
    database recorded. One table serves functions and variables, since
    a function type already has a surface spelling;
  - a scope chain, so a (var c int 9) inside a do ends with the block;
  - form-type, keyed by cons cell, so one form has one type;
  - macros expanded during the walk rather than before it, so type-of
    is answered in the scope the macro was written in.

Closures take the same machinery: a receiver whose type comes from a
call, captures written (name expr) and typed from the expression, and
conversion from a bare function wherever a closure is expected.

type-match grew `_' on the pattern side, since (closure ((int)) int)
and (closure ((float)) int) were separate clauses for one case.
2026-09-30 00:09:32 +03:00
53f92727a5 mark multi-form macro expansions with $
A bare list was read as several forms, so a macro returning
`((make-adder 10) 5)' -- a call of what make-adder returns -- was
spliced into two. Add a new $ char to denote splicing, so ($ form ...)
splices.
2026-09-30 00:08:33 +03:00
6f54bfbe08 restore type inference layer 0
The type IR, unification with the unknown type, constraints and
schemes, built and tested on its own. Nothing calls it yet.
2026-09-30 00:08:19 +03:00
cd3016b6b8 fix (¤ int N) expanding to int N a[]
Now only C type keywords will stay as a part of type, i.e.
(¤ unsigned int) -> unsigned int a[] was and is okay
2026-09-29 22:25:07 +03:00
d3f05cecae fix (. (* p) x) expanding to *(p).x
Was an fmt-c-writer bug: unary-op must parenthize itself, and not it's
arg
2026-09-29 22:25:07 +03:00
8a3f51e833 implement closures 2026-09-29 21:07:16 +03:00
1084b7d266 add static-assert to sex
Expands to C11 _Static_assert keyword
2026-09-29 21:05:52 +03:00
aa5cfc1b7d remove capture list from lambdas, they are always pure
Explicitly rename capturing lambdas to closures, they will be
implemented later
2026-09-29 16:09:51 +03:00
ca88d9b386 implement compound and designated literals support
All checks were successful
Sex CI / build-linux (pull_request) Successful in 4m46s
Sex CI / build-linux (push) Successful in 4m50s
2026-09-27 19:35:37 +03:00
a9b00f2135 use true bool, since we target c99 onwards 2026-09-27 18:06:59 +03:00
26e8e6c374 fix lambdas and add a test to prevent rotting
Lambdas rotted because there wasn't any test for them, and after
return type moved to the end, lambdas stayed assumning it in the
front. We fix this and add test program
2026-09-27 16:11:05 +03:00
62316e1e3d enable includes to have multiple targets
All checks were successful
Sex CI / build-linux (pull_request) Successful in 4m47s
Sex CI / build-linux (push) Successful in 4m47s
2026-09-26 16:54:51 +03:00
Pavel Kulyov
b1ab18b9af dev: require make deps manually for dev & CI
All checks were successful
Sex CI / build-linux (pull_request) Successful in 4m50s
Sex CI / build-linux (push) Successful in 4m52s
2026-09-24 01:10:59 +03:00
e0987c1836 read the feature flags in sextest in the way sexc does
All checks were successful
Sex CI / build-linux (pull_request) Successful in 4m54s
Sex CI / build-linux (push) Successful in 4m48s
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
All checks were successful
Sex CI / build-linux (pull_request) Successful in 4m47s
Sex CI / build-linux (push) Successful in 4m50s
2026-09-22 23:40:16 +03:00
Pavel Kulyov
434086ade8 tests: add docstring cases
Some checks failed
Sex CI / build-linux (pull_request) Failing after 4m47s
Sex CI / build-linux (push) Failing after 4m50s
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
Pavel Kulyov
8588f5a531 semen: extract counting header length into a function 2026-09-20 15:33:09 +03:00
Pavel Kulyov
05929951cb modules: save fn docstring in prototype 2026-09-20 00:39:05 +03:00
43 changed files with 3993 additions and 249 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

3
.gitignore vendored
View File

@@ -9,3 +9,6 @@ sexc
sex-tests
sextest
tools/sextest/sextest
# Scrapped design docs, kept for reference
/attic

View File

@@ -26,7 +26,7 @@ INSTALL_PROGRAM = $(INSTALL)
MODULE_FLAGS = -emit-all-import-libraries -module-registration -c
# Order matters, since module check correctness on compilation
MODULES = utils types sex-macros reader sex-modules semen sex-fmt-c fmt-c-writer sexc
MODULES = utils types infer sex-macros reader sex-modules semen sex-fmt-c fmt-c-writer sexc
OBJ = $(MODULES:%=%.o)
DEPSFILE = dependencies.txt
@@ -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
@@ -60,6 +63,9 @@ utils.o: utils.module.scm utils.scm
types.o: types.module.scm types.scm
$(CHICKEN_C) $(CSC_FLAGS) $(MODULE_FLAGS) types.module.scm -o types.o -unit types
infer.o: infer.module.scm infer.scm types.o utils.o
$(CHICKEN_C) $(CSC_FLAGS) $(MODULE_FLAGS) infer.module.scm -o infer.o -unit infer -link types,utils
sex-macros.o: sex-macros.module.scm sex-macros.scm
$(CHICKEN_C) $(CSC_FLAGS) $(MODULE_FLAGS) sex-macros.module.scm -o sex-macros.o -unit sex-macros
@@ -69,29 +75,45 @@ reader.o: reader.module.scm reader.scm utils.o
sex-modules.o: sex-modules.module.scm sex-modules.scm reader.o utils.o
$(CHICKEN_C) $(CSC_FLAGS) $(MODULE_FLAGS) sex-modules.module.scm -o sex-modules.o -unit sex-modules -link reader,utils
semen.o: semen.module.scm semen.scm sex-macros.o sex-modules.o types.o utils.o
$(CHICKEN_C) $(CSC_FLAGS) $(MODULE_FLAGS) semen.module.scm -o semen.o -unit semen -link sex-macros,sex-modules,types,utils
semen.o: semen.module.scm semen.scm infer.o sex-macros.o sex-modules.o types.o utils.o
$(CHICKEN_C) $(CSC_FLAGS) $(MODULE_FLAGS) semen.module.scm -o semen.o -unit semen -link sex-macros,sex-modules,infer,types,utils
sex-fmt-c.o: sex-fmt-c.scm
$(CHICKEN_C) $(CSC_FLAGS) $(MODULE_FLAGS) sex-fmt-c.scm -o sex-fmt-c.o -unit sex-fmt-c
fmt-c-writer.o: fmt-c-writer.module.scm fmt-c-writer.scm sex-fmt-c.o utils.o
$(CHICKEN_C) $(CSC_FLAGS) $(MODULE_FLAGS) fmt-c-writer.module.scm -o fmt-c-writer.o -unit fmt-c-writer -link sex-fmt-c,utils
fmt-c-writer.o: fmt-c-writer.module.scm fmt-c-writer.scm sex-fmt-c.o types.o utils.o
$(CHICKEN_C) $(CSC_FLAGS) $(MODULE_FLAGS) fmt-c-writer.module.scm -o fmt-c-writer.o -unit fmt-c-writer -link sex-fmt-c,types,utils
sexc.o: sexc.module.scm types.o sexc.scm fmt-c-writer.o sex-macros.o sex-modules.o reader.o semen.o utils.o
$(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
sexc.o: sexc.module.scm infer.o types.o sexc.scm fmt-c-writer.o sex-macros.o sex-modules.o reader.o semen.o utils.o
$(CHICKEN_C) $(CSC_FLAGS) $(MODULE_FLAGS) sexc.module.scm -o sexc.o -unit sexc -link fmt-c-writer,sex-macros,sex-modules,reader,semen,infer,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 \
feature-flags
SEX_TEST_PROGRAMS = c99 \
closure-signatures \
closures \
comments \
compound-literals \
feature-flags \
features \
fixpoint \
hello-world \
inference \
lambdas \
lists \
operators \
serialize \
type-shapes \
unicode \
unnamed-params \
wildcards
# Multi-module linking is checked end to end; see tests/modules/Makefile.
check-modules: sexc
@@ -116,9 +138,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)
@@ -126,10 +150,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
@@ -172,11 +175,57 @@ e.g. for checking output for other platform:
sexc example/sdl3-triangle.sex -C --no-platform-features --features=linux,unix,x86-64
#+end_src
** Aggregate initializers and compound literals
~#(...)~ is a brace initializer. On its own it has no type and takes one
from where it is written:
#+begin_src scheme
(var p (struct point) #(1 2))
#+end_src
A ~:~ inside one ends a type and makes the whole thing a compound
literal --- an unnamed object of that type, usable anywhere an
expression is:
#+begin_src scheme
(var q (struct point) #(struct point : 3 4))
(var a (* int) #([int 3] : 10 20 30))
(draw-line ui #(struct point : 0 0) end)
(var p (* struct point) (& #(struct point : 9 9))) ; an lvalue, so `&' works
#+end_src
The type is written as bare words, the way it is everywhere else in the
language; ~:~ is what ends it.
A leading ~.~ names a field, so initializers may be designated, given in
any order, and mixed with positional ones:
#+begin_src scheme
(var r (struct named) #(struct named : .n 7 .first-name "zoe"))
#+end_src
A compound literal written inside a block lives until the end of that
block and no longer, so returning its address is a dangling pointer.
** Syntactic macros
Sex has support for syntactic macros. Macro definitions look like
functions: they have a name, an argument list and a body. Macro should
return Sex code.
A macro returns *one* form. To return several --- a function beside the
struct it works on, say --- return them under =$=, which splices them in
where the macro was written:
#+begin_src scheme
(defmacro (pair-of-fns a b)
`($ (fn ,a () int (return 1))
(fn ,b () int (return 2))))
#+end_src
=($)= expands to nothing. Everything else is a single form, including
one whose head is itself a form: =`((make-adder 10) 5)= calls what
=make-adder= returned, and is not two forms.
*** Examples:
**** Structure with templated value type
#+begin_src scheme

View File

@@ -3,19 +3,25 @@
(fn sum ((a int) (b int)) int
(return (+ a b)))
;;; A lambda captures nothing and is a bare function pointer; a closure
;;; captures and is a value carrying its own environment
(fn make-adder ((a int)) (closure ((int)) int)
(return (closure ((b int)) int (a)
(return (+ a b)))))
(pub fn main () int
(var a int 10)
(var b int 20)
(var (fn ((int) (int)) int) sum-fn sum)
(var sum-fn (fn ((int) (int)) int) sum)
(var (fn ((int) (int)) int) sum-lambda
(var sum-lambda (fn ((int) (int)) int)
(lambda ((a int) (b int)) int ()
(lambda ((a int) (b int)) int
(return (+ a b))))
(var (fn ((int)) int) sum-lambda-2
(var sum-lambda-2 (fn ((int)) int)
(lambda ((a int)) int ()
(lambda ((a int)) int
(return (+ a 20))))
(printf "Hello from main fn!\n")
@@ -24,28 +30,19 @@
(printf "Calling fn ptr: %d\n" (sum-fn a b))
(printf "Calling lambda: %d\n" (sum-lambda a b))
(printf "Calling other lambda: %d\n" (sum-lambda-2 a))
(printf "Calling lambda inplace: %d\n" ((lambda ((a int) (b int)) int ()
(printf "Calling lambda inplace: %d\n" ((lambda ((a int) (b int)) int
(return (+ a b 100)))
a b))
(var (fn ((int)) int) l-1
(lambda ((a int)) int ()
(var (fn ((int)) int) l-2
(lambda ((a int)) int ()
(var l-1 (fn ((int)) int)
(lambda ((a int)) int
(var l-2 (fn ((int)) int)
(lambda ((a int)) int
(return (+ 60 a))))
(return (+ 600 (l-2 a)))))
(printf "Calling nested lambdas: %d\n" (l-1 6))
;; Not supported yet
;; Closure
;; (var (fn (fn ((int)) int) ((int))) make-adder
;; (lambda (fn int ((int a))) ()
;; (return (lambda int ((int b)) (a)
;; (return (+ a b))))))
;;
;; (var (fn int ((int))) add-10
;; (make-adder 10))
;; (var (fn int ((int))) add-20
;; (make-adder 20))
;; (printf "Calling closures: %d\n" (add-10 24))
(var add-10 (closure ((int)) int) (make-adder 10))
(var add-20 (closure ((int)) int) (make-adder 20))
(printf "Calling closures: %d %d\n" (add-10 24) (add-20 24))
(return 0))

View File

@@ -13,6 +13,7 @@
(chicken irregex) ; unkebabify
srfi-1 ; lists
srfi-13 ; strings
types ; array-bound?, named-arg?
utils)
;;; egg `tree' not ported to CHICKEN 6 yet
@@ -114,16 +115,13 @@ forms, and what remains."
((attribute) '%attribute)
((¤) 'vector-ref)
((include) '%include)
((static-assert) '_Static_assert)
;; a `|' inside a symbol has to be escaped to be written in
;; a Scheme source, so we just rename it in fmt-c compatible
;; way
((|\||) 'bit-or)
((|\|\||) '%or)
((|\|=|) 'bit-or=)
;; uh things we do for c89 compatibility
((bool) 'int)
((true) 1)
((false) 0)
(else
(if (symbol? atom)
(unkebabify atom)
@@ -176,9 +174,7 @@ forms, and what remains."
(define (walk-expr form)
(match form
((? vector?)
(list->vector
(walk-expr (vector->list form))))
((? vector?) (walk-initializer (vector->list form)))
((? atom?)
(atom-to-fmt-c form))
;; (comment "text") -> /* text */. `%comment' is the fmt-c directive.
@@ -238,6 +234,48 @@ forms, and what remains."
;; Drop comments so they will not generate additional comma
(else (map walk-expr (remove comment-form? form)))))
;;; #(a b c) is a brace initializer. A `:' inside one ends a type and
;;; turns the whole thing into a C99 compound literal:
;;; #(struct point : 1 2) is (struct point){1, 2}, and the type is
;;; written as bare words, the way it is everywhere else in the
;;; language. `:' is the separator because it is the one thing that can
;;; be neither a type word nor an expression -- a named form would be a
;;; C identifier, and so shadowable (see issue #36).
(define (walk-initializer elements)
(let ((parts (list-split (remove comment-form? elements) ':)))
(if (null? (cdr parts))
(list->vector (walk-designators (car parts)))
(cons* '%compound
(walk-type (maybe-unwrap-type (car parts)))
(walk-designators (cadr parts))))))
;;; `.field value' is a designated initializer; anything else is
;;; positional. C lets the two be mixed, and nothing here stops it. A
;;; leading `.' cannot begin a C identifier, so a field name needs no
;;; keyword to introduce it and cannot collide with one.
(define (walk-designators elements)
(let loop ((es elements) (acc (list)))
(match es
(() (reverse acc))
(((? designator? d))
(sex-error elements "designated initializer without a value" d))
(((? designator? d) value . rest)
(loop rest
(cons (list '%designate
(atom-to-fmt-c (designator-field d))
(walk-expr value))
acc)))
((e . rest) (loop rest (cons (walk-expr e) acc))))))
(define (designator? x)
(and (symbol? x)
(let ((s (symbol->string x)))
(and (> (string-length s) 1)
(char=? #\. (string-ref s 0))))))
(define (designator-field d)
(string->symbol (substring (symbol->string d) 1)))
(define (walk-var form)
;; (var a int) -> (%var int a)
;; (var a (const int) 32) -> (%var (const int) a 32)
@@ -265,16 +303,11 @@ forms, and what remains."
;; (fn ((int) (float)) void) -> (%fun void ((int) (float)))
(match form
(('¤ . array-type)
(if (integer? (last array-type))
;; sized array
(let* ((type-list (drop-right array-type 1))
(type (maybe-unwrap-type type-list))
(size (last array-type)))
`(%array ,(walk-type type)
,size))
(if (array-bound? form)
`(%array ,(walk-type (array-element-type form)) ,(last array-type))
;; sugar for pointer... Do we really need it? Guess why not,
;; it's a strong semantic cue
`(%array ,(walk-type (maybe-unwrap-type array-type)))))
`(%array ,(walk-type (array-element-type form)))))
(('fn arglist ret-type)
`(%fun ,(walk-type ret-type) ,(walk-arg-types arglist)))
(('fn . _)
@@ -340,48 +373,36 @@ forms, and what remains."
.
,(walk-body maybe-body)))))
;;; TODO: isn't there a better way?
(define (is-probably-type form)
(case (car form)
((¤ * 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))))
;; are not part of it: ((* const char)), (int)
(walk-type (maybe-unwrap-type arg))))
;;; fmt-c reads a parameter as `(type name)', taking the name with
;;; `cadr'. A nameless one is the type and an explicit #f:
;;;
;;; (* const char) -> const char the star read as the name
;;; ((* const char) #f) -> const char *
;;; (int) -> (cadr) error
;;; (int #f) -> int
(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 (lambda (arg)
(if (named-arg? arg)
(list (arg-type arg) (walk-type (car arg)))
(arg-type arg)))
(list (arg-type arg)
(and (named-arg? arg) (walk-type (car 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
;; (fn name arglist ret-type body) -> normal function
;; (fn name arglist ret-type) -> prototype
(if (>= (length form) 5)
(walk-fn-def form)
(cons '%prototype (cdr (walk-fn-def form)))))

45
infer.module.scm Normal file
View File

@@ -0,0 +1,45 @@
(module infer
(;; The IR
tvar?
tvar-id
tvar-classes
tvar-rigid?
fresh-tvar
fresh-rigid-tvar
prim-type? prim-name prim-quals make-prim
ptr-type? ptr-target ptr-quals make-ptr
array-type? array-elt array-size make-array-type
fn-type? fn-ret fn-args fn-variadic? make-fn-type
agg-type? agg-kind agg-name agg-spelling agg-quals make-agg
alias-type? alias-name alias-expansion alias-quals make-alias
unknown-type? the-unknown-type
resolve
underlying
c-primitive?
type-quals
free-tvars
decay
;; The boundary
parse-type
unparse-type
;; Constraints
register-class!
add-instance!
entails?
default-tvar!
default-type-variables!
;; Unification
unify
;; Type schemes
scheme? scheme-vars scheme-constraints scheme-type
make-scheme
generalize
instantiate
substitute)
"infer.scm")

644
infer.scm Normal file
View File

@@ -0,0 +1,644 @@
;;; Type inference, layer 0: the type representation and unification.
;;;
;;; Nothing in the compiler calls this unit yet. It is the ground floor
;;; of the pass described in Type-inference.org -- built and tested on
;;; its own before a single form is routed through it.
;;;
;;; Two representations meet here. *Surface* types are the forms the
;;; rest of the compiler passes around -- `int', `(* const char)',
;;; `(¤ int 16)', `(fn ((int)) int)'. They are what the reader
;;; produces, what the C writer consumes and what `type-match' compares
;;; with `equal?', and they are hopeless for unification. The *IR*
;;; below is the other one: mutable cells, so that solving a type
;;; variable is a side effect rather than a substitution rebuilt at
;;; every step.
;;;
;;; `parse-type' and `unparse-type' are the boundary between the two,
;;; and they carry the whole compatibility burden: `unparse-type' must
;;; produce the exact spelling `type-match' compares against, or the
;;; reflection macros break by silently falling into their `else'
;;; branch. That is what the round-trip test in tests/infer.scm is for,
;;; and why it is driven by every type spelling that appears in the
;;; repository.
(import
scheme
(scheme base)
(chicken base)
matchable
srfi-1
srfi-69
types
utils)
;;; ---------------------------------------------------------------
;;; The IR
;;; ---------------------------------------------------------------
;;; A type variable is a mutable cell. `ref' is #f while unsolved and
;;; the type it stands for once bound -- union-find, with the path
;;; compression done in `resolve'.
;;;
;;; `classes' is the list of type classes the variable must satisfy
;;; (`numeric', and one day `ord'); see "constraints" below. `rigid?'
;;; marks a variable that must not unify with anything but itself --
;;; unused until a `fn' grows type parameters, and five lines now
;;; against an IR change later.
(define-record-type <tvar>
(%make-tvar id ref classes rigid?)
tvar?
(id tvar-id)
(ref tvar-ref tvar-ref-set!)
(classes tvar-classes tvar-classes-set!)
(rigid? tvar-rigid?))
;;; A primitive or otherwise nominal type. `name' is the list of words
;;; making it up, so `int', `(unsigned int)' and `(long long)' are all
;;; one node, and so is a name we have never parsed a declaration for
;;; (`size-t', `GLuint'). The two cases are told apart by
;;; `c-primitive?', which is what keeps a constraint over an unparsed C
;;; typedef from being an error.
(define-record-type <prim>
(make-prim name quals)
prim-type?
(name prim-name)
(quals prim-quals))
(define-record-type <ptr>
(make-ptr target quals)
ptr-type?
(target ptr-target)
(quals ptr-quals))
;;; `size' is an integer, or #f for `(¤ int)' -- an array of unwritten
;;; length.
(define-record-type <array>
(make-array-type elt size)
array-type?
(elt array-elt)
(size array-size))
(define-record-type <fn>
(make-fn-type ret args variadic?)
fn-type?
(ret fn-ret)
(args fn-args)
(variadic? fn-variadic?))
;;; struct / union / enum. Nominal: two of them are the same type when
;;; they are the same kind and the same name. `spelling' is the surface
;;; form it was written as, kept verbatim so that an aggregate defined
;;; inline in a type position round-trips unchanged.
(define-record-type <agg>
(make-agg kind name spelling quals)
agg-type?
(kind agg-kind)
(name agg-name)
(spelling agg-spelling)
(quals agg-quals))
;;; A typedef. Transparent to unification -- it unifies as whatever it
;;; expands to -- and opaque to printing, so a diagnostic and a
;;; generated declaration both say `size-t' rather than `unsigned long'.
(define-record-type <alias>
(make-alias name expansion quals)
alias-type?
(name alias-name)
(expansion alias-expansion)
(quals alias-quals))
;;; `?'. Sex has full C interop, so `printf', `SDL-CreateWindow' and
;;; `size-t' arrive from headers nobody parsed. Rather than reject
;;; every real program, the lattice gets a top element: `?' is
;;; consistent with every type and constrains nothing.
(define-record-type <unknown>
(%make-unknown)
unknown-type?)
(define the-unknown-type (%make-unknown))
(define tvar-counter 0)
(define (fresh-tvar . classes)
(set! tvar-counter (+ tvar-counter 1))
(%make-tvar tvar-counter #f (if (null? classes) (list) (car classes)) #f))
(define (fresh-rigid-tvar . classes)
(set! tvar-counter (+ tvar-counter 1))
(%make-tvar tvar-counter #f (if (null? classes) (list) (car classes)) #t))
;;; Follow a bound variable to what it stands for, compressing the path
;;; on the way out. Every procedure that looks at a type's shape starts
;;; here.
(define (resolve type)
(if (and (tvar? type) (tvar-ref type))
(let ((target (resolve (tvar-ref type))))
(tvar-ref-set! type target)
target)
type))
;;; ...and through any typedef as well, for the places that care what a
;;; type *is* rather than what it is called.
(define (underlying type)
(let ((t (resolve type)))
(if (alias-type? t)
(underlying (alias-expansion t))
t)))
(define (type-quals type)
(cond ((prim-type? type) (prim-quals type))
((ptr-type? type) (ptr-quals type))
((agg-type? type) (agg-quals type))
((alias-type? type) (alias-quals type))
(else (list))))
;;; Array-to-pointer and function-to-function-pointer, for the
;;; positions where C decays: a call argument, an operand of `+', the
;;; subscripted half of `(¤ a i)'.
(define (decay type)
(let ((t (underlying type)))
(cond ((array-type? t) (make-ptr (array-elt t) (list)))
((fn-type? t) (make-ptr t (list)))
(else (resolve type)))))
(define (free-tvars type)
(let collect ((t type) (acc (list)))
(let ((t (resolve t)))
(cond ((tvar? t) (if (memq t acc) acc (cons t acc)))
((ptr-type? t) (collect (ptr-target t) acc))
((array-type? t) (collect (array-elt t) acc))
((alias-type? t) (collect (alias-expansion t) acc))
((fn-type? t) (fold collect (collect (fn-ret t) acc) (fn-args t)))
(else acc)))))
;;; ---------------------------------------------------------------
;;; Surface -> IR
;;; ---------------------------------------------------------------
(define +qualifiers+ '(const volatile restrict))
(define (qualifier? word) (memq word +qualifiers+))
;;; `(const char)' written as `((const char))' is the same type: a
;;; sublist that merely groups. The C writer unwraps these too.
(define (maybe-unwrap type)
(if (and (list? type) (= 1 (length type)))
(car type)
type))
(define (parse-type surface)
(cond
((symbol? surface) (parse-words (list surface) (list) surface))
((not (pair? surface)) (sex-error surface "not a type" surface))
((eq? (car surface) '¤) (parse-array surface))
((eq? (car surface) 'fn) (parse-fn surface))
((memq '* surface) (parse-pointer-chain surface))
(else (parse-words surface (list) surface))))
;;; A `*'-free run of words: qualifiers, then whatever they qualify.
;;; `form' is only carried along so a complaint can say where it was
;;; written.
(define (parse-words words quals form)
(cond
((null? words) (sex-error form "type is nothing but qualifiers" form))
((qualifier? (car words))
(parse-words (cdr words) (cons (car words) quals) form))
;; A single sublist left: grouping parens, as in (* (const struct s))
((and (null? (cdr words)) (pair? (car words)))
(with-quals (parse-type (car words)) (reverse quals)))
((memq (car words) '(struct union enum)) (parse-agg words (reverse quals)))
((eq? (car words) '¤) (parse-array words))
((eq? (car words) 'fn) (parse-fn words))
((memq '* words) (parse-pointer-chain (append (reverse quals) words)))
(else (parse-name words (reverse quals) form))))
;;; A name, one word or several: `int', `size-t', `(unsigned int)'.
(define (parse-name words quals form)
(cond
((not (every symbol? words)) (sex-error form "malformed type" form))
;; The type-level wildcard. It is a fresh variable wherever it
;; appears, which is what makes partial types -- `(* _)', `(¤ _ 4)'
;; -- fall out for free rather than needing their own grammar.
((equal? words '(_)) (fresh-tvar))
((and (null? (cdr words)) (get-underlying-type (car words)))
=> (lambda (target)
(make-alias (car words) (parse-type target) quals)))
(else (make-prim words quals))))
;;; ([pub] struct name), (struct name (fields ...)), (struct (fields ...))
(define (parse-agg words quals)
(let* ((kind (car words))
(name (and (pair? (cdr words)) (symbol? (cadr words)) (cadr words))))
(make-agg kind name words quals)))
;;; (¤ elt ... size) -- the size is the last element when it is an
;;; integer, and absent otherwise. The element words are unwrapped the
;;; way the C writer unwraps them, so `[int 16]' and `[(int) 16]' are
;;; one type.
(define (parse-array surface)
(let* ((rest (cdr surface))
(sized? (and (pair? rest) (integer? (last rest))))
(size (and sized? (last rest)))
(words (if sized? (drop-right rest 1) rest)))
(when (null? words)
(sex-error surface "array type without an element type" surface))
(make-array-type (parse-type (maybe-unwrap words)) size)))
;;; (fn ((int) (float)) void). Argument entries are types, not named
;;; parameters -- a `fn' in type position has no room for names.
(define (parse-fn surface)
(match surface
(('fn (? list? arglist) ret)
(let* ((variadic? (and (pair? arglist) (variadic-marker? (last arglist))))
(entries (if variadic? (drop-right arglist 1) arglist)))
(make-fn-type (parse-type ret)
(map (lambda (entry) (parse-type (maybe-unwrap entry)))
entries)
variadic?)))
(else (sex-error surface "malformed function type" surface))))
;;; `...' in an arglist, written bare or wrapped the way every other
;;; entry is.
(define (variadic-marker? entry)
(or (eq? entry '...) (equal? entry '(...))))
;;; Pointer chains are written flat and read right to left: the last
;;; `*'-separated run is the pointed-to type, and each run before it
;;; qualifies one level of indirection. `(const * const char)' is a
;;; const pointer to a const char.
(define (parse-pointer-chain words)
(let* ((segments (list-split words '*))
(base (last segments))
(levels (reverse (drop-right segments 1))))
(when (null? base)
(sex-error words "pointer to nothing" words))
(fold (lambda (level acc)
(unless (every qualifier? level)
(sex-error words "only qualifiers may sit between two `*'" words))
(make-ptr acc level))
(parse-words (maybe-unwrap-segment base) (list) words)
levels)))
(define (maybe-unwrap-segment segment)
(let ((s (maybe-unwrap segment)))
(if (list? s) s (list s))))
;;; Re-qualify a parsed type, for the grouping case `(const (struct s))'
;;; where the qualifier is read before the thing it qualifies.
(define (with-quals type quals)
(if (null? quals)
type
(cond ((prim-type? type) (make-prim (prim-name type)
(append quals (prim-quals type))))
((ptr-type? type) (make-ptr (ptr-target type)
(append quals (ptr-quals type))))
((agg-type? type) (make-agg (agg-kind type) (agg-name type)
(agg-spelling type)
(append quals (agg-quals type))))
((alias-type? type) (make-alias (alias-name type)
(alias-expansion type)
(append quals (alias-quals type))))
(else type))))
;;; ---------------------------------------------------------------
;;; IR -> surface
;;; ---------------------------------------------------------------
;;; Every result here has to be the spelling the rest of the compiler
;;; already writes by hand, since `type-match' compares with `equal?'
;;; and a near miss is silent.
(define (unparse-type type)
(let ((t (resolve type)))
(cond
((tvar? t) '_)
((unknown-type? t) '?)
((alias-type? t) (qualify (alias-quals t) (list (alias-name t))))
((prim-type? t) (qualify (prim-quals t) (prim-name t)))
((agg-type? t) (qualify (agg-quals t) (agg-spelling t)))
((ptr-type? t)
(append (ptr-quals t) (list '*) (as-words (unparse-type (ptr-target t)))))
((array-type? t)
(let ((elt (as-words (unparse-type (array-elt t)))))
(append (list '¤)
(if (and (pair? elt) (eq? (car elt) '¤)) (list elt) elt)
(if (array-size t) (list (array-size t)) (list)))))
((fn-type? t)
(list 'fn
(append (map (lambda (arg) (as-arg (unparse-type arg))) (fn-args t))
(if (fn-variadic? t) (list '(...)) (list)))
(unparse-type (fn-ret t))))
(else (error "unparse-type: not a type" t)))))
;;; A one-word type is written bare, anything longer as a list --
;;; `int', but `(const int)' and `(struct point)'.
(define (qualify quals words)
(let ((all (append quals words)))
(if (and (null? quals) (= 1 (length all)))
(car all)
all)))
;;; An argument in a `fn' type is written as a list even when it is one
;;; word -- `((int) (float))' -- so only an atom needs wrapping.
(define (as-arg surface)
(if (pair? surface) surface (list surface)))
;;; Splice a type into a surrounding word list, the way `(* const char)'
;;; and `[* const char]' splice theirs. An array keeps its parentheses:
;;; `(¤ ¤ char 4)' would read back as something else entirely.
(define (as-words surface)
(cond ((not (pair? surface)) (list surface))
((memq (car surface) '(¤ fn)) (list surface))
(else surface)))
;;; ---------------------------------------------------------------
;;; Constraints
;;; ---------------------------------------------------------------
;;; `(numeric a)' is already a type class, so it is written as one from
;;; the start: one representation, one table, one entailment check. A
;;; trait bound `(ord (struct circle))' is the same shape, discharged
;;; the same way, and reported by the same procedure -- which is the
;;; whole reason to build it this way while there is only one kind of
;;; constraint to build.
;;;
;;; `default' is the type an unresolved constraint falls back to, the
;;; way Haskell defaults `Num a' to Integer. `test' is how the built-in
;;; classes say "every arithmetic type" without enumerating twenty
;;; spellings as instances; a user trait has no test and lives entirely
;;; in the instance table. `strict?' marks a class that must not be
;;; guessed at: static dispatch needs a real instance, so `?' fails it.
(define-record-type <type-class>
(%make-type-class name default test strict?)
type-class?
(name type-class-name)
(default type-class-default)
(test type-class-test)
(strict? type-class-strict?))
(define +classes+ (make-hash-table))
(define +instances+ (make-hash-table))
(define (register-class! name default test strict?)
(hash-table-set! +classes+ name (%make-type-class name default test strict?)))
(define (get-class name)
(or (hash-table-ref/default +classes+ name #f)
(error "no such type class" name)))
;;; Instances key on the *resolved* type, so `(impl show for size-t)'
;;; and `(impl show for unsigned long)' collide rather than quietly
;;; coexisting as two instances of one C type.
(define (instance-key type)
(unparse-type (underlying type)))
(define (add-instance! class-name type)
(hash-table-set! +instances+ (cons class-name (instance-key type)) #t))
(define (has-instance? class-name type)
(hash-table-exists? +instances+ (cons class-name (instance-key type))))
;;; #t, #f, or 'unknown -- and the third answer is the important one.
;;; A C name we never parsed a declaration for might well be numeric;
;;; saying #f there would reject working programs, and saying #t would
;;; invent knowledge. 'unknown means "do not constrain, do not
;;; complain".
(define (entails? class-name type)
(let ((cls (get-class class-name))
(t (underlying type)))
(cond
((tvar? t) 'unknown)
;; `?' is consistent with every type, but it entails nothing:
;; there is no instance to select and no name to mangle.
((unknown-type? t) (if (type-class-strict? cls) #f 'unknown))
((has-instance? class-name t) #t)
((type-class-test cls) => (lambda (test) (test t)))
(else #f))))
(define +integer-words+ '(char short int long signed unsigned bool _Bool))
(define +float-words+ '(float double))
(define +known-words+ (append '(void) +integer-words+ +float-words+))
;;; A prim built only out of words we recognise. Anything else is a
;;; name from a header, and we have no opinion about it.
(define (c-primitive? t)
(and (prim-type? t)
(every (lambda (word) (memq word +known-words+)) (prim-name t))))
(define (void-type? t)
(and (prim-type? t) (equal? (prim-name t) '(void))))
(define (arithmetic-type? t)
(cond ((and (agg-type? t) (eq? (agg-kind t) 'enum)) #t) ; an enum is an integer
((not (prim-type? t)) #f)
((not (c-primitive? t)) 'unknown)
((void-type? t) #f)
(else #t)))
(define (integral-type? t)
(cond ((and (agg-type? t) (eq? (agg-kind t) 'enum)) #t)
((not (prim-type? t)) #f)
((not (c-primitive? t)) 'unknown)
((void-type? t) #f)
((any (lambda (word) (memq word +float-words+)) (prim-name t)) #f)
(else #t)))
(define (floating-type? t)
(cond ((not (prim-type? t)) #f)
((not (c-primitive? t)) 'unknown)
(else (and (any (lambda (word) (memq word +float-words+)) (prim-name t))
#t))))
(define (scalar-type? t)
(cond ((ptr-type? t) #t)
((array-type? t) #t) ; decays to one
((fn-type? t) #t) ; likewise
(else (arithmetic-type? t))))
;;; The built-ins. They are ordinary classes, registered the same way a
;;; trait will be -- that is the point.
(register-class! 'numeric 'int arithmetic-type? #f)
(register-class! 'integral 'int integral-type? #f)
(register-class! 'floating 'double floating-type? #f)
(register-class! 'scalar #f scalar-type? #f)
;;; A constraint that survives to the end of a function is defaulted:
;;; `(numeric a)' with nothing else known is an `int'. A *strict*
;;; class has no default and no business guessing, so an unresolved one
;;; is an error -- the rule is worth stating while there is only one
;;; kind of constraint to state it about.
(define (default-tvar! v form)
(let ((strict (find (lambda (c) (type-class-strict? (get-class c)))
(tvar-classes v))))
(cond
(strict (sex-error form "unresolved constraint" (list strict (unparse-type v))))
((find (lambda (c) (type-class-default (get-class c))) (tvar-classes v))
=> (lambda (c)
(tvar-ref-set! v (parse-type (type-class-default (get-class c))))
#t))
(else #f))))
;;; Default every variable still open in TYPE. Returns #t when none is
;;; left unsolved, so a caller can tell "inferred" from "give up and
;;; ask for the type in writing".
(define (default-type-variables! type form)
(fold (lambda (v ok) (and (default-tvar! v form) ok))
#t
(free-tvars type)))
(define (check-classes classes type form)
(for-each
(lambda (c)
(when (eq? #f (entails? c type))
(sex-error form "type does not satisfy a constraint"
(list c (unparse-type type)))))
classes))
;;; ---------------------------------------------------------------
;;; Unification
;;; ---------------------------------------------------------------
;;; Consistency in the gradual-typing sense rather than equality: `?'
;;; succeeds against anything and binds nothing, which is what keeps
;;; the pass from rejecting every program that includes a C header.
;;;
;;; FORM is carried only so a failure can say where it was written.
(define (unify t1 t2 form)
(let ((a (resolve t1))
(b (resolve t2)))
(cond
((eq? a b) #t)
((unknown-type? a) #t)
((unknown-type? b) #t)
;; Whichever side is free takes the binding: `(unify a r)' and
;; `(unify r a)' both leave `a' bound to `r'. Two rigid and
;; distinct is the mismatch `eq?' above let through.
((and (tvar? a) (not (tvar-rigid? a))) (bind-tvar! a b form))
((and (tvar? b) (not (tvar-rigid? b))) (bind-tvar! b a form))
((or (tvar? a) (tvar? b)) (type-mismatch a b form))
;; A typedef unifies as what it stands for. Its name survives in
;; whichever side is printed later, since neither side is rebuilt.
((alias-type? a) (unify (alias-expansion a) b form))
((alias-type? b) (unify a (alias-expansion b) form))
((and (prim-type? a) (prim-type? b))
(check-quals a b form)
(or (equal? (prim-name a) (prim-name b))
(type-mismatch a b form)))
((and (ptr-type? a) (ptr-type? b))
(check-quals a b form)
(unify (ptr-target a) (ptr-target b) form))
((and (array-type? a) (array-type? b))
;; One of them may be `(¤ int)': an unwritten length constrains
;; nothing, the way it does not in C either.
(when (and (array-size a) (array-size b)
(not (= (array-size a) (array-size b))))
(type-mismatch a b form))
(unify (array-elt a) (array-elt b) form))
((and (fn-type? a) (fn-type? b))
(unless (and (= (length (fn-args a)) (length (fn-args b)))
(eq? (fn-variadic? a) (fn-variadic? b)))
(type-mismatch a b form))
(unify (fn-ret a) (fn-ret b) form)
(for-each (lambda (x y) (unify x y form)) (fn-args a) (fn-args b))
#t)
((and (agg-type? a) (agg-type? b))
(check-quals a b form)
(or (and (eq? (agg-kind a) (agg-kind b))
(if (and (agg-name a) (agg-name b))
(eq? (agg-name a) (agg-name b))
(equal? (agg-spelling a) (agg-spelling b))))
(type-mismatch a b form)))
(else (type-mismatch a b form)))))
(define (type-mismatch a b form)
(sex-error form "type mismatch: expected"
(unparse-type a) 'got (unparse-type b)))
;;; Qualifiers are compared, and a mismatch is a warning rather than a
;;; failure: C's const-correctness is not this pass's fight yet, and
;;; making it one would reject programs that compile today.
(define (check-quals a b form)
(let ((qa (type-quals a))
(qb (type-quals b)))
(unless (lset= eq? qa qb)
(sex-warning form "qualifiers differ between"
(unparse-type a) "and" (unparse-type b)))))
(define (bind-tvar! v t form)
(cond
;; Without recursive types this cannot trigger. It is four lines,
;; and the alternative to having it is a hang.
((occurs? v t) (sex-error form "recursive type" (unparse-type v)))
;; A rigid variable is a type *parameter*: inside a generic body it
;; stands for one specific unknown type and must not be solved.
((tvar-rigid? v) (type-mismatch v t form))
(else
(when (tvar? t)
(tvar-classes-set! t (lset-union eq? (tvar-classes t) (tvar-classes v))))
(tvar-ref-set! v t)
(unless (tvar? t)
(check-classes (tvar-classes v) t form))
#t)))
(define (occurs? v type)
(let ((t (resolve type)))
(cond ((eq? v t) #t)
((ptr-type? t) (occurs? v (ptr-target t)))
((array-type? t) (occurs? v (array-elt t)))
((alias-type? t) (occurs? v (alias-expansion t)))
((fn-type? t) (or (occurs? v (fn-ret t))
(any (lambda (a) (occurs? v a)) (fn-args t))))
(else #f))))
;;; ---------------------------------------------------------------
;;; Type schemes
;;; ---------------------------------------------------------------
;;; Nothing generalizes yet -- every `fn' in Sex carries a written
;;; signature and there is no polymorphism to abstract over. These are
;;; here because they are ten lines on top of unification and because
;;; they are exactly what a `fn' with type parameters needs, and
;;; because a scheme without a constraint list is the wrong shape for
;;; every bounded generic. `(forall vars constraints type)' it is,
;;; from the start.
(define-record-type <scheme>
(make-scheme vars constraints type)
scheme?
(vars scheme-vars)
(constraints scheme-constraints)
(type scheme-type))
;;; Quantify over everything free in TYPE that is not also free in the
;;; environment, carrying each variable's class constraints along as
;;; the scheme's context.
(define (generalize type env-tvars)
(let ((vars (lset-difference eq? (free-tvars type) env-tvars)))
(make-scheme vars
(append-map (lambda (v)
(map (lambda (c) (cons c v)) (tvar-classes v)))
vars)
type)))
(define (instantiate scheme)
(let ((subst (map (lambda (v) (cons v (fresh-tvar (tvar-classes v))))
(scheme-vars scheme))))
(substitute (scheme-type scheme) subst)))
;;; Structural copy with the variables in SUBST replaced. Copying is
;;; how a generic body must be handled anyway -- `form-type' is keyed
;;; by cons cell, one form one type, so an instantiation gets fresh
;;; cells rather than a second type for the same cell.
(define (substitute type subst)
(let ((t (resolve type)))
(cond
((tvar? t) (let ((hit (assq t subst))) (if hit (cdr hit) t)))
((ptr-type? t) (make-ptr (substitute (ptr-target t) subst) (ptr-quals t)))
((array-type? t) (make-array-type (substitute (array-elt t) subst)
(array-size t)))
((alias-type? t) (make-alias (alias-name t)
(substitute (alias-expansion t) subst)
(alias-quals t)))
((fn-type? t) (make-fn-type (substitute (fn-ret t) subst)
(map (lambda (a) (substitute a subst))
(fn-args t))
(fn-variadic? t)))
(else t))))

1080
semen.scm

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,7 @@
c-in-expr c-in-stmt c-in-test
c-paren c-maybe-paren c-type c-literal? c-literal char->c-char
c-struct c-union c-class c-enum c-typedef c-cast
c-braced-list c-compound c-designate
c-expr c-expr/sexp c-apply c-op c-indent c-current-indent-string
c-wrap-stmt c-open-brace c-close-brace
c-block c-braced-block c-begin
@@ -277,6 +278,8 @@
((%comment) ((apply c-comment (cdr x)) st))
((:) ((apply c-label (cdr x)) st))
((%cast) ((apply c-cast (cdr x)) st))
((%compound) ((apply c-compound (cdr x)) st))
((%designate) ((apply c-designate (cdr x)) st))
((+ - & * / % ! ~ ^ && < > <= >= == != << >>
= *= /= %= &= ^= >>= <<=) ; |\|| |\|\|| |\|=|
((apply c-op x) st))
@@ -306,17 +309,7 @@
((apply c-op "-=" (cdr x)) st))
(else ((c-apply x) st))))))
((vector? x)
((c-wrap-stmt
(fmt-try-fit
(fmt-let 'no-wrap? #t
(cat "{" (fmt-join c-expr (vector->list x) ", ") "}"))
(lambda (st)
(let* ((col (fmt-col st))
(sep (string-append "," (make-nl-space col))))
((cat "{" (fmt-join c-expr (vector->list x) sep)
"}" nl)
st)))))
st))
((c-wrap-stmt (c-braced-list (vector->list x))) st))
(else
((c-literal x) st))))))
@@ -832,6 +825,25 @@
(cat "(" (c-with-op 'paren (c-expr expr)) ")")
(c-expr expr))))
;; { a, b, c } -- on one line if it fits, one element per line if not.
(define (c-braced-list ls)
(fmt-try-fit
(fmt-let 'no-wrap? #t (cat "{" (fmt-join c-expr ls ", ") "}"))
(lambda (st)
(let* ((col (fmt-col st))
(sep (string-append "," (make-nl-space col))))
((cat "{" (fmt-join c-expr ls sep) "}" nl) st)))))
;; (T){ ... } -- a C99 compound literal, not a cast: the result is an
;; unnamed object and an lvalue, so `&' on it is legal. At block scope
;; it lives until the end of the enclosing block and no longer.
(define (c-compound type . init)
(cat "(" (c-type type) ")" (c-braced-list init)))
;; .field = value, inside a braced list
(define (c-designate field value)
(cat "." (c-expr field) " = " (c-expr value)))
(define (c-typedef type alias . o)
(c-wrap-stmt
(cat "typedef " (c-type type alias) (fmt-join/prefix c-expr o " "))))
@@ -1003,9 +1015,24 @@
(cat nl (make-space (+ 2 (fmt-col st))) str " "))
st))))))))))
;; `-' arrives as the symbol binary minus uses, and so with binary
;; precedence: `(+ (- b) b)' asked under that spelling comes out
;; `(-b) + b'.
(define (unary-operator op)
(case op
((-) 'unary-)
((+) 'unary+)
((*) 'unary-*)
((&) 'unary-&)
(else op)))
;; Parenthesises the whole expression rather than the operand: the
;; other way round, `(. (* p) x)' came out `*(p).x', which C reads as
;; `*(p.x)'.
(define (c-unary-op op x)
(c-wrap-stmt
(cat (display-to-string op) (c-maybe-paren op (c-expr x)))))
(c-maybe-paren (unary-operator op)
(cat (display-to-string op) (c-expr x)))))
;; some convenience definitions

View File

@@ -25,9 +25,14 @@
;; the type database
(import scheme
(scheme base)
;; a type is a list, so a macro reading one wants
;; `(third type)' rather than `(caddr type)'
(only srfi-1 first second third fourth fifth last)
(only sex-macros cat comment)
(only types get-type-info get-tag-info get-fields
get-underlying-type type-match map-fields))
get-underlying-type type-match type-pattern-matches?
map-fields
get-name-type get-return-type type-of))
,@body)))
(define (get-macro name)

View File

@@ -8,6 +8,7 @@
(chicken process-context)
(chicken string)
fmt
matchable
reader
srfi-1
utils)
@@ -71,28 +72,35 @@
(list)
raw-forms)))
(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)
(('pub 'fn name args ret ('comment . _) . rest)
(public-fn-interface `(pub fn ,name ,args ,ret ,@rest)))
(('pub 'fn name args ret (? string? doc) . _)
`(pub fn ,name ,args ,ret ,doc))
(('pub 'fn name args ret . _)
`(pub fn ,name ,args ,ret)))))
;;; TODO: use semen facilities to analyze modules
(define (process-public-interface-form form acc)
(case (car form)
((pub)
(case (cadr form)
;; A function is reduced to a prototype and keeps its `pub', so
;; the importing unit declares it with external linkage
((fn)
(cons (copy-form-source!
form
(take (strip-header-comments form 5) 5))
acc))
;; A variable becomes an `extern' declaration
((var)
(cons (copy-form-source!
form
(cons 'extern (take (cdr (strip-header-comments form 4)) 3)))
acc))
((define defmacro enum import include struct typedef union)
(cons (copy-form-source! form (cdr form)) acc))
(else (sex-error form "pub must be followed by a definition" form))))
(else acc)))
(match form
;; Reduced to a prototype, still `pub', so the importer declares it
;; with external linkage
(('pub 'fn . _)
(cons (copy-form-source! form (public-fn-interface form)) 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 . _)
(sex-error form "pub must be followed by a definition" form))
(_ acc)))
(define (load-persistent-module-paths)
(let ((sex-module-path-env-var

View File

@@ -175,16 +175,22 @@ status, which is ours to pass on."
(semen-process raw-forms))))
(define prelude
'((include inttypes.h)
(append
'((include inttypes.h)
(include stdbool.h)
(include stddef.h) ; max_align_t, for closure environments
(typedef u8 uint8-t)
(typedef i8 int8-t)
(typedef u16 uint16-t)
(typedef i16 int16-t)
(typedef u32 uint32-t)
(typedef i32 int32-t)
(typedef u64 uint64-t)
(typedef i64 int64-t))
(typedef u8 uint8-t)
(typedef i8 int8-t)
(typedef u16 uint16-t)
(typedef i16 int16-t)
(typedef u32 uint32-t)
(typedef i32 int32-t)
(typedef u64 uint64-t)
(typedef i64 int64-t)))
;; The closure environment is the part of the ABI, so include it in
;; every module
(list (closure-env-declaration))))
(define (main)
(let* ((argv (command-line-arguments))

View File

@@ -3,10 +3,10 @@ CHICKEN_C = csc
CSC_FLAGS += -K prefix -static
MODULE_FLAGS = -emit-all-import-libraries -module-registration -c
MODULES = utils types sex-macros reader sex-modules semen sex-fmt-c fmt-c-writer sexc
MODULES = utils types infer sex-macros reader sex-modules semen sex-fmt-c fmt-c-writer sexc
SEX_OBJ = $(MODULES:%=%.o)
TESTS = basic semen reader fmt-c-writer utils line-directives codegen args types
TESTS = basic semen reader fmt-c-writer utils line-directives codegen args types infer
TEST_SRCS = $(TESTS:%=%.scm)
sex-tests: run.scm $(TEST_SRCS) $(SEX_OBJ)
@@ -20,6 +20,9 @@ utils.o: utils.module.scm ../utils.scm
types.o: types.module.scm ../types.scm
$(CHICKEN_C) $(CSC_FLAGS) $(MODULE_FLAGS) types.module.scm -o types.o -unit types
infer.o: infer.module.scm ../infer.scm types.o utils.o
$(CHICKEN_C) $(CSC_FLAGS) $(MODULE_FLAGS) infer.module.scm -o infer.o -unit infer -link types,utils
sex-macros.o: sex-macros.module.scm ../sex-macros.scm
$(CHICKEN_C) $(CSC_FLAGS) $(MODULE_FLAGS) sex-macros.module.scm -o sex-macros.o -unit sex-macros
@@ -29,17 +32,17 @@ reader.o: reader.module.scm ../reader.scm utils.o
sex-modules.o: sex-modules.module.scm ../sex-modules.scm reader.o utils.o
$(CHICKEN_C) $(CSC_FLAGS) $(MODULE_FLAGS) sex-modules.module.scm -o sex-modules.o -unit sex-modules -link reader,utils
semen.o: semen.module.scm ../semen.scm sex-macros.o sex-modules.o types.o utils.o
$(CHICKEN_C) $(CSC_FLAGS) $(MODULE_FLAGS) semen.module.scm -o semen.o -unit semen -link sex-macros,sex-modules,types,utils
semen.o: semen.module.scm ../semen.scm infer.o sex-macros.o sex-modules.o types.o utils.o
$(CHICKEN_C) $(CSC_FLAGS) $(MODULE_FLAGS) semen.module.scm -o semen.o -unit semen -link sex-macros,sex-modules,infer,types,utils
sex-fmt-c.o: ../sex-fmt-c.scm
$(CHICKEN_C) $(CSC_FLAGS) $(MODULE_FLAGS) ../sex-fmt-c.scm -o sex-fmt-c.o -unit sex-fmt-c
fmt-c-writer.o: fmt-c-writer.module.scm ../fmt-c-writer.scm sex-fmt-c.o utils.o
$(CHICKEN_C) $(CSC_FLAGS) $(MODULE_FLAGS) fmt-c-writer.module.scm -o fmt-c-writer.o -unit fmt-c-writer -link sex-fmt-c,utils
fmt-c-writer.o: fmt-c-writer.module.scm ../fmt-c-writer.scm sex-fmt-c.o types.o utils.o
$(CHICKEN_C) $(CSC_FLAGS) $(MODULE_FLAGS) fmt-c-writer.module.scm -o fmt-c-writer.o -unit fmt-c-writer -link sex-fmt-c,types,utils
sexc.o: sexc.module.scm types.o ../sexc.scm fmt-c-writer.o sex-macros.o sex-modules.o reader.o semen.o utils.o
$(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
sexc.o: sexc.module.scm infer.o types.o ../sexc.scm fmt-c-writer.o sex-macros.o sex-modules.o reader.o semen.o utils.o
$(CHICKEN_C) $(CSC_FLAGS) $(MODULE_FLAGS) sexc.module.scm -o sexc.o -unit sexc -link fmt-c-writer,sex-macros,sex-modules,reader,semen,infer,types,utils
clean:
rm -f $(SEX_OBJ)

View File

@@ -31,10 +31,10 @@
(test 'vector-ref (atom-to-fmt-c '¤))
(test '%include (atom-to-fmt-c 'include))
;; c89 stuff
(test 'int (atom-to-fmt-c 'bool))
(test 1 (atom-to-fmt-c 'true))
(test 0 (atom-to-fmt-c 'false))
;; C99 onwards, true C spellings for bool
(test 'bool (atom-to-fmt-c 'bool))
(test 'true (atom-to-fmt-c 'true))
(test 'false (atom-to-fmt-c 'false))
;; dot-access -> %. member-access directive (kebab-converted operands)
(test '(%. a b) (walk-expr '(dot-access a b)))

View File

@@ -230,4 +230,382 @@ 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. */")))
;; `static-assert' is the keyword rather than the <assert.h> macro, so
;; a static assertion costs no include. Mapped in `atom-to-fmt-c'
;; because `unkebabify' alone would spell it `static_assert'.
(test-group "static-assert"
(test-assert "emits the C11 keyword"
(emits? (in-fn "(static-assert (== (sizeof int) 4) \"int is four bytes\")")
"_Static_assert(sizeof(int) == 4, \"int is four bytes\")"))
(test-assert "and not the header macro"
(not (emits? (in-fn "(static-assert (== (sizeof int) 4) \"x\")")
"static_assert("))))
;; A closure is a code pointer beside its captures. The struct is
;; named from the signature, so separate translation units agree on
;; it, and calling one goes through `code' with `env' passed first.
;;
;; A closure struct, and the helper its calls go through, are each
;; emitted once per signature; the registries deciding that are
;; compile-time state like the type databases, and outlive a single
;; `sex->c' here. So every case below that looks for a *definition*
;; uses a signature of its own -- cases looking at a call site can
;; share one.
(test-group "closures"
(test-assert "the type becomes a struct named for its signature"
(emits? "(fn f ((c (closure ((int)) int))) int (return (c 1)))"
"struct ƛint_int"))
;; The environment is one shared union, declared in the prelude --
;; its layout is part of the ABI two units agree on, so it cannot
;; depend on what either file contains. `sex->c' has no prelude, so
;; what is visible here is the member
(test-assert "whose environment is the shared union"
(emits? "(fn f ((c (closure ((float)) int))) int (return (c 1.0)))"
"union ƛenv env;"))
;; The receiver goes through a helper rather than being written
;; out twice, so that `[table (++ i)]' evaluates its index once,
;; exactly as it would for an array of function pointers
(test-assert "a call passes the receiver to a helper"
(emits? "(fn f ((c (closure ((int)) int))) int (return (c 1)))"
"ƛint_int_call(c, 1)"))
(test-assert "and the helper is what dereferences it"
(emits? "(fn f ((c (closure ((long)) int))) int (return (c 1)))"
"ƛc.code(&ƛc.env, ƛa0)"))
(test-assert "a subscript receiver is evaluated once"
(emits? "(fn f ((t (¤ (closure ((int)) int) 4)) (i int)) int (return ((¤ t (++ i)) 1)))"
"ƛint_int_call(t[++i], 1)"))
(test-assert "so is a member receiver"
(emits? "(struct h ((cb (closure ((int)) int))))
(fn f ((s (struct h))) int (return ((. s cb) 1)))"
"ƛint_int_call(s.cb, 1)"))
(test-assert "a captured name is rebound in the lifted body"
(emits? "(fn f ((n int)) (closure ((char)) int) (return (closure ((x char)) int (n) (return n))))"
"int n = ƛcaptures->n;"))
(test-assert "captures are checked against the environment"
(emits? "(fn f ((n int)) (closure ((short)) int) (return (closure ((x short)) int (n) (return n))))"
"_Static_assert(sizeof(struct"))
;; A closure over nothing has no record to point at, and C has no
;; empty struct to declare for it
(test-assert "no captures means no capture record"
(not (emits? "(fn f () (closure () int) (return (closure () int () (return 7))))"
"_captures {")))
;; `(name expr)' names a capture and gives what it holds, so the
;; expression is evaluated once, where the closure is written
(test-assert "a named capture takes its type from the expression"
(emits? "(struct p ((x int) (y int)))
(fn f ((s (struct p))) (closure () int)
(return (closure () int ((sum (+ (. s x) (. s y)))) (return sum))))"
"int sum;"))
(test-assert "and the constructor is handed the expression"
(emits? "(struct p ((x int) (y int)))
(fn f ((s (struct p))) (closure () int)
(return (closure () int ((sum (+ (. s x) (. s y)))) (return sum))))"
"_make(s.x + s.y)"))
(test-assert "capturing a pointer is how by-reference is spelled"
(emits? "(struct p ((x int)))
(fn f ((s (* (struct p)))) (closure () int)
(return (closure () int ((q s)) (return (-> q x)))))"
"struct p* q;"))
;; A bare function is a closure that captures nothing, so it
;; converts wherever one is expected -- the pointer goes in the
;; environment and one thunk per signature reads it back out
(test-assert "a named function in a var initializer"
(emits? "(fn g ((n int)) int (return n))
(fn f () void (var c (closure ((int)) int) g))"
"ƛint_int_fromfn(g)"))
(test-assert "a lambda, which is a bare function too"
(emits? "(fn f () void (var c (closure ((int)) int)
(lambda ((n int)) int (return n))))"
"ƛint_int_fromfn(λ0_f)"))
(test-assert "an argument, against the parameter that signature wrote"
(emits? "(fn g ((n int)) int (return n))
(fn h ((c (closure ((int)) int))) int (return (c 1)))
(fn f () int (return (h g)))"
"h(ƛint_int_fromfn(g))"))
(test-assert "a return, against the declared return type"
(emits? "(fn g ((n int)) int (return n))
(fn f () (closure ((int)) int) (return g))"
"return ƛint_int_fromfn(g)"))
(test-assert "the thunk reads the pointer out of the environment"
(emits? "(fn g ((n float)) int (return 1))
(fn f () void (var c (closure ((float)) int) g))"
"return ƛcaptures->f(ƛa0)"))
;; a signature that does not match is left alone, and C rejects it
(test-assert "a function of the wrong signature does not convert"
(not (emits? "(fn g ((n float)) int (return 1))
(fn f () void (var c (closure ((int)) int) g))"
"_fromfn(g)")))
;; a closure is lifted into the function it was written in, so
;; there has to be one
(test-assert "a closure at toplevel is refused"
(reports? "(var c (closure () int) (closure () int () (return 1)))"
"only be written inside a function"))
(test-assert "and so is one in a struct field"
(reports? "(struct s ((f (closure () int) (closure () int () (return 1)))))"
"only be written inside a function"))
;; A block opens a scope, so what it declares ends with it
(test-assert "a name shadowed in a block does not escape it"
(emits? "(fn mk () (closure () int) (return (closure () int () (return 1))))
(fn f () int (var c (closure () int) (mk))
(do (var c int 9) (g c))
(return (c)))"
"ƛvoid_int_call(c)"))
(test-assert "and the shadowing declaration is what the block sees"
(emits? "(fn mk () (closure () int) (return (closure () int () (return 1))))
(fn f () int (var c (closure () int) (mk))
(do (var c int 9) (g c))
(return (c)))"
"g(c)"))
;; The receiver is written twice, so a name used as an argument
;; must not be mistaken for a call of its own
(test-assert "a closure passed as an argument stays a value"
(emits? "(fn g ((c (closure ((int)) int))) int (return 0))
(fn f ((c (closure ((int)) int))) int (return (g c)))"
"g(c)")))
;; a macro body reads types as lists, so the srfi-1 accessors are in
;; scope beside the type database
(test-group "macro list accessors"
(test-assert "third reads an array's length"
(emits? "(defmacro (len t) (third t))
(fn f () int (return (len (¤ int 7))))"
"return 7;"))
(test-assert "second reads a tag"
(emits? "(defmacro (tag t) (symbol->string (second t)))
(fn f () void (g (tag (struct point))))"
"g(\"point\")")))
;; `type-of' hands a macro the type of an *expression*, where
;; `get-name-type' only answers for a name. The macro is expanded
;; during the walk rather than before it, so the scope is still live.
(test-group "type-of"
(test-assert "a local, from its declaration"
(emits? "(defmacro (t x) (type-match (type-of x) (int 1) (else 0)))
(fn f () int (var n int 0) (return (t n)))"
"return 1;"))
(test-assert "an expression, not just a name"
(emits? "(defmacro (t x) (type-match (type-of x) (double 1) (else 0)))
(fn f () int (var d double 0.0) (return (t (+ d 1))))"
"return 1;"))
(test-assert "a call, through the callee's signature"
(emits? "(fn g () float (return 1.0))
(defmacro (t x) (type-match (type-of x) (float 1) (else 0)))
(fn f () int (return (t (g))))"
"return 1;"))
;; a macro is shown the written spelling, not the generated struct
(test-assert "a closure, spelled the way it was written"
(emits? "(fn mk () (closure ((int)) int)
(return (closure ((b int)) int () (return b))))
(defmacro (t x) (type-match (type-of x) ((closure _ _) 1) (else 0)))
(fn f () int (var c _ (mk)) (return (t c)))"
"return 1;"))
(test-assert "and calling one has the closure's return type"
(emits? "(fn mk () (closure ((int)) int)
(return (closure ((b int)) int () (return b))))
(defmacro (t x) (type-match (type-of x) (int 1) (else 0)))
(fn f () int (var c _ (mk)) (return (t (c 1))))"
"return 1;"))
;; outside an expansion there is no scope to ask about
(test-assert "a name the walk has not reached is unknown"
(emits? "(defmacro (t x) (type-match (type-of x) (int 1) (else 0)))
(fn f () int (return (t nope)))"
"return 0;")))
;; `_' as a type is written out from what the initializer says. The
;; answer comes from declarations and from the signature a call names,
;; never from unification -- a partial type would need one.
(test-group "wildcard types"
(test-assert "an integer literal"
(emits? (in-fn "(var x _ 42)") "int x = 42"))
(test-assert "a float literal"
(emits? (in-fn "(var x _ 3.5)") "double x = 3.5"))
(test-assert "a string literal"
(emits? (in-fn "(var x _ \"hi\")") "const char * x"))
(test-assert "a call, through the name table"
(emits? "(fn g ((a int)) float (return 1.0))
(fn f () void (var x _ (g 1)))"
"float x = g(1)"))
(test-assert "a struct member"
(emits? "(struct p ((a int) (b float)))
(fn f ((s (struct p))) void (var x _ (. s b)))"
"float x = s.b"))
(test-assert "an address, which composes"
(emits? "(struct p ((a int)))
(fn f ((s (struct p))) void (var x _ (& s)))"
"struct p* x = &s"))
(test-assert "a comparison is a bool"
(emits? (in-fn "(var x _ (< a b))") "bool x = a < b"))
;; a wildcard inside a spelling is solved in place, leaving the rest
;; of the written type alone -- this is what needs the unifier
(test-assert "a wildcard inside a pointer"
(emits? "(struct p ((a int)))
(fn f ((s (struct p))) void (var x (* _) (& s)))"
"struct p* x = &s"))
(test-assert "a wildcard inside an array"
(emits? (in-fn "(var t (¤ _ 3) #((¤ int 3) : 1 2 3))") "int t[3]"))
;; a compound literal carries its own type, where a brace
;; initializer has none and takes one from its context
(test-assert "a compound literal answers a bare wildcard"
(emits? "(struct p ((a int) (b int)))
(fn f () void (var x _ #((struct p) : 1 2)))"
"struct p x = (struct p){1, 2}"))
(test-assert "a brace initializer cannot"
(reports? (in-fn "(var x _ #(1 2))") "cannot infer the type"))
;; ...but its elements still solve the hole in an array type
(test-assert "elements solve an array's element type"
(emits? (in-fn "(var t (¤ _ 4) #(0 1 4 9))") "int t[4] = {0, 1, 4, 9}"))
(test-assert "including when there are fewer than the length"
(emits? (in-fn "(var t (¤ _ 10) #(1 2))") "int t[10] = {1, 2}"))
(test-assert "and they have to agree with each other"
(reports? (in-fn "(var t (¤ _ 2) #(1 \"s\"))") "type mismatch"))
;; a closure type reaches the solver as the struct that stands for
;; it, which is the spelling `parse-type' knows
(test-assert "a closure, from the signature that produced it"
(emits? "(fn mk () (closure ((int)) int)
(return (closure ((b int)) int () (return b))))
(fn f () void (var c _ (mk)))"
"struct ƛint_int c = mk()"))
(test-assert "and it is callable once inferred"
(emits? "(fn mk () (closure ((int)) int)
(return (closure ((b int)) int () (return b))))
(fn f () int (var c _ (mk)) (return (c 1)))"
"ƛint_int_call(c, 1)"))
;; C's usual arithmetic conversions, far enough to answer `_'
(test-assert "floating beats integral"
(emits? (in-fn "(var d double 1.0) (var x _ (+ a d))") "double x = a + d"))
(test-assert "the wider integer wins"
(emits? (in-fn "(var l long 1) (var x _ (+ a l))") "long x = a + l"))
(test-assert "double beats float"
(emits? (in-fn "(var g float 1.0) (var d double 1.0) (var x _ (+ g d))")
"double x = g + d"))
(test-assert "and same-width operands stay put"
(emits? (in-fn "(var g float 1.0) (var x _ (+ g g))") "float x = g + g"))
(test-assert "a pointer operand makes it pointer arithmetic"
(emits? "(struct p ((a int)))
(fn f ((s (struct p))) void (var x _ (+ (& s) 1)))"
"struct p* x = &s + 1"))
;; a written type that cannot match what the initializer gives
(test-assert "a mismatch is reported, not papered over"
(reports? (in-fn "(var p (* _) 42)") "type mismatch"))
;; a wildcard that cannot be answered is an error, not a guess
(test-assert "with no initializer there is nothing to infer from"
(reports? (in-fn "(var x _)") "cannot infer the type"))
(test-assert "nor from a name the compiler never saw declared"
(reports? (in-fn "(var x _ (never-declared))") "cannot infer the type")))
;; `(car res)' on the expansion assumed it was a pair, so a macro
;; computing a value rather than building a form crashed the compiler.
(test-group "macro expanding to an atom"
(test-assert "a number"
(emits? "(defmacro (two) 2) (fn f () int (return (two)))"
"return 2;"))
(test-assert "a string"
(emits? "(defmacro (who) \"sex\") (fn f () void (g (who)))"
"g(\"sex\")"))
;; a symbol expansion can stand where a type does, which is what
;; makes a macro able to compute one
(test-assert "a symbol, used as a type"
(emits? "(defmacro (ty) 'int) (fn f () void (var x (ty) 0))"
"int x = 0"))
;; ...and nothing at all, for a macro that only registers something
(test-assert "nothing, at toplevel"
(emits? "(defmacro (quiet) (list)) (quiet) (fn f () int (return 1))"
"return 1;"))
(test-assert "nothing, in a body"
(emits? "(defmacro (quiet) (list)) (fn f () int (quiet) (return 1))"
"return 1;"))
;; several forms need `$', which is what tells a splice from a call
(test-assert "$ splices"
(emits? "(defmacro (pair) (list '$ '(fn a () int (return 1))
'(fn b () int (return 2))))
(pair)"
"b (void)"))
(test-assert "and ($) is nothing at all"
(emits? "(defmacro (quiet) (list '$)) (quiet) (fn f () int (return 1))"
"return 1;"))
;; without `$' a list is one form, so a head that is itself a form
;; stays a call rather than becoming two statements
(test-assert "a computed callee stays one form"
(emits? "(fn mk () (closure ((int)) int)
(return (closure ((b int)) int () (return b))))
(defmacro (apply-it x) `((mk) ,x))
(fn f () int (return (apply-it 5)))"
"ƛint_int_call(mk(), 5)"))
;; spliced, it would have become two forms in the `return' -- the
;; comma operator, and the wrong answer
(test-assert "rather than two forms in its context"
(not (emits? "(fn mk () (closure ((int)) int)
(return (closure ((b int)) int () (return b))))
(defmacro (apply-it x) `((mk) ,x))
(fn f () int (return (apply-it 5)))"
"return mk(), 5"))))
;; A unary expression parenthesised its operand rather than itself, so
;; the parens landed inside: `*(p).x', which C reads as `*(p.x)'.
(test-group "unary operand precedence"
(test-assert "member access through a dereference"
(emits? "(struct pt ((x int))) (fn f ((p (* (struct pt)))) int (return (. (* p) x)))"
"(*p).x"))
(test-assert "and not with the parens inside"
(not (emits? "(struct pt ((x int))) (fn f ((p (* (struct pt)))) int (return (. (* p) x)))"
"*(p).x")))
(test-assert "member access through a cast"
(emits? (in-fn "(var n int (. (* (cast a (* (struct s)))) f))")
"(*(struct s*)a).f"))
;; ...without gaining parens where none are due
(test-assert "a bare dereference is left alone"
(emits? (in-fn "(var p (* int) 0) (= a (* p))") "a = *p"))
(test-assert "so is address-of in an argument"
(emits? (in-fn "(g (& a))") "g(&a)"))
(test-assert "and negation beside a binary operator"
(emits? (in-fn "(var n int (+ (- a) b))") "-a + b")))
;; An array bound was taken only when it was an integer literal, so a
;; symbolic one fell into the type: `(¤ int N)' came out `int N a[]'.
(test-group "array bounds"
(test-assert "a symbolic bound"
(emits? "(define N 4) (struct s ((a (¤ int N))))" "int a[N]"))
(test-assert "an expression bound"
(emits? "(define N 4) (struct s ((a (¤ char (* 2 N)))))" "char a[2 * N]"))
(test-assert "an integer bound still works"
(emits? "(struct s ((a (¤ int 4))))" "int a[4]"))
;; a multi-word type is keywords all the way down, so a trailing
;; keyword belongs to the type and leaves the array unsized
(test-assert "a multi-word type is not a bound"
(emits? "(struct s ((a (¤ unsigned int))))" "unsigned int a[]"))
;; ...and a tag always follows its keyword
(test-assert "nor is an aggregate tag"
(emits? "(struct t ((z int))) (struct s ((a (¤ struct t))))" "struct t a[]"))
(test-assert "nor one behind a pointer"
(emits? "(struct t ((z int))) (struct s ((a (¤ * struct t))))" "struct t* a[]"))))

View File

@@ -40,15 +40,15 @@
(walk-type '(const * const char)))
(test
'(%fun void ((int) (float) (%array (struct what * const))))
'(%fun void (int float (%array (struct what * const))))
(walk-type '(fn ((int) (float) (¤ (const * struct what))) void)))
(test
'(%fun void ((int) (%array float) (%array (struct what * const))))
'(%fun void (int (%array float) (%array (struct what * const))))
(walk-type '(fn ((int) (¤ float) (¤ (const * struct what))) void)))
(test
'(%array (%fun void ((int) (%array float) (%array (struct what * const)))))
'(%array (%fun void (int (%array float) (%array (struct what * const)))))
(walk-type '(¤ (fn ((int) (¤ float) (¤ (const * struct what))) void))))
;; Type convert to C
@@ -101,9 +101,42 @@
'(%var (struct suc *) s (hoge piyo))
(walk-var '(var s (* struct suc) (hoge piyo))))
;;; Initializers and compound literals
(test "a bare initializer is unchanged"
'#(1 2)
(walk-expr '#(1 2)))
(test "`:' ends the type and makes it a compound literal"
'(%compound (struct point) 3 4)
(walk-expr '#(struct point : 3 4)))
(test "the type is bare words, as everywhere else"
'(%compound (const char *) 65)
(walk-expr '#(* const char : 65)))
(test "and may be an array type"
'(%compound (%array int 3) 10 20 30)
(walk-expr '#(¤ int 3 : 10 20 30)))
(test "a grouped type is unwrapped the way a declaration's is"
'(%compound (%array int 3) 10)
(walk-expr '#((¤ int 3) : 10)))
(test "`.field value' is a designated initializer, kebab and all"
'(%compound (struct named) (%designate n 7) (%designate first_name "zoe"))
(walk-expr '#(struct named : .n 7 .first-name "zoe")))
(test "positional and designated may be mixed"
'(%compound (struct point) 1 (%designate y 5))
(walk-expr '#(struct point : 1 .y 5)))
(test "designators work in an untyped initializer too"
'#((%designate y 5))
(walk-expr '#(.y 5)))
;;; Fn defs
(test
'(%fun void puk ((int) (%array float 8)))
'(%fun void puk ((int #f) ((%array float 8) #f)))
(walk-fn-def '(fn puk ((int) (¤ float 8)) void)))
(test
@@ -150,7 +183,7 @@
(test
'(struct mega_kebab ((int a)
((struct ((int year) (int month) (int day))) dob)
((%fun int ((int) (%array int))) min)))
((%fun bool (int (%array int))) min)))
(walk-struct '(struct mega-kebab
((a int)
(dob (struct ((year int)

45
tests/infer.module.scm Normal file
View File

@@ -0,0 +1,45 @@
(module infer
(;; The IR
tvar?
tvar-id
tvar-classes
tvar-rigid?
fresh-tvar
fresh-rigid-tvar
prim-type? prim-name prim-quals make-prim
ptr-type? ptr-target ptr-quals make-ptr
array-type? array-elt array-size make-array-type
fn-type? fn-ret fn-args fn-variadic? make-fn-type
agg-type? agg-kind agg-name agg-spelling agg-quals make-agg
alias-type? alias-name alias-expansion alias-quals make-alias
unknown-type? the-unknown-type
resolve
underlying
c-primitive?
type-quals
free-tvars
decay
;; The boundary
parse-type
unparse-type
;; Constraints
register-class!
add-instance!
entails?
default-tvar!
default-type-variables!
;; Unification
unify
;; Type schemes
scheme? scheme-vars scheme-constraints scheme-type
make-scheme
generalize
instantiate
substitute)
"../infer.scm")

319
tests/infer.scm Normal file
View File

@@ -0,0 +1,319 @@
;;; Type inference, layer 0.
;;;
;;; Names registered in the type database are prefixed, since the
;;; database is one table shared by every suite in the linked binary.
(import infer types (chicken sort))
;;; Parse and print a surface type again. Everything in this suite goes
;;; through this pair, which is deliberate: they are the only thing the
;;; rest of the compiler will ever see of the IR.
(define (round-trip surface)
(unparse-type (parse-type surface)))
(test-group "infer"
(test-group "round-trip"
;; Every spelling below appears in example/ or tests/, or is one
;; the C writer documents in walk-type. `type-match' compares types
;; with equal?, so a near miss here is not a cosmetic bug -- it is
;; a reflection macro silently falling into its else branch.
(for-each
(lambda (surface)
(test (conc "round-trips: " surface) surface (round-trip surface)))
'(int
void
char
float
double
size-t
GLfloat
(unsigned int)
(long long)
(const int)
(const char)
(* char)
(* void)
(* const char)
(* * char)
(* const * const char)
(const * const char)
(* FILE)
(* SDL-Window)
(struct point)
(struct list-int)
(union value)
(enum mood)
(const struct list-int)
(* struct list-int)
(* const struct point)
(¤ int 16)
(¤ char 512)
(¤ GLfloat 15)
(¤ float)
(¤ * const char)
(¤ * const struct res 32)
(¤ (¤ const char))
(fn () void)
(fn ((int)) int)
(fn ((int) (int)) int)
(fn ((* const char)) size-t)
(fn ((* const char) (...)) int)
(fn ((¤ float 4)) void)))
;; Grouping parens are not part of the type, so these come back
;; canonicalised rather than verbatim -- which is the whole reason
;; unparse-type exists rather than "keep what was written".
(test "a grouped element is the same array" '(¤ int 16) (round-trip '(¤ (int) 16)))
(test "a grouped base is the same pointer" '(* const char)
(round-trip '(* (const char))))
(test "a grouped aggregate keeps its qualifier" '(const struct point)
(round-trip '(const (struct point))))
;; A typedef is transparent to unification and opaque to printing:
;; the generated declaration has to say what the programmer said.
(add-typedef 'i-handle '(typedef i-handle int))
(test "a typedef prints as itself" 'i-handle (round-trip 'i-handle))
(test "and qualified, as itself" '(const i-handle) (round-trip '(const i-handle)))
(test "and under a pointer" '(* i-handle) (round-trip '(* i-handle))))
(test-group "wildcards"
(test "a bare _ is a variable" '_ (round-trip '_))
(test "and composes under a pointer" '(* _) (round-trip '(* _)))
(test "and inside an array" '(¤ _ 4) (round-trip '(¤ _ 4)))
(test "and in a signature" '(fn ((_)) _) (round-trip '(fn ((_)) _)))
;; Each _ is its own variable: solving one must not solve the rest.
(let ((t (parse-type '(fn ((_)) _))))
(unify (car (fn-args t)) (parse-type 'int) #f)
(test "one hole at a time" '(fn ((int)) _) (unparse-type t))))
(test-group "structure"
(test-assert "a pointer is a pointer" (ptr-type? (parse-type '(* char))))
(test "and knows what it points at" 'char
(unparse-type (ptr-target (parse-type '(* char)))))
(test "quals sit on the level they were written at" '(const)
(ptr-quals (parse-type '(const * char))))
(test "an unsized array has no size" #f (array-size (parse-type '(¤ int))))
(test "a sized one does" 16 (array-size (parse-type '(¤ int 16))))
(test "an aggregate is nominal" 'point (agg-name (parse-type '(struct point))))
(test-assert "a variadic signature says so"
(fn-variadic? (parse-type '(fn ((* const char) (...)) int))))
(test-assert "and a plain one does not"
(not (fn-variadic? (parse-type '(fn ((int)) int)))))
;; decay: the conversion C performs at a call site, an operand of
;; `+', or the left half of a subscript.
(test "an array decays to a pointer" '(* int) (unparse-type (decay (parse-type '(¤ int 16)))))
(test "a function decays to a pointer to itself" '(* (fn ((int)) int))
(unparse-type (decay (parse-type '(fn ((int)) int)))))
(test "anything else is left alone" 'int (unparse-type (decay (parse-type 'int)))))
(test-group "unification"
(test-assert "a type unifies with itself"
(unify (parse-type 'int) (parse-type 'int) #f))
(test-error "and not with another one"
(unify (parse-type 'int) (parse-type 'char) #f))
(let ((a (fresh-tvar)))
(unify a (parse-type '(* const char)) #f)
(test "a variable takes the shape it is unified with"
'(* const char) (unparse-type a)))
;; The point of the exercise: `(var p (* _) (& x))' with x : int.
(let ((p (parse-type '(* _))))
(unify p (parse-type '(* int)) #f)
(test "a partial type is completed by one step" '(* int) (unparse-type p)))
(let ((a (fresh-tvar))
(b (fresh-tvar)))
(unify a b #f)
(unify b (parse-type 'double) #f)
(test "two variables joined then solved" 'double (unparse-type a)))
(test-error "structure has to match"
(unify (parse-type '(* int)) (parse-type '(* char)) #f))
(test-error "and arity"
(unify (parse-type '(fn ((int)) int))
(parse-type '(fn ((int) (int)) int)) #f))
(test-error "and aggregates are told apart by name"
(unify (parse-type '(struct point)) (parse-type '(struct box)) #f))
(test-error "and by kind"
(unify (parse-type '(struct point)) (parse-type '(union point)) #f))
;; An unwritten array length constrains nothing, the way it does
;; not in C either.
(test-assert "an unsized array unifies with a sized one"
(unify (parse-type '(¤ int)) (parse-type '(¤ int 16)) #f))
(test-error "but two written lengths must agree"
(unify (parse-type '(¤ int 4)) (parse-type '(¤ int 16)) #f))
;; A typedef unifies as whatever it stands for.
(add-typedef 'i-count '(typedef i-count int))
(test-assert "a typedef unifies with its target"
(unify (parse-type 'i-count) (parse-type 'int) #f))
(let ((a (fresh-tvar)))
(unify a (parse-type 'i-count) #f)
(test "and keeps its name when it is the one printed"
'i-count (unparse-type a)))
(test-group "the unknown type"
(test-assert "? is consistent with anything"
(unify the-unknown-type (parse-type '(struct point)) #f))
(test-assert "in either order"
(unify (parse-type 'int) the-unknown-type #f))
;; ...and binds nothing. Degrading to ? is what keeps an
;; unparsed C declaration from poisoning everything it touches.
(let ((a (fresh-tvar)))
(unify a the-unknown-type #f)
(test "a variable met with ? stays open" '_ (unparse-type a))))
(test-group "occurs check"
;; Unreachable without recursive types, and the alternative to
;; having it is not an error but a hang.
(let ((a (fresh-tvar)))
(test-error "a variable may not contain itself"
(unify a (make-ptr a (list)) #f))))
(test-group "rigid variables"
(let ((r (fresh-rigid-tvar))
(a (fresh-tvar)))
(test-error "a type parameter does not unify with a type"
(unify r (parse-type 'int) #f))
(test-assert "an ordinary variable binds to it instead"
(unify a r #f))
;; An unsolved variable resolves to itself.
(test-assert "it is still open" (tvar? (resolve r))))
;; ...and the same the other way round: it is which side is free
;; that decides, not which side was written first.
(let ((r (fresh-rigid-tvar))
(a (fresh-tvar)))
(test-assert "rigid first binds the free one" (unify r a #f))
(test-assert "to the parameter itself" (eq? r (resolve a))))
(let ((r1 (fresh-rigid-tvar))
(r2 (fresh-rigid-tvar)))
(test-error "two parameters do not unify with each other"
(unify r1 r2 #f)))))
(test-group "constraints"
(test #t (entails? 'numeric (parse-type 'int)))
(test #t (entails? 'numeric (parse-type '(unsigned long))))
(test #t (entails? 'integral (parse-type 'char)))
(test #f (entails? 'integral (parse-type 'double)))
(test #t (entails? 'floating (parse-type 'double)))
(test #f (entails? 'floating (parse-type 'int)))
(test #f (entails? 'numeric (parse-type '(* char))))
(test #t (entails? 'scalar (parse-type '(* char))))
(test #f (entails? 'numeric (parse-type 'void)))
(add-enum 'i-mood '(enum i-mood (glad sad)))
(test "an enum is an integer" #t (entails? 'integral (parse-type '(enum i-mood))))
;; The third answer, and the important one. A name from a header
;; might well be numeric; #f would reject working programs and #t
;; would invent knowledge.
(test "an unparsed C name is not known either way"
'unknown (entails? 'numeric (parse-type 'size-t)))
(test "nor is an open variable" 'unknown (entails? 'numeric (fresh-tvar)))
(test "? entails nothing, but says so quietly"
'unknown (entails? 'numeric the-unknown-type))
;; A typedef is entailed by what it resolves to, so an alias cannot
;; sneak past a constraint its target would fail.
(add-typedef 'i-len '(typedef i-len int))
(test "a typedef is judged by its target" #t (entails? 'numeric (parse-type 'i-len)))
;; A constrained variable checks its classes at the moment it is
;; solved, not at the end.
(let ((a (fresh-tvar '(numeric))))
(test-error "solving to a type that fails the class is an error"
(unify a (parse-type '(* char)) #f)))
(let ((a (fresh-tvar '(numeric))))
(test-assert "and to one that satisfies it is not"
(unify a (parse-type 'double) #f)))
;; ...but an unparsed name is not a failure, it is an absence of
;; knowledge, and must stay silent.
(let ((a (fresh-tvar '(numeric))))
(test-assert "an unparsed C name does not trip a constraint"
(unify a (parse-type 'GLuint) #f)))
;; Joining two variables joins what is known about both.
(let ((a (fresh-tvar '(numeric)))
(b (fresh-tvar '(integral))))
(unify a b #f)
(test "constraints merge when variables do"
'("integral" "numeric")
(sort (map symbol->string (tvar-classes b)) string<?)))
(test-group "defaulting"
(let ((a (fresh-tvar '(numeric))))
(default-type-variables! a #f)
(test "an open numeric is an int" 'int (unparse-type a)))
(let ((a (fresh-tvar '(floating))))
(default-type-variables! a #f)
(test "an open floating is a double" 'double (unparse-type a)))
(let ((a (fresh-tvar)))
(test "a variable with nothing known about it cannot be defaulted"
#f (default-type-variables! a #f))
(test "and stays a hole, for the caller to complain about"
'_ (unparse-type a)))
;; Defaulting reaches into the structure, since the hole may be
;; anywhere: `(var p (* _) ...)'.
(let ((t (parse-type '(* _))))
(unify (ptr-target t) (fresh-tvar '(numeric)) #f)
(default-type-variables! t #f)
(test "and it reaches inside a type" '(* int) (unparse-type t))))
(test-group "user classes"
;; A trait bound is the same shape as `numeric', discharged by
;; the same procedure -- that is the point of one representation.
;; It differs in two rules, and both are stated while there is
;; still only one kind of constraint to state them about.
(register-class! 'i-ord #f #f #t)
(add-struct 'i-circle '(struct i-circle ((r int))))
(test "no instance, no entailment" #f (entails? 'i-ord (parse-type '(struct i-circle))))
(add-instance! 'i-ord (parse-type '(struct i-circle)))
(test "and with one, entailment" #t (entails? 'i-ord (parse-type '(struct i-circle))))
;; Instances key on the resolved type, so an alias cannot be
;; registered twice under two names.
(add-typedef 'i-circle-alias '(typedef i-circle-alias (struct i-circle)))
(test "an alias of an instance is the same instance"
#t (entails? 'i-ord (parse-type 'i-circle-alias)))
;; Decision 2: static dispatch cannot select an instance for a
;; type it does not know, so a strict class says no to ? rather
;; than shrugging the way `numeric' does.
(test "a strict class refuses ?" #f (entails? 'i-ord the-unknown-type))
(test "where a lenient one abstains" 'unknown (entails? 'numeric the-unknown-type))
;; ...and it has no default to fall back on.
(let ((a (fresh-tvar '(i-ord))))
(test-error "an unresolved user constraint is an error, not a guess"
(default-type-variables! a #f)))))
(test-group "schemes"
;; Nothing generalizes yet. The shape is here because a scheme
;; without a constraint list is the wrong shape for every bounded
;; generic, and because instantiation is how a generic body gets
;; fresh cells instead of a second type for the same one.
(let* ((a (fresh-tvar '(numeric)))
(id (make-fn-type a (list a) #f))
(s (generalize id (list))))
(test "the free variable is quantified" 1 (length (scheme-vars s)))
(test "carrying its class as the scheme's context"
'((numeric)) (list (map car (scheme-constraints s))))
(let ((one (instantiate s))
(two (instantiate s)))
(test "an instantiation is still open" '(fn ((_)) _) (unparse-type one))
(unify (fn-ret one) (parse-type 'int) #f)
(test "solving one instantiation" '(fn ((int)) int) (unparse-type one))
(test "leaves the other alone" '(fn ((_)) _) (unparse-type two))
(test "and the scheme itself untouched" '(fn ((_)) _) (unparse-type id))))
(let* ((a (fresh-tvar))
(b (fresh-tvar))
(s (generalize (make-fn-type a (list b) #f) (list b))))
(test "a variable free in the environment is not quantified"
1 (length (scheme-vars s)))
(let ((inst (instantiate s)))
(unify (car (fn-args inst)) (parse-type 'char) #f)
(test "so instantiating solves it for everyone" 'char (unparse-type b))))))

View File

@@ -10,14 +10,17 @@
#
# 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.
# reads the imported struct's fields; and that a closure type crossing
# the boundary works both ways -- one built in the module and called
# here, one built here and called there, through a code pointer that is
# static in the other object.
#
# 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
EXPECTED = hello, world\nhello, sex\n2 greetings\ntext times \ntext times \nmood 1
EXPECTED = hello, world\nhello, sex\n2 greetings\ntext times \ntext times \nmood 1\nclosure 15 21 201
check:
@$(SEXC) greet.sex -c -o greet.o

View File

@@ -26,4 +26,11 @@
(printf "\n")
(var m (enum mood) grumpy)
(printf "mood %d\n" m)
(var add-10 (closure ((int)) int) (make-adder 10))
(printf "closure %d %d" (add-10 5) (apply-twice add-10 1))
(var base int 100)
(var here (closure ((int)) int)
(closure ((x int)) int (base) (return (+ base x))))
(printf " %d\n" (apply-twice here 1))
(return 0))

View File

@@ -13,7 +13,9 @@
;; 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))
@@ -29,9 +31,23 @@
(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))
;;; A closure type crossing the boundary. Both units generate the
;;; struct for this signature independently, so they have to agree on
;;; its tag and its layout, or the value is passed wrong and nothing
;;; says so.
(pub fn make-adder ((n int)) (closure ((int)) int)
(return (closure ((b int)) int (n)
(return (+ n b)))))
;;; The other direction: a closure built by the importer, whose code
;;; pointer is static in *its* object, called from here
(pub fn apply-twice ((f (closure ((int)) int)) (x int)) int
(return (f (f x))))
;;; Not `pub': invisible to importers, and static in the generated C.
(fn unused-helper () void
(printf "private\n"))

View File

@@ -10,6 +10,7 @@
(include "codegen.scm")
(include "args.scm")
(include "types.scm")
(include "infer.scm")
;;; Should be the last in the test suite
(test-exit)

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,78 @@
'((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))))))
;; Includes
(test '((include stdio.h))
(semen-process
'((include stdio.h))))
(test '((include stdio.h)
(include stdlib.h))
(semen-process
'((include stdio.h
stdlib.h))))
(test '()
(semen-process
'((include))))
)

View File

@@ -0,0 +1,28 @@
(compilation "-- -std=c99 -pedantic-errors")
(input)
(output "c99: 42")
(return 0)
;;; The closure environment is part of the ABI, so its union is declared
;;; in every translation unit whether or not one is used. That put
;;; whatever it was written with into every program: `max_align_t' named
;;; the alignment in one word, and made C11 the floor for a program with
;;; no closure in it at all.
;;;
;;; The widest built-ins say the same thing -- a union is aligned for the
;;; strictest of its members -- and say it in C99.
;;;
;;; Closures themselves still want C11 for the `_Static_assert' that
;;; checks the captures fit, so this program keeps clear of them.
(include stdio.h)
(struct point ((x int) (y int)))
(fn area ((p (struct point))) int
(return (* (. p x) (. p y))))
(pub fn main () int
(var p (struct point) #((struct point) : 6 7))
(printf "c99: %d\n" (area p))
(return 0))

View File

@@ -0,0 +1,52 @@
(input)
(output "one argument of two words: 7"
"two arguments of one: 7"
"unsigned, one argument: 9"
"unsigned, two arguments: 3"
"captured global: 12"
"captured function: 8")
(return 0)
;;; A closure's struct is named after its signature, so that two
;;; translation units agree on it without sharing a header. The name is
;;; built by flattening the argument list, and flattening loses where one
;;; argument ends and the next begins: `((long long))' and
;;; `((long) (long))' are different signatures that used to mangle alike,
;;; and the second quietly reused the first one's struct.
;;;
;;; A capture that borrows a name reads it from wherever the name is
;;; declared, a global or a function included.
(include stdio.h)
(var scale int 3)
(fn double-it ((n int)) int
(return (* n 2)))
(pub fn main () int
(var one-wide (closure ((long long)) int)
(closure ((a (long long))) int () (return (cast a int))))
(printf "one argument of two words: %d\n" (one-wide 7))
(var two-longs (closure ((long) (long)) int)
(closure ((a long) (b long)) int () (return (cast (+ a b) int))))
(printf "two arguments of one: %d\n" (two-longs 3 4))
(var one-unsigned (closure ((unsigned int)) int)
(closure ((a (unsigned int))) int () (return (cast a int))))
(printf "unsigned, one argument: %d\n" (one-unsigned 9))
(var two-unsigned (closure ((unsigned) (int)) int)
(closure ((a unsigned) (b int)) int () (return (+ (cast a int) b))))
(printf "unsigned, two arguments: %d\n" (two-unsigned 1 2))
;; a capture names what it borrows, and the name need not be a local
(var scaled (closure ((int)) int)
(closure ((x int)) int (scale) (return (* x scale))))
(printf "captured global: %d\n" (scaled 4))
(var doubled (closure ((int)) int)
(closure ((x int)) int (double-it) (return (double-it x))))
(printf "captured function: %d\n" (doubled 4))
(return 0))

View File

@@ -0,0 +1,121 @@
(input)
(output "Adders: 15 25"
"Two captures: 47"
"No captures: 7"
"Through a parameter: 110"
"From an array: 1 2 3"
"Index evaluated once: 21 i 1"
"Through a struct member: 8"
"Shadowed in a block: 9 then 15"
"Nested: 33"
"Named capture: 7"
"Captured pointer: 11 then 12"
"From a bare fn: 20 42 7")
(return 0)
;;; A closure is a code pointer beside its captures, so what this
;;; checks is that the captures survive the lifting -- that two
;;; closures of one shape keep their own environments, that a closure
;;; outlives the call that built it, and that calling one through a
;;; parameter, an array element or a struct member resolves the same
;;; way as through a local.
;;;
;;; A receiver is also an ordinary expression: `[table (++ i)]' has to
;;; evaluate its index exactly once, the way it would for an array of
;;; function pointers.
(include stdio.h)
(fn double-it ((n int)) int
(return (* n 2)))
;;; a bare function is a closure that captures nothing, so it converts
;;; wherever one is expected -- here a declared return type
(fn as-closure () (closure ((int)) int)
(return double-it))
(fn make-adder ((n int)) (closure ((int)) int)
(return (closure ((b int)) int (n)
(return (+ n b)))))
(fn make-affine ((k int) (b int)) (closure ((int)) int)
(return (closure ((x int)) int (k b)
(return (+ (* k x) b)))))
(fn make-const-7 () (closure () int)
(return (closure () int ()
(return 7))))
;;; A closure arriving as a parameter: its type is written, so the call
;;; resolves without knowing where it came from
(fn apply-twice ((f (closure ((int)) int)) (x int)) int
(return (f (f x))))
(struct handlers ((on-tick (closure ((int)) int))))
(pub fn main () int
(var add-10 (closure ((int)) int) (make-adder 10))
(var add-20 (closure ((int)) int) (make-adder 20))
(printf "Adders: %d %d\n" (add-10 5) (add-20 5))
(var affine (closure ((int)) int) (make-affine 5 2))
(printf "Two captures: %d\n" (affine 9))
(var seven (closure () int) (make-const-7))
(printf "No captures: %d\n" (seven))
(printf "Through a parameter: %d\n" (apply-twice (make-adder 50) 10))
(var table (¤ (closure ((int)) int) 3))
(var i int 0)
(for (= i 0) (< i 3) (++ i)
(= (¤ table i) (make-adder i)))
(printf "From an array: %d %d %d\n"
((¤ table 0) 1) ((¤ table 1) 1) ((¤ table 2) 1))
;; the index must be evaluated once, so `i' ends at 1 and not 2 --
;; read in a separate statement, since reading and bumping it in one
;; printf would be unsequenced whatever the closure did
(= i 0)
(var once int ([table (++ i)] 20))
(printf "Index evaluated once: %d i %d\n" once i)
(var h (struct handlers) #((struct handlers) : .on-tick (make-adder 5)))
(printf "Through a struct member: %d\n" ((. h on-tick) 3))
;; a block opens a scope: the inner `add-10' ends with it, and the
;; call after it is the closure again
(do (var add-10 int 9)
(printf "Shadowed in a block: %d then " add-10))
(printf "%d\n" (add-10 5))
;; a closure built inside a closure, capturing that one's capture
(var outer (closure ((int)) int)
(closure ((x int)) int ()
(var inner (closure ((int)) int) (make-adder x))
(return (inner 3))))
(printf "Nested: %d\n" (outer 30))
;; a capture can name what it holds rather than borrow a variable's
;; name, and the expression is evaluated where the closure is written
(var pt (struct handlers))
(var sum-once (closure () int)
(closure () int ((sum (+ 3 4))) (return sum)))
(printf "Named capture: %d\n" (sum-once))
;; capturing a pointer is how by-reference is spelled; the caller owns
;; what it points at
(var counter int 11)
(var peek (closure () int)
(closure () int ((at (& counter))) (return (* at))))
(printf "Captured pointer: %d then " (peek))
(++ counter)
(printf "%d\n" (peek))
;; ...and in an initializer, as an argument, and as a return
(var from-fn (closure ((int)) int) double-it)
(printf "From a bare fn: %d %d %d\n"
(apply-twice from-fn 5)
((as-closure) 21)
(apply-twice (lambda ((n int)) int (return (+ n 1))) 5))
(return 0))

View File

@@ -0,0 +1,49 @@
(input)
(output "plain 1 2"
"literal 3 4"
"designated zoe 7"
"through a pointer 9"
"array 10 20 30"
"argument 6"
"mixed 1 5")
(return 0)
;;; `:' inside #(...) ends a type and makes the rest a C99 compound
;;; literal. Without one, #(...) is the brace initializer it always was.
(include stdio.h)
(struct point ((x int) (y int)))
(struct named ((first-name (* const char)) (n int)))
(fn sum ((p (struct point))) int
(return (+ (. p x) (. p y))))
(pub fn main () int
;; unchanged: a bare initializer has no type of its own
(var p (struct point) #(1 2))
(printf "plain %d %d\n" (. p x) (. p y))
(var q (struct point) #(struct point : 3 4))
(printf "literal %d %d\n" (. q x) (. q y))
;; designated, out of declaration order, and kebab-cased
(var r (struct named) #(struct named : .n 7 .first-name "zoe"))
(printf "designated %s %d\n" (. r first-name) (. r n))
;; a compound literal is an lvalue, so its address can be taken --
;; until the end of the enclosing block, and no longer
(var pp (* struct point) (& #(struct point : 9 9)))
(printf "through a pointer %d\n" (-> pp x))
;; an array literal decays the way an array does
(var a (* int) #([int 3] : 10 20 30))
(printf "array %d %d %d\n" (¤ a 0) (¤ a 1) (¤ a 2))
(printf "argument %d\n" (sum #(struct point : 2 4)))
;; positional and designated may be mixed, as in C
(var m (struct point) #(struct point : 1 .y 5))
(printf "mixed %d %d\n" (. m x) (. m y))
(return 0))

View File

@@ -1,6 +1,6 @@
(compilation "-f alpha --features beta --features=gamma --no-platform-features")
(compilation "-f alpha -f beta,gamma --features=delta --no-platform-features")
(input)
(output "alpha" "beta" "gamma" "elsewhere")
(output "alpha" "beta" "gamma" "delta" "elsewhere")
(return 0)
;;; The flags naming the features, in every spelling sexc takes. This
@@ -19,6 +19,7 @@
#+alpha (puts "alpha")
#+beta (puts "beta")
#+gamma (puts "gamma")
#+delta (puts "delta")
#-unix (puts "elsewhere")
#+unix (puts "here")
(return 0))

View File

@@ -0,0 +1,61 @@
(input)
(output "direct: 120"
"fac: 120 3628800"
"fib: 55 6765"
"applied on the spot: 120")
(return 0)
;;; A fixed point built out of closures, which is the hardest thing to
;;; ask of them: recursion with no recursive function anywhere, only
;;; self-application.
;;;
;;; Self-application needs `x x' and so a recursive type, which is
;;; spelled here by routing it through a named struct whose field is a
;;; closure whose own signature mentions that struct. The generated
;;; closure struct is written before `struct rec' is, so this only
;;; compiles because a forward declaration is emitted ahead of both.
;;;
;;; Note what `fix' captures: a *pointer* to the knot, not the knot. A
;;; closure is a code pointer beside N bytes of environment, so
;;; capturing one by value would need N >= 8 + N. No budget makes that
;;; true, and the static assertion says so rather than letting it
;;; corrupt anything.
(include stdio.h)
(struct rec ((f (closure (((* (struct rec))) (int)) int))))
;;; Takes a step that expects itself, returns an ordinary closure with
;;; the self-application hidden inside
(fn fix ((step (* (struct rec)))) (closure ((int)) int)
(return (closure ((n int)) int (step)
(return ((-> step f) step n)))))
(pub fn main () int
(var fac-knot (struct rec))
(= (. fac-knot f)
(closure ((self (* (struct rec))) (n int)) int ()
(if (<= n 1) (return 1))
(return (* n ((-> self f) self (- n 1))))))
;; the knot applied to itself directly, without fix
(printf "direct: %d\n" ((. fac-knot f) (& fac-knot) 5))
(var fib-knot (struct rec))
(= (. fib-knot f)
(closure ((self (* (struct rec))) (n int)) int ()
(if (< n 2) (return n))
(return (+ ((-> self f) self (- n 1))
((-> self f) self (- n 2))))))
;; one combinator, two different recursions
(var fac (closure ((int)) int) (fix (& fac-knot)))
(var fib (closure ((int)) int) (fix (& fib-knot)))
(printf "fac: %d %d\n" (fac 5) (fac 10))
(printf "fib: %d %d\n" (fib 10) (fib 20))
;; the combinator's result invoked where it is returned, with no
;; intervening `var' -- the receiver's type is the return type of the
;; signature it came from, which is what the name table records
(printf "applied on the spot: %d\n" ((fix (& fac-knot)) 5))
(return 0))

View File

@@ -0,0 +1,178 @@
(input)
(output "15"
"42 0.25"
"(5, 7)"
"(1, 2)"
"3"
"(5, 7)"
"0 1 4 9 "
"4"
"42"
"<closure of 0: 10>"
"42"
"2 1"
"Hello from Sex!")
(return 0)
;;; Both halves of inference, from the two sides that read the same
;;; answers: `_' in a type means "work it out", and `type-of' hands a
;;; macro the type of an expression.
;;;
;;; This was written as a draft before either existed, to be read
;;; before it was built. It is registered now.
;;;
;;; Two features, one mechanism:
;;;
;;; `_' in a type means "work it out", and
;;; `type-of' hands a macro the type of an expression.
;;;
;;; Both are the same solved constraint store, read from two sides.
(include stdio.h)
(include string.h)
;;; `(include string.h)' is for the C compiler; it tells Sex nothing.
;;; A signature has to be written before `_' can be resolved from a
;;; call to `strlen' -- without one the call's type is `?', and a `_'
;;; that resolves to `?' is an error, not a silent int.
(extern fn strlen ((s (* const char))) size-t)
(struct point ((x int) (y int)))
(fn midpoint ((a (* const struct point)) (b (* const struct point))) (struct point)
(var m (struct point))
;; No `_' here: `m' has no initializer to infer from. Inference fills
;; in a type, it does not invent one.
(= (. m x) (/ (+ (-> a x) (-> b x)) 2))
(= (. m y) (/ (+ (-> a y) (-> b y)) 2))
(return m))
;;; The closure's type is written once, in the signature; `_' reads it
;;; from there at every use.
(fn make-adder ((n int)) (closure ((int)) int)
(return (closure ((b int)) int (n)
(return (+ n b)))))
;;; A macro that asks what it was handed.
;;;
;;; `type-of' returns a *surface* type -- the same spelling the type
;;; database hands to `map-fields' -- so it composes with the
;;; `type-match' that already exists, and dispatch over a user struct
;;; costs nothing extra.
(defmacro (print x)
(type-match (type-of x)
(int `(printf "%d\n" ,x))
(size-t `(printf "%zu\n" ,x))
(double `(printf "%g\n" ,x))
((* const char) `(printf "%s\n" ,x))
;; NOTE: ,x twice -- a macro that duplicates its argument still has
;; to think about evaluating it twice. Inference does not fix that.
((struct point) `(printf "(%d, %d)\n" (. ,x x) (. ,x y)))
;; a closure is a type like any other, so it dispatches like one --
;; and `_' saves a clause per signature
((closure _ int) `(printf "<closure of 0: %d>\n" (,x 0)))
(else (error "print: don't know how to print" (type-of x)))))
;;; The temporary's type is the thing the macro could not write down
;;; before. Either spelling works -- `_' is the lazier one, and it is
;;; inferred in the expansion's own scope.
(defmacro (swap a b)
`(do (var tmp _ ,a)
(= ,a ,b)
(= ,b tmp)))
(pub fn main () int
;; Written out, for contrast with everything below it.
(var greeting (* const char) "Hello from Sex!")
;; size-t, from the signature above -- not int, and not a guess.
(var n _ (strlen greeting))
(printf "%zu\n" n)
;; Literals carry a constraint, not a type: the int-ish one defaults
;; to int, and the mixed division joins to double the way C does.
(var count _ (+ 20 22))
(var half _ (/ 1.0 4))
(printf "%d %g\n" count half)
;; An aggregate initializer has no type of its own, so the type flows
;; in and has to be written. `(var origin _ #(3 4))' is an error --
;; there is nothing to infer from.
(var origin (struct point) #(3 4))
(var corner (struct point) #(7 10))
;; A compound literal is the way out of that rule: the `:' is where an
;; initializer stops needing a type from its context, so `_' has
;; something to read after all.
(var centre _ #((struct point) : 5 7))
(print centre)
;; ...and the same designated, which names fields instead of counting
;; positions.
(var offset _ #((struct point) : .x 1 .y 2))
(print offset)
;; A partial type: "a pointer to something". The something arrives
;; from the initializer. This is why the wildcard lives in the type
;; grammar rather than beside it -- it composes.
(var p (* _) (& origin))
;; Member access reads the same type database the macros do.
(var x _ (-> p x))
(printf "%d\n" x)
;; A call into a function Sex has actually parsed: the return type is
;; the whole answer, and `print' then dispatches on it.
(var mid _ (midpoint (& origin) (& corner)))
(print mid)
;; The loop variable, which is where `_' earns its keep most often.
(for (var i _ 0) (< i 4) (++ i)
(printf "%d " (* i i)))
(printf "\n")
;; Four of something: the element type is fixed by the initializer,
;; the count by the type. Subscripting gives the element type back.
(var squares [_ 4] #(0 1 4 9))
(print [squares 2])
;; A closure's type comes from the signature that produced it, and
;; calling one needs that type and nothing else.
(var add-10 _ (make-adder 10))
(print (add-10 32))
(print add-10)
;; ...including where it is returned, with no name in between.
(print ((make-adder 20) 22))
;; A macro writing a declaration it could not have written before.
(var a _ 1)
(var b _ 2)
(swap a b)
(printf "%d %d\n" a b)
(print greeting)
(return 0))
;;; Open questions this draft raises, to settle before Layer 2 ships:
;;;
;;; 1. SETTLED. `type-match' took `_' on the pattern side, so
;;; `(closure _ int)' above is one clause rather than one per
;;; signature, and `(* _)' and `(¤ _ _)' say "any pointer" and "any
;;; array". A `_' written last takes the rest, since a type's words
;;; are spread and not nested: `(* const char)' is three elements.
;;; Nothing destructures -- a macro body is Scheme and a type is a
;;; list, so `(caddr (type-of x))' reads an array's length.
;;;
;;; 2. SETTLED, allowed. `[_ 4]' against `#(0 1 4 9)' unifies each
;;; element with the hole, so the element type comes from the
;;; literals and the length stays as written -- `[_ 10]' with two
;;; initializers is still ten. Elements that disagree are a type
;;; mismatch. A bare `_' is still refused: `#(0 1 4 9)' has no type
;;; of its own, only elements.
;;;
;;; 3. POSTPONED to the standard library design. `(extern fn strlen
;;; ...)' above duplicates string.h, which is the same bargain every
;;; FFI makes, but it is where "no C header parsing" starts costing
;;; the user something. A `sex/libc' module of prototypes is the
;;; obvious answer and belongs with the rest of the stdlib.

View File

@@ -0,0 +1,44 @@
(input)
(output "Named fn through a pointer: 30"
"Lambda through a pointer: 30"
"Lambda called in place: 130"
"Nested lambdas: 666")
(return 0)
;;; Lambdas are lifted into toplevel functions by semen, so what this
;;; really checks is that the lifted `fn' comes out in the argument
;;; order the writer expects -- (fn name arglist ret-type . body).
(include stdio.h)
(fn sum ((a int) (b int)) int
(return (+ a b)))
(pub fn main () int
(var a int 10)
(var b int 20)
(var sum-fn (fn ((int) (int)) int) sum)
(printf "Named fn through a pointer: %d\n" (sum-fn a b))
(var sum-lambda (fn ((int) (int)) int)
(lambda ((a int) (b int)) int
(return (+ a b))))
(printf "Lambda through a pointer: %d\n" (sum-lambda a b))
(printf "Lambda called in place: %d\n"
((lambda ((a int) (b int)) int
(return (+ a b 100)))
a b))
;; A lambda inside a lambda: the inner one is lifted out of a
;; function that is itself being lifted
(var outer (fn ((int)) int)
(lambda ((x int)) int
(var inner (fn ((int)) int)
(lambda ((y int)) int
(return (+ 60 y))))
(return (+ 600 (inner x)))))
(printf "Nested lambdas: %d\n" (outer 6))
(return 0))

View File

@@ -0,0 +1,90 @@
(input)
(output "logical: 1 1 0"
"bitwise: 7 2 5"
"shifts: 48 0 12"
"increment: 7"
"decayed: 2 2 there"
"unsigned wins either way: 4294967295 4294967295"
"toplevel: 1 2.5 hi 12"
"closure in place: 5"
"a type is not a call: 7")
(return 0)
;;; The walk types an expression by its head, and the heads it had a
;;; rule for were the ones inference was written against. `&&', the
;;; bitwise operators, the shifts and `++' were not among them and each
;;; stopped with `cannot infer'.
;;;
;;; The rest of this is the same mistake in three other places: an array
;;; is a pointer the moment it is an operand, a rank tie is not decided
;;; by which operand was written first, and `_' is not a local's
;;; privilege.
(include stdio.h)
(fn area ((w int) (h int)) int
(return (* w h)))
;;; a toplevel `_' reads the same table a local's does, so it can name
;;; anything declared above it
(var n _ 1)
(var d _ 2.5)
(var s _ "hi")
(var a _ (+ 3 (* 3 3)))
(fn make-adder ((k int)) (closure ((int)) int)
(return (closure ((b int)) int (k) (return (+ k b)))))
(pub fn main () int
(var x int 6)
(var y int 3)
(var ok _ (&& x y))
(var orr _ (|| x y))
(var neg _ (! x))
(printf "logical: %d %d %d\n" ok orr neg)
(var bor _ (| x y))
(var band _ (& x y))
(var bxor _ (^ x y))
(printf "bitwise: %d %d %d\n" bor band bxor)
;; a shift is the promoted left operand, not a join: the right one
;; says only how far
(var c char 12)
(var shl _ (<< x y))
(var shr _ (>> x y))
(var wide _ (>> c 0))
(printf "shifts: %d %d %d\n" shl shr wide)
;; ...and an increment is the operand, unpromoted
(var inc _ (++ x))
(printf "increment: %d\n" inc)
;; an array operand decays, so this is a pointer and not an array
(var xs (¤ int 4) #(1 2 3 4))
(var p _ (+ xs 1))
(var q _ (+ 1 xs))
(var names (¤ (* const char) 2) #("hi" "there"))
(var np _ (+ names 1))
(printf "decayed: %d %d %s\n" (* p) (* q) (* np))
;; at equal rank C takes the unsigned operand, whichever side it is on
(var i int -1)
(var u (unsigned int) 1)
(var u1 _ (+ i u))
(var u2 _ (+ u i))
(printf "unsigned wins either way: %u %u\n" (- u1 1) (- u2 1))
(printf "toplevel: %d %g %s %d\n" n d s a)
;; a closure literal is its own type, so it can be called where it is
;; written, the way a lambda already could
(printf "closure in place: %d\n"
((closure ((v int)) int () (return v)) 5))
;; a `fn' type's parameter list looks exactly like a call; with a
;; closure named `f' in scope it used to be read as one
(var f (closure ((int)) int) (make-adder 1))
(var fp (fn ((f int) (g int)) int) area)
(printf "a type is not a call: %d\n" (f 6))
(return 0))

View File

@@ -0,0 +1,72 @@
(input)
(output "aggregate element: 3 4"
"pointer element: there"
"multi-word element: 9"
"through a pointer: 55"
"unsized of a typedef: 1 2"
"unsized of a pointer: 5"
"unnamed parameters: 7 -1 2")
(return 0)
;;; Three questions about a written type that used to be answered in
;;; three places and disagreed: is `(a b)' a named parameter or a bare
;;; type, is the last element of a `¤' its bound or the last word of
;;; its element type, and what is one element of an array.
;;;
;;; They are one question -- where does the type end -- so the answer
;;; lives in `types' and everything else asks it.
(include stdio.h)
(struct point ((x int) (y int)))
(typedef small int)
;;; a parameter that names nothing is a type, however many words it
;;; takes: `(unsigned int)' is one of them, not a `unsigned' called
;;; `int'
(fn width ((n unsigned int)) int
(return (cast n int)))
(fn sign ((c const char)) int
(if (== c #\a) (return -1))
(return 1))
(fn twice ((n small)) int
(return (* n 2)))
(pub fn main () int
;; an element keeps every word of its type, tag and all
(var pts (¤ (struct point) 2) #(#((struct point) : 1 2)
#((struct point) : 3 4)))
(var p _ (¤ pts 1))
(printf "aggregate element: %d %d\n" (. p x) (. p y))
(var names (¤ (* const char) 2) #("hi" "there"))
(var s _ (¤ names 1))
(printf "pointer element: %s\n" s)
(var nums (¤ unsigned int 3) #(7 8 9))
(var u _ (¤ nums 2))
(printf "multi-word element: %u\n" u)
;; subscripting a pointer answers the same as subscripting an array
(var q (* (struct point)) (& (¤ pts 0)))
(var r _ (¤ q 1))
(printf "through a pointer: %d\n" (+ (. r x) (* 13 (. r y))))
;; the last word of an unsized array's type is not its bound: neither
;; a typedef name nor the target of a `*' can be one
(var tail (¤ const small) #(1 2))
(printf "unsized of a typedef: %d %d\n" (¤ tail 0) (¤ tail 1))
(var one size-t 5)
(var sizes (¤ * size-t) #((& one)))
(var w _ (¤ sizes 0))
(printf "unsized of a pointer: %d\n" (cast (* w) int))
;; the same question in type position: `(fn ((unsigned int)) int)'
;; takes one parameter, not two
(var fp (fn ((unsigned int)) int) width)
(printf "unnamed parameters: %d %d %d\n" (fp 7) (sign #\a) (twice 1))
(return 0))

View File

@@ -0,0 +1,51 @@
(input)
(output "one word: 7"
"pointer: 2"
"aggregate: 3"
"array: 2.5"
"variadic: 1 two")
(return 0)
;;; A parameter that names nothing still has to reach the C writer as a
;;; type and a name, the name being absent. Handed the bare type
;;; instead, fmt-c read the type's own second word as the name -- so
;;; `(* const char)' came out `const char', which is a different
;;; function -- and a one-word type had no second word to read at all.
(include stdio.h)
(include stdarg.h)
(struct point ((x int) (y int)))
;;; declared here rather than included, so the prototype we emit is the
;;; one the C compiler checks the call against
(extern fn abs ((int)) int)
(extern fn strlen ((* const char)) size-t)
(fn origin-x ((p (* (struct point)))) int
(return (. (* p) x)))
(fn second-of ((xs (¤ float 4))) float
(return (¤ xs 1)))
(fn say ((fmt (* const char)) ...) void
(var ap va-list)
(va-start ap fmt)
(vprintf fmt ap)
(va-end ap))
(pub fn main () int
(printf "one word: %d\n" (abs -7))
(printf "pointer: %d\n" (cast (strlen "hi") int))
;; the same parameter lists written as types
(var p (struct point) #((struct point) : 3 4))
(var f (fn ((* (struct point))) int) origin-x)
(printf "aggregate: %d\n" (f (& p)))
(var xs (¤ float 4) #(1.5 2.5 3.5 4.5))
(var g (fn ((¤ float 4)) float) second-of)
(printf "array: %g\n" (g xs))
(say "variadic: %d %s\n" 1 "two")
(return 0))

View File

@@ -0,0 +1,109 @@
(input)
(output "literals: 42 3.5 hello"
"calls: 12"
"members: 1 2.5"
"pointers: 1 2.5"
"arrays: 30"
"loop: 0 1 2"
"shadowed: 9 then 42"
"still an int: 200"
"partial: 1 2.5"
"joined: 43.5 84 49 1"
"promoted: 200 60000 3705032704"
"from elements: 4 9 0")
(return 0)
;;; `_' as a type means "work it out from the initializer". What the
;;; pass can answer comes from declarations -- Sex writes a type at
;;; every binding site -- and from the signature of whatever a call
;;; names. A partial type like `(* _)' is solved by unifying what was
;;; written against what the initializer gives, so only the wildcard
;;; inside the spelling is filled in.
(include stdio.h)
(struct point ((x int) (y float)))
(fn area ((w int) (h int)) int
(return (* w h)))
(pub fn main () int
(var n _ 42)
(var f _ 3.5)
(var s _ "hello")
(printf "literals: %d %g %s\n" n f s)
(var a _ (area 3 4))
(printf "calls: %d\n" a)
(var p (struct point) #((struct point) : .x 1 .y 2.5))
(var px _ (. p x))
(var py _ (. p y))
(printf "members: %d %g\n" px py)
(var pp _ (& p))
(printf "pointers: %d %g\n" (-> pp x) (-> pp y))
(var table (¤ int 3))
(= (¤ table 0) 10)
(= (¤ table 1) 20)
(var first _ (¤ table 0))
(var second _ (¤ table 1))
(printf "arrays: %d\n" (+ first second))
;; a for opens a scope, and its initializer is declared inside it
(printf "loop:")
(for (var i _ 0) (< i 3) (++ i)
(printf " %d" i))
(printf "\n")
;; a block's declarations end with it, and so do the declarations of
;; everything else C brackets -- a `while' body is a block with no
;; `do' written around it
(do (var n _ 9)
(printf "shadowed: %d then " n))
(printf "%d\n" n)
(var wide int 200)
(while false (var wide char 1) (printf "%d" wide))
(if false (do (var wide char 1) (printf "%d" wide)))
;; a statement before the declaration: a label may not be followed by
;; one until C23
(switch a (case 1 (printf "") (var wide char 1) (printf "%d" wide) (break)))
;; a copy, not a sum: an arithmetic result would be promoted to `int'
;; whatever leaked, and say nothing
(var copy _ wide)
(printf "still an int: %d\n" copy)
;; a wildcard inside a written type: only it is solved
(var pp2 (* _) (& p))
(printf "partial: %d %g\n" (-> pp2 x) (-> pp2 y))
;; C's usual arithmetic conversions, far enough to answer `_'
(var d double 1.5)
(var l long 7)
(var g float 0.5)
(var mixed _ (+ n d))
(var same _ (+ n n))
(var wider _ (+ n l))
(var single _ (+ g g))
(printf "joined: %g %d %ld %g\n" mixed same wider single)
;; ...including the promotions, which two operands of one narrow type
;; are exactly where they show: `char' + `char' is an `int'
(var c1 char 100)
(var c2 char 100)
(var h1 short 30000)
(var narrow _ (+ c1 c2))
(var narrower _ (+ h1 h1))
(var kept (unsigned int) 4000000000)
(var unpromoted _ (+ kept kept))
(printf "promoted: %d %d %u\n" narrow narrower unpromoted)
;; a brace initializer has no type of its own, but its elements solve
;; the hole in the array type around it -- and the length stays as
;; written, whether or not every slot is initialized
(var squares (¤ _ 4) #(0 1 4 9))
(var sparse (¤ _ 8) #(0 1))
(printf "from elements: %d %d %d\n" (¤ squares 2) (¤ squares 3) (¤ sparse 7))
(return 0))

View File

@@ -6,10 +6,23 @@
add-define
type-match
type-pattern-matches?
map-fields
add-name-type!
get-name-type
type-of
current-type-of
get-return-type
get-type-info
get-tag-info
get-fields
get-underlying-type)
get-underlying-type
type-head?
named-arg?
typedef-name?
array-bound?
array-element-type)
"../types.scm")

View File

@@ -166,9 +166,55 @@
#f
(type-match 'float (int 'yes)))
;; `_' in a pattern matches anything in that position; written last it
;; takes the rest, since a type's words are spread and not nested
(test "a wildcard matches an atom"
'yes (type-match 'int (_ 'yes) (else 'no)))
(test "a pointer to anything"
'yes (type-match '(* int) ((* _) 'yes) (else 'no)))
(test "including one spelled with qualifiers"
'yes (type-match '(* const char) ((* _) 'yes) (else 'no)))
(test "an array of anything, any length"
'yes (type-match '(¤ int 4) ((¤ _ _) 'yes) (else 'no)))
(test "but a sized pattern does not match an unsized array"
'no (type-match '(¤ int) ((¤ _ _) 'yes) (else 'no)))
(test "an aggregate of any tag"
'yes (type-match '(struct point) ((struct _) 'yes) (else 'no)))
(test "and the keyword still has to agree"
'no (type-match '(union point) ((struct _) 'yes) (else 'no)))
(test "a closure of any signature"
'yes (type-match '(closure ((float)) int) ((closure _ _) 'yes) (else 'no)))
(test "an exact pattern is still exact"
'no (type-match '(* int) ((* const char) 'yes) (else 'no)))
(test "an undeclared name has no entry"
#f
(get-type-info 't-never-declared))
(test "and no fields"
#f
(get-fields 't-never-declared)))
(get-fields 't-never-declared))
;; What type a *name* has -- the third table, which functions and
;; variables share because a function type has a surface spelling
(add-name-type! 't-sum '(fn ((int) (int)) int))
(add-name-type! 't-origin '(struct t-point))
(test "a function's signature comes back whole"
'(fn ((int) (int)) int)
(get-name-type 't-sum))
(test "and a variable's type"
'(struct t-point)
(get-name-type 't-origin))
(test "the return type is what a call site wants"
'int
(get-return-type 't-sum))
(test "a variable has no return type"
#f
(get-return-type 't-origin))
;; not an error: this is how a name from an included C header looks,
;; and the caller decides what to make of it
(test "an undeclared name has no type"
#f
(get-name-type 't-never-declared))
(test "nor a return type"
#f
(get-return-type 't-never-declared)))

View File

@@ -86,8 +86,13 @@
(compiled-file (create-temporary-file)))
;; `process' returns one record; `process-input-port' is named from
;; the child's side, so it is the port we write to.
;;
;; Sex reads no symbol escaping -- `|' is an operator there. Left
;; on, `(|| a b)' leaves here as `(|\|\|| a b)' and reaches sexc
;; as a different symbol.
(let* ((proc (process compiler (append (list "-o" compiled-file) flags)))
(sexc-stdin (process-input-port proc)))
(symbol-escape #f)
(with-output-to-port sexc-stdin
(fn (map (fn (fmt #t x)) src)))
(close-output-port sexc-stdin)

View File

@@ -6,10 +6,23 @@
add-define
type-match
type-pattern-matches?
map-fields
add-name-type!
get-name-type
type-of
current-type-of
get-return-type
get-type-info
get-tag-info
get-fields
get-underlying-type)
get-underlying-type
type-head?
named-arg?
typedef-name?
array-bound?
array-element-type)
"types.scm")

146
types.scm
View File

@@ -21,6 +21,12 @@
(define +type-db+ (make-hash-table)) ; typedefs and defines
(define +tag-db+ (make-hash-table)) ; struct, union and enums
;;; What type a *name* has, which neither of the two above records:
;;;
;;; (fn sum ((a int) (b int)) int ...) -> (fn ((int) (int)) int)
;;; (var origin (struct point) ...) -> (struct point)
(define +name-db+ (make-hash-table))
(define (strip-pub form)
(if (eq? (car form) 'pub) (cdr form) form))
@@ -74,6 +80,34 @@
(hash-table-set! +type-db+ name
(list 'define name (cddr (strip-pub form)))))
;;; `(type-of x)' inside a macro body: the type of the expression the
;;; macro was handed, where `get-name-type' only answers for a name.
;;; The walker that can answer it lives in `semen', which is compiled
;;; after this, so it installs itself here for the length of one
;;; expansion. Outside one there is no scope to ask about, and the
;;; answer is #f.
(define current-type-of (make-parameter (lambda (form) #f)))
(define (type-of form) ((current-type-of) form))
(define (add-name-type! name type)
(hash-table-set! +name-db+ name type))
;;; #f for a name never declared, which is what `printf' looks like
;;; until something parses stdio.h. Not an error here; the caller
;;; decides.
(define (get-name-type name)
(hash-table-ref/default +name-db+ name #f))
;;; What `(make-adder 10)' has for a type: `make-adder's return type,
;;; or #f when NAME is not a function with a signature on record
(define (get-return-type name)
(let ((type (get-name-type name)))
(and (pair? type)
(eq? 'fn (car type))
(= 3 (length type))
(third type))))
(define (get-tag-info name)
(hash-table-ref/default +tag-db+ name #f))
@@ -142,21 +176,47 @@
;;; (int ...)
;;; ((* const char) ...)
;;; ([int 10] ...)
;;; ((* _) ...) ; a pointer to anything
;;; ((¤ _ _) ...) ; an array of anything, any length
;;; (else ...))
;;;
;;; A type is a form, not an atom, so this compares with equal? rather
;;; than dispatching like `case'. Patterns are literal types and are not
;;; evaluated; `else' is optional and the whole thing is #f when nothing
;;; matches and there is no else.
;;; A type is a form, not an atom, so patterns are matched structurally
;;; rather than dispatched on like `case'. They are literal types and
;;; are not evaluated; `else' is optional and the whole thing is #f when
;;; nothing matches and there is no else.
;;;
;;; `_' in a pattern matches anything in that position, the same thing
;;; it means in a type. Without it every spelling has to be enumerated:
;;; `(closure ((int)) int)' and `(closure ((float)) int)' are separate
;;; clauses for what is one case.
;;;
;;; A `_' written last takes everything that remains, because a type's
;;; words are spread rather than nested -- `(* const char)' is three
;;; elements, so `(* _)' has to cover two of them to mean "a pointer to
;;; anything".
;;;
;;; Nothing destructures: a macro body is ordinary Scheme and a type is
;;; a list, so `(caddr (type-of x))' already reads the length out of
;;; `(¤ int 4)'.
(define-syntax type-match
(syntax-rules (else)
((_ type) #f)
((_ type (else body ...)) (begin body ...))
((_ type (pattern body ...) clause ...)
(if (equal? type 'pattern)
(if (type-pattern-matches? 'pattern type)
(begin body ...)
(type-match type clause ...)))))
(define (type-pattern-matches? pattern type)
(cond
((eq? pattern '_) #t)
((and (pair? pattern) (pair? type))
(if (and (eq? (car pattern) '_) (null? (cdr pattern)))
#t ; a trailing `_' takes the rest
(and (type-pattern-matches? (car pattern) (car type))
(type-pattern-matches? (cdr pattern) (cdr type)))))
(else (equal? pattern type))))
;;; Map function to each field/value of a structure/union/enum
;;; For enums, field-type is the type of the enum (since C 23)
;;; (map-fields type-name
@@ -177,3 +237,79 @@
(map (lambda (value) (fn value type))
(caddr info))))
(else #f)))))
;;; The shape of a written type
;;;
;;; Where a type ends, asked by an arglist and by an array bound:
;;;
;;; (f1 float) a name and a type (unsigned int) a type
;;; (¤ int 4) four of int (¤ const t) unsized, of const t
;;; (¤ mytype N) N of mytype (¤ * size-t) unsized, of (* size-t)
;;; A qualifier cannot end a type: `(¤ const t)' is unsized, `(¤ int 4)'
;;; is four of int.
(define +c-qualifiers+ '(const volatile restrict _Atomic))
(define +c-specifiers+
'(void char short int long float double signed unsigned
bool _Bool complex _Complex))
;;; Does this list start a type rather than name one? `(const char)'
;;; and `(unsigned int)' are types; `(f1 float)' is a named parameter.
(define (type-head? form)
(and (pair? form)
(symbol? (car form))
(or (memq (car form) '(* ¤ struct union enum))
(memq (car form) +c-qualifiers+)
(memq (car form) +c-specifiers+))))
;;; Does the parameter name itself?
;;; (f1 float) does
;;; (float), (const char), (unsigned int) and (¤ float 4) do not
(define (named-arg? arg)
(and (pair? arg)
(pair? (cdr arg)) ; 1 element args are always type
(not (type-head? arg))))
;;; A typedef and a `define' share +type-db+; only the typedef is part
;;; of a type:
;;;
;;; (typedef small int) -> (¤ small N) is N of small
;;; (define CAP 4) -> (¤ int CAP) is CAP of int
(define (typedef-name? name)
(let ((info (and (symbol? name) (get-type-info name))))
(and info (memq (car info) '(typedef struct union enum)) #t)))
;;; The last element is a bound only where what precedes it already
;;; spells a whole type -- a specifier, a tag after its keyword, or a
;;; typedef we have seen declared:
;;;
;;; (¤ int 4) four of int (¤ unsigned int) unsized
;;; (¤ mytype CAP) CAP of mytype (¤ struct point) unsized
;;; (¤ const mytype) unsized (¤ * size-t) unsized
;;;
;;; TYPE is the whole `(¤ ...)' form.
(define (array-bound? type)
(and (> (length type) 2)
(let ((bound (last type))
(preceding (last (drop-right type 1))))
(cond
((not (symbol? bound)) #t)
((or (memq bound +c-specifiers+) (memq bound +c-qualifiers+)) #f)
((memq preceding +c-specifiers+) #t)
;; a tag always follows its keyword, so `(¤ * struct tt)' ends
;; in a name belonging to the type
((memq preceding '(struct union enum)) #f)
(else (typedef-name? preceding))))))
;;; What one element of a written array type is:
;;; (¤ struct point 2) -> (struct point), (¤ * const char 2) -> (* const char)
(define (array-element-type type)
(and (pair? type)
(eq? '¤ (car type))
(pair? (cdr type))
(let ((words (if (array-bound? type)
(drop-right (cdr type) 1)
(cdr type))))
(and (pair? words)
(if (null? (cdr words)) (car words) words)))))

View File

@@ -15,7 +15,10 @@
copy-form-source!
stamp-form-source!
form-location
set-form-type!
form-type
sex-error
sex-warning
with-directory
)
"utils.scm")

View File

@@ -101,6 +101,19 @@
;;; The file `parse-all' is currently reading. Bound by the reader
(define current-source-file (make-parameter "<unknown>"))
;;; What type a form has, once something has worked it out. Keyed by
;;; cons cell like the sources above, so one form has one type: a body
;;; typed at two instantiations has to be copied before the second.
(define +form-types+ (make-hash-table eq?))
(define (set-form-type! form type)
(when (pair? form)
(hash-table-set! +form-types+ form type))
type)
(define (form-type form)
(hash-table-ref/default +form-types+ form #f))
(define (set-form-source! form file line)
(hash-table-set! +form-sources+ form (cons file line)))
@@ -138,6 +151,17 @@ wrap a form-building expression."
"Signal an error about FORM, prefixed with where it was written."
(apply error (string-append (form-location form) message) args))
(define (sex-warning form message . args)
"Report something about FORM that does not stop the compilation.
Goes to stderr, prefixed with where the form was written, so a warning
reads like an error and sorts alongside one in a build log."
(let ((port (current-error-port)))
(display (form-location form) port)
(display "warning: " port)
(display message port)
(for-each (lambda (arg) (display " " port) (display arg port)) args)
(newline port)))
(define (stamp-form-source! form src)
"Give FORM and every subform that has none the location SRC. Used for
macro expansions, which inherit the location of the call site the way a