1
0
forked from alex-eg/sex

21 Commits

Author SHA1 Message Date
Pavel Kulyov
63801b47b2 deps: lock versions 2026-09-18 00:37:56 +03:00
Pavel Kulyov
82d9402857 readme: update installation docs 2026-09-18 00:27:08 +03:00
Pavel Kulyov
22936e68e3 infra: pepper some GNU on top of Makefile 2026-09-18 00:27:07 +03:00
Pavel Kulyov
e2fb4adae5 readme: add docs about static compilation 2026-09-18 00:26:17 +03:00
Pavel Kulyov
e3da2a1e79 Update gitignore 2026-09-18 00:26:17 +03:00
Pavel Kulyov
79ce3dace2 Add project-local dependencies installation 2026-09-18 00:26:16 +03:00
a221e0f8ea build the triangle on Linux too
<OpenGL/gl3.h> does not exist there and -framework is not a gcc flag,
so the documented build line failed with no hint why. #+macosx picks
the header now, and both build lines are in the file -- pkg-config
carries the flags on either platform, apart from Apple's GL framework,
which has no pkg-config file to carry.
2026-09-16 18:01:02 +03:00
bf83baa508 read-time feature expressions
'#+' and '#-' introduce conditional compilation: the form that follows
is kept only when the feature expression is true, and otherwise is read
and thrown away.  An expression is a feature name, or and / or / not
of them.

They are read time, not compile time.

Default features are the host's software-version, software-type and
machine-type as CHICKEN reports them, plus what --features flag adds.
2026-09-16 17:59:37 +03:00
6e4cb80424 make sextest's (compilation ...) form work
1. Look for `compilation', not `compile' among the source
2. Read the file with provided --features from the (compilation ...)
form, then handle resulting file to sexc
2026-09-16 17:48:44 +03:00
294a275905 fail when the C compiler fails, and clean up when we do
compile-to-file returned process-wait's values and main dropped them,
so cc errors were printed, but then main compiler exited 0.

Also cleanup tmp C files when compilation failed.
2026-09-16 17:34:24 +03:00
9763f5fa8d export explicitly from the test build's types module 2026-09-16 17:29:53 +03:00
97ef62b7f4 resolve typedefs in get-fields and map-fields
Now these macro helpers take in account the possibility of typedefing
one type to another, and correctly find the one intended
2026-09-16 17:07:21 +03:00
847bb14340 accept pub enum, and enums used as types
Implement pub support for enums, and also anonymous enums declared
in-place.

Also give the cdr of a `pub' form the form's own location, so a
error about what follows `pub' can say where it was written
2026-09-16 17:02:45 +03:00
d0d0ea8e84 process imported forms as toplevel, not as text
1. register imported types
2. protect against multiple imports (diamond, circular)
2026-09-16 16:57:34 +03:00
85bf1c163c generate temp .c files instead of passing to compiler's stdin
For a clearer architecture
2026-09-15 21:28:41 +03:00
0b2b97a0c6 add type database and compile-time reflection 2026-09-15 21:28:41 +03:00
dae19715df add module testing
Also fix module import
2026-09-15 21:28:41 +03:00
522c5a2c01 reject nested pointer types with proper error message 2026-09-15 21:28:41 +03:00
28ad33ca38 add sex-error reporting 2026-09-15 21:28:41 +03:00
e751ce2cb9 add sdl3 triangle example 2026-09-15 21:28:41 +03:00
Pavel Kulyov
df5a933f01 ci: use gitea actions platform without access to github
Fortunately Gitea actions runner is not too tied to GHA, so
just eliminating `uses:` clauses we can get a little more freedom
in sex.
2026-09-15 20:47:40 +03:00
29 changed files with 691 additions and 137 deletions

View File

@@ -0,0 +1,76 @@
name: Sex CI
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
build-linux:
runs-on: ubuntu-latest
steps:
- name: Fetch repository
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
set -eu
git config --global --add safe.directory "$PWD"
git init
git remote add origin "${GITHUB_SERVER_URL%/}/${GITHUB_REPOSITORY}.git"
git -c http.extraHeader="Authorization: token ${GITEA_TOKEN}" \
fetch --depth 1 origin "${GITHUB_SHA}"
git checkout --force FETCH_HEAD
- name: Install toolchain
run: |
set -eu
if [ "$(id -u)" -eq 0 ]; then
apt-get update
apt-get install -y --no-install-recommends build-essential git wget ca-certificates
else
sudo apt-get update
sudo apt-get install -y --no-install-recommends build-essential git wget ca-certificates
fi
- name: Install Chicken
env:
CHICKEN_VERSION: "6.0.0"
CHICKEN_SHA256: 92835552b1b687ad26737e429b5aba36510bf429f8816ec0f6d336c8cb41f443
run: |
set -eu
tarball="chicken-${CHICKEN_VERSION}.tar.gz"
wget -N "https://code.call-cc.org/releases/${CHICKEN_VERSION}/${tarball}"
echo "${CHICKEN_SHA256} ${tarball}" | sha256sum -c
tar zxf "${tarball}"
(
cd "chicken-${CHICKEN_VERSION}"
./configure --prefix=/usr/local
make -j"$(nproc)"
if [ "$(id -u)" -eq 0 ]; then
make install
else
sudo make install
fi
)
hash -r
csc -version
- name: Install eggs
run: |
set -eu
mkdir -p .eggs
repo="$(chicken-install -repository)"
echo "CHICKEN_INSTALL_REPOSITORY=${PWD}/.eggs" >> "$GITHUB_ENV"
echo "CHICKEN_REPOSITORY_PATH=${PWD}/.eggs:${repo}" >> "$GITHUB_ENV"
export CHICKEN_INSTALL_REPOSITORY="${PWD}/.eggs"
export CHICKEN_REPOSITORY_PATH="${PWD}/.eggs:${repo}"
# shellcheck disable=SC2046
chicken-install $(cat dependencies.txt)
- name: Build sexc
run: make sexc && ./sexc --help
- name: Run tests
run: make run-tests

View File

@@ -1,55 +0,0 @@
name: Sex CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install chicken
run: |
wget -N https://code.call-cc.org/releases/6.0.0/chicken-6.0.0.tar.gz
tar zxf chicken-6.0.0.tar.gz
sudo apt install -y make
make -C chicken-6.0.0 PLATFORM=linux
sudo make -C chicken-6.0.0 PLATFORM=linux install
- name: Install dependencies
# FIXME: [project-local deps]: use venv or something
# run: make deps
run: sudo chicken-install $(cat dependencies.txt)
- name: Make sure that sexc builds
# FIXME: sexc being built with local .eggs/ libs needs the path at runtime.
# Without it there will be `Error: cannot load extension: fmt`.
run: make sexc && ./sexc --help
- name: Run tests
# FIXME: [project-local deps]: use local deps or build with -static
# run: make run-tests
run: make sex-tests && ./sex-tests
build-macos:
runs-on: macos-15
steps:
- uses: actions/checkout@v3
- name: Install chicken
run: brew install chicken make
- name: Install dependencies
# FIXME: [project-local deps]: use venv or something
# run: make deps
run: chicken-install $(cat dependencies.txt)
- name: Make sure that sexc builds
# FIXME: sexc being built with local .eggs/ libs needs the path at runtime.
# Without it there will be `Error: cannot load extension: fmt`.
run: make sexc && ./sexc --help
- name: Run tests
# FIXME: [project-local deps]: use local deps or build with -static
# run: make run-tests
run: make sex-tests && ./sex-tests

6
.gitignore vendored
View File

@@ -1,5 +1,11 @@
# Project-local Chicken egg repository
/.eggs
# Compilation artifacts
*.o *.o
*.import.scm *.import.scm
*.link *.link
sexc sexc
sex-tests sex-tests
sextest
tools/sextest/sextest

View File

@@ -1,4 +1,6 @@
CHICKEN_C = csc CHICKEN_C ?= csc
CHICKEN_INSTALL ?= chicken-install
CHICKEN_STATUS ?= chicken-status
CSC_FLAGS += -K prefix -static CSC_FLAGS += -K prefix -static
# What and why: # What and why:
# -emit-all-import-libraries: Emit import-libraries for all defined modules. # -emit-all-import-libraries: Emit import-libraries for all defined modules.
@@ -12,17 +14,44 @@ CSC_FLAGS += -K prefix -static
# error. # error.
# -c: Stop after compilation to object files. This one is obvious. # -c: Stop after compilation to object files. This one is obvious.
# GNU directory variables. Command line overrides, e.g.
# make prefix=$(HOME)/.local install
# make DESTDIR=/tmp/stage prefix=/usr install
prefix = /usr/local
exec_prefix = $(prefix)
bindir = $(exec_prefix)/bin
INSTALL = install
INSTALL_PROGRAM = $(INSTALL)
MODULE_FLAGS = -emit-all-import-libraries -module-registration -c MODULE_FLAGS = -emit-all-import-libraries -module-registration -c
# Order matters, since module check correctness on compilation # 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 sex-macros reader sex-modules semen sex-fmt-c fmt-c-writer sexc
OBJ = $(MODULES:%=%.o) OBJ = $(MODULES:%=%.o)
sexc: $(OBJ) main.scm DEPSFILE = dependencies.txt
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)
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)
export CHICKEN_REPOSITORY_PATH := $(EGGS_DIR):$(SYSTEM_CHICKEN_REPO)
all: sexc
sexc: $(EGGS_STAMP) $(OBJ) main.scm
$(CHICKEN_C) $(CSC_FLAGS) main.scm -o sexc-tmp -link sexc $(CHICKEN_C) $(CSC_FLAGS) main.scm -o sexc-tmp -link sexc
# otherwise csc hangs, probably because it tries to compile to sexc.o first # otherwise csc hangs, probably because it tries to compile to sexc.o first
mv sexc-tmp sexc mv sexc-tmp sexc
$(OBJ): $(EGGS_STAMP)
#------------------------------------------------------------------ #------------------------------------------------------------------
utils.o: utils.module.scm utils.scm utils.o: utils.module.scm utils.scm
@@ -53,28 +82,68 @@ sexc.o: sexc.module.scm types.o sexc.scm fmt-c-writer.o sex-macros.o sex-modules
$(CHICKEN_C) $(CSC_FLAGS) $(MODULE_FLAGS) sexc.module.scm -o sexc.o -unit sexc -link fmt-c-writer,sex-macros,sex-modules,reader,semen,types,utils $(CHICKEN_C) $(CSC_FLAGS) $(MODULE_FLAGS) sexc.module.scm -o sexc.o -unit sexc -link fmt-c-writer,sex-macros,sex-modules,reader,semen,types,utils
# Unit testing # Unit testing
sex-tests: sex-tests: $(EGGS_STAMP)
$(MAKE) -C ./tests sex-tests $(MAKE) -C ./tests sex-tests
cp ./tests/sex-tests ./ cp ./tests/sex-tests ./
sextest: sextest: $(EGGS_STAMP)
$(MAKE) -C ./tools/sextest sextest $(MAKE) -C ./tools/sextest sextest
cp ./tools/sextest/sextest . cp ./tools/sextest/sextest .
SEX_TEST_PROGRAMS = hello-world lists comments unicode serialize SEX_TEST_PROGRAMS = hello-world lists comments unicode serialize features
# Multi-module linking is checked end to end; see tests/modules/Makefile. # Multi-module linking is checked end to end; see tests/modules/Makefile.
check-modules: sexc check-modules: sexc
@$(MAKE) --no-print-directory -C ./tests/modules check SEXC=../../sexc @$(MAKE) --no-print-directory -C ./tests/modules check SEXC=../../sexc
# The failure paths are checked end to end; see tests/exit-code/Makefile.
check-exit-code: sexc
@$(MAKE) --no-print-directory -C ./tests/exit-code check SEXC=../../sexc
run-tests: sexc sex-tests sextest run-tests: sexc sex-tests sextest
./sex-tests && ./sextest --sexc=./sexc $(SEX_TEST_PROGRAMS:%=./tests/sex-programs/%.sex) && $(MAKE) check-modules ./sex-tests && ./sextest --sexc=./sexc $(SEX_TEST_PROGRAMS:%=./tests/sex-programs/%.sex) && $(MAKE) check-modules && $(MAKE) check-exit-code
install: all installdirs
$(INSTALL_PROGRAM) sexc $(DESTDIR)$(bindir)/sexc
install-strip:
$(MAKE) INSTALL_PROGRAM='$(INSTALL_PROGRAM) -s' install
installdirs:
$(INSTALL) -d $(DESTDIR)$(bindir)
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): $(DEPSLOCK)
mkdir -p $(CHICKEN_EGG_CACHE)
$(CHICKEN_INSTALL) -force -from-list $(DEPSLOCK)
touch $@
deps: $(EGGS_STAMP)
deps-update: $(DEPSFILE)
rm -rf $(EGGS_DIR)
mkdir -p $(CHICKEN_EGG_CACHE)
$(CHICKEN_INSTALL) -force $$(cat $(DEPSFILE))
CHICKEN_REPOSITORY_PATH=$(EGGS_DIR) $(CHICKEN_STATUS) -list | sort > $(DEPSLOCK).tmp
mv $(DEPSLOCK).tmp $(DEPSLOCK)
touch $(EGGS_STAMP)
clean: clean:
rm -f $(OBJ) main.o rm -f $(OBJ) main.o
rm -f *.import.scm rm -f *.import.scm
rm -f *.link rm -f *.link
rm -f sexc sex-tests sextest rm -f sexc sex-tests sextest
$(MAKE) -C ./tests clean
$(MAKE) -C ./tests/modules clean $(MAKE) -C ./tests/modules clean
$(MAKE) -C ./tools/sextest clean
.PHONY: clean run-tests sex-tests sextest check-modules deps-clean:
rm -rf $(EGGS_DIR)
.PHONY: all check run-tests check-modules check-exit-code install install-strip installdirs
uninstall deps deps-update clean depsclean sex-tests sextest

View File

@@ -12,16 +12,46 @@ Sex is statically typed, compiled general purpose language.
First, get yourself a Chicken, then, some Chicken deps. You also will First, get yourself a Chicken, then, some Chicken deps. You also will
need a C compiler. need a C compiler.
** Install Chicken Eggs Eggs are installed into a project-local ~.eggs/~ repository; they do not
Tip: there's a way to make Chicken install eggs non-globally. You need touch the Chicken system repository.
to export ~CHICKEN_INSTALL_REPOSITORY~ and ~CHICKEN_REPOSITORY_PATH~
environment variables. Refer to the documentation for more info:
https://wiki.call-cc.org/man/5/Extension%20tools#changing-the-repository-location
~chicken-install `cat dependencies.txt`~
** Compilation ** Compilation
~make~ #+begin_src sh
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:
#+begin_src sh
make deps-update
#+end_src
** Static compilation
The default ~CSC_FLAGS~ include ~-static~, so ~sexc~ does not need
~.eggs~ at runtime and ~make install~ stays relocatable. Chicken must
provide ~libchicken.a~ (E.g. for gentoo: ~dev-scheme/chicken~ with
~static-libs~ use flag).
To link dynamically instead (the binary will look for eggs under this
tree's ~.eggs~ path):
#+begin_src sh
make CSC_FLAGS='-K prefix'
#+end_src
** Installation
GNU directory variables: ~prefix~, ~exec_prefix~, ~bindir~, ~DESTDIR~.
#+begin_src sh
# default installation (/usr/local/bin/)
make install
# customize the prefix (installs to ~/.local/bin)
make prefix=$(HOME)/.local install
# staged install for packaging
make DESTDIR=/tmp/stage prefix=/usr install
#+end_src
* Usage * Usage
** Summary ** Summary
@@ -31,13 +61,21 @@ Options:
--c-compiler=ARG Select C compiler. Defaults to value of SEX_CC --c-compiler=ARG Select C compiler. Defaults to value of SEX_CC
environment variable, or if it is empty, to cc environment variable, or if it is empty, to cc
-c, --compile-object Compile object file instead of executable program -c, --compile-object Compile object file instead of executable program
-C, --preprocess Emit C code -f, --features=ARG Comma-separated feature names, added to the host's own
for #+ and #- feature expressions. May be given
more than once
--no-platform-features Leave out the host's own features. With --features,
this reads a file the way another platform would
-C, --emit-c Emit C code
--public-interface Get module's public interface --public-interface Get module's public interface
-h, --help Show this help -h, --help Show this help
-m, --macro-expand Emit macro-expanded semantically processed Sex code -m, --macro-expand Emit macro-expanded semantically processed Sex code
(sort of IR). May be useful for debugging
-o, --output=ARG Write output to file. Default file name is a.out. -o, --output=ARG Write output to file. Default file name is a.out.
If -E or -m options are provided, defaults to stdout If -E or -m options are provided, defaults to stdout
--line-directives=ARG How much #line information to emit: statement (default),
toplevel, or none. `statement' is what makes a debugger
land on the right source line; `none' is for reading -C
output by eye
#+end_src #+end_src
** Compiling Hello World ** Compiling Hello World
#+begin_src shell #+begin_src shell
@@ -101,6 +139,39 @@ Module's public interface consists of everything declared
~pub~. Structures, function, macros, types, variables can be ~pub~. Structures, function, macros, types, variables can be
public. public.
** Read-time feature expressions
Sex is able to use ~#+~ and ~#-~ for conditional compilation: the form that
follows is kept only when the feature expression is true, and otherwise
is read and thrown away.
#+begin_src scheme
#+macosx (include OpenGL/gl3.h)
#-macosx (include GL/gl.h)
#+(and unix (not macosx)) (define HAVE-EPOLL 1)
#+end_src
An expression is a feature name, or ~and~, ~or~ and ~not~ of them.
This is read time, not compile time. What does not apply never reaches macro
expansion, the type database or the generated C.
The features are the host's ~(software-version)~, ~(software-type)~
and ~(machine-type)~, e.g. ~macosx unix arm64~ or ~linux unix
x86-64~. ~--features~ adds to them:
#+begin_src shell
sexc prog.sex -f debug,with-sdl
sexc prog.sex --features=debug --features=with-sdl
#+end_src
A feature is never taken away. The host's features can be disabled,
e.g. for checking output for other platform:
#+begin_src shell
sexc example/sdl3-triangle.sex -C --no-platform-features --features=linux,unix,x86-64
#+end_src
** Syntactic macros ** Syntactic macros
Sex has support for syntactic macros. Macro definitions look like Sex has support for syntactic macros. Macro definitions look like
functions: they have a name, an argument list and a body. Macro should functions: they have a name, an argument list and a body. Macro should

8
eggs.lock Normal file
View File

@@ -0,0 +1,8 @@
(brev-separate "1.100")
(fmt "0.8.14")
(getopt-long "4.0")
(matchable "1.2")
(srfi-1 "0.5.1")
(srfi-13 "0.3.8")
(srfi-69 "0.5.3")
(test "1.3")

View File

@@ -8,13 +8,29 @@
;;; triangle is and how far it has spun are passed in as uniforms, so ;;; triangle is and how far it has spun are passed in as uniforms, so
;;; the geometry itself is uploaded once and never touched again. ;;; the geometry itself is uploaded once and never touched again.
;;; ;;;
;;; Build: ;;; Build, macOS:
;;; ./sexc example/sdl3-triangle.sex -o sdl3-triangle -- `pkg-config --cflags --libs sdl3` -framework OpenGL ;;; ./sexc example/sdl3-triangle.sex -o sdl3-triangle -- `pkg-config --cflags --libs sdl3` -framework OpenGL
;;;
;;; Build, Linux:
;;; ./sexc example/sdl3-triangle.sex -o sdl3-triangle -- `pkg-config --cflags --libs sdl3 gl`
;;;
;;; The GL header is the one platform difference, and #+ / #- picks it.
;;; Apple keeps the core-profile entry points in <OpenGL/gl3.h> and
;;; links them through -framework OpenGL, which no pkg-config file
;;; describes; everywhere else the prototypes come from <GL/glext.h>
;;; with GL_GLEXT_PROTOTYPES defined, and -lGL -- `pkg-config --libs
;;; gl' -- resolves them. SDL3/SDL_opengl.h is not the shortcut it
;;; looks like: on Apple it resolves to the 2.1 header, which has no
;;; glGenVertexArrays and so cannot bind the VAO this program needs.
(define GL-SILENCE-DEPRECATION 1) ; OpenGL is deprecated on macOS #+macosx (define GL-SILENCE-DEPRECATION 1) ; OpenGL is deprecated on macOS
(include SDL3/SDL.h) (include SDL3/SDL.h)
(include OpenGL/gl3.h)
#+macosx (include OpenGL/gl3.h)
#-macosx (define GL-GLEXT-PROTOTYPES 1)
#-macosx (include GL/gl.h)
#-macosx (include GL/glext.h)
(define WINDOW-WIDTH 800) (define WINDOW-WIDTH 800)
(define WINDOW-HEIGHT 600) (define WINDOW-HEIGHT 600)

View File

@@ -382,10 +382,17 @@ forms, and what remains."
(define (walk-enum form) (define (walk-enum form)
(match form (match form
;; Naming one without defining it: `(var m (enum mood))', the same
;; shape walk-struct accepts for `(struct foo)'. Guarded, and
;; before the anonymous case, since `(enum (red green))' is also a
;; two-element form
(('enum (? symbol? name))
`(enum ,(atom-to-fmt-c name)))
(('enum (values ...)) (('enum (values ...))
`(enum ,(map atom-to-fmt-c values))) `(enum ,(map atom-to-fmt-c values)))
(('enum name (values ...)) (('enum name (values ...))
`(enum ,(atom-to-fmt-c name) ,(map atom-to-fmt-c values))))) `(enum ,(atom-to-fmt-c name) ,(map atom-to-fmt-c values)))
(else (sex-error form "malformed enum" form))))
(define (walk-extern form) (define (walk-extern form)
(match form (match form
@@ -410,6 +417,7 @@ forms, and what remains."
('struct . _) ('struct . _)
('union . _) ('union . _)
('enum . _)
('typedef . _)) ('typedef . _))
;; ignore here, used in generating public interface ;; ignore here, used in generating public interface
@@ -423,7 +431,10 @@ forms, and what remains."
(('fn . _) (list 'static (walk-function form))) (('fn . _) (list 'static (walk-function form)))
(('var . _) (list 'static (walk-var form))) (('var . _) (list 'static (walk-var form)))
(('extern . rest) (walk-extern rest)) (('extern . rest) (walk-extern rest))
(('pub . rest) (walk-public rest)) ;; The cdr of a form has no location of its own, so hand it the
;; `pub' form's -- otherwise a complaint about what follows `pub'
;; cannot say where it was written
(('pub . rest) (walk-public (copy-form-source! form rest)))
((or ('struct . _) ((or ('struct . _)
('union . _)) (walk-struct form)) ('union . _)) (walk-struct form))
(('enum . _) (walk-enum form)) (('enum . _) (walk-enum form))

View File

@@ -1,3 +1,6 @@
(module reader (read-from-file (module reader (read-from-file
read-raw-forms) read-raw-forms
current-features
platform-features)
"reader.scm") "reader.scm")

View File

@@ -7,6 +7,8 @@
;;; - a leading `.' rewritten to the symbol `dot-access' ;;; - a leading `.' rewritten to the symbol `dot-access'
;;; - `;' comments preserved as (comment "...") forms, so they can be ;;; - `;' comments preserved as (comment "...") forms, so they can be
;;; re-emitted into the generated C (keeping the source mapping) ;;; re-emitted into the generated C (keeping the source mapping)
;;; - #+ / #- feature expressions, which decide at read time what the
;;; compiler gets to see at all
;;; It also records the source location of every form it reads (see ;;; It also records the source location of every form it reads (see
;;; utils' form-source), so the C writer can emit #line directives. ;;; utils' form-source), so the C writer can emit #line directives.
@@ -15,6 +17,8 @@
(scheme base) ; make-parameter (scheme base) ; make-parameter
(chicken base) (chicken base)
(chicken pathname) (chicken pathname)
(chicken platform) ; software-version, machine-type
(only srfi-1 every any) ; srfi-1 also has an append-reverse
utils) utils)
;;; Sentinels for structural tokens ;;; Sentinels for structural tokens
@@ -160,7 +164,8 @@
((string->number s) => identity) ((string->number s) => identity)
(else (string->symbol s)))) (else (string->symbol s))))
;;; #-dispatch: booleans, characters, vectors, block/datum comments ;;; #-dispatch: booleans, characters, vectors, block/datum comments,
;;; feature expressions
(define (read-hash port) (define (read-hash port)
(let ((c (get-ch port))) (let ((c (get-ch port)))
(cond (cond
@@ -171,8 +176,46 @@
((char=? c #\() (list->vector (read-list port close-paren))) ((char=? c #\() (list->vector (read-list port close-paren)))
((char=? c #\|) (skip-block-comment port 1) (next-token port)) ((char=? c #\|) (skip-block-comment port 1) (next-token port))
((char=? c #\;) (read-datum port) (next-token port)) ; datum comment ((char=? c #\;) (read-datum port) (next-token port)) ; datum comment
((char=? c #\+) (read-conditional port #t))
((char=? c #\-) (read-conditional port #f))
(else (error "Unsupported # syntax" c))))) (else (error "Unsupported # syntax" c)))))
;;; Feature expressions
;;;
;;; #+linux (include GL/gl.h) kept on Linux
;;; #-macosx (foo) kept only on other than macOS
;;; #+(and unix (not macosx)) (bar) and / or / not combine them
;;;
(define (platform-features)
(list (software-version) (software-type) (machine-type)))
;;; The host's features are the default, so anything reading Sex sees
;;; what the compiler would. sexc rebinds this to add --features
(define current-features (make-parameter (platform-features)))
(define (feature-true? test)
(cond
((symbol? test) (and (memq test (current-features)) #t))
((pair? test)
(case (car test)
((and) (every feature-true? (cdr test)))
((or) (any feature-true? (cdr test)))
((not)
(if (and (pair? (cdr test)) (null? (cddr test)))
(not (feature-true? (cadr test)))
(error "Feature expression `not' takes exactly one operand" test)))
(else (error "Unknown operator in feature expression" (car test)))))
(else (error "Malformed feature expression" test))))
;;; The #-/#+ preceded datum is always read -- there is no other way
;;; to know where it ends -- and then either returned or dropped
(define (read-conditional port keep-when)
(let ((keep (eq? keep-when (feature-true? (read-datum port)))))
(if keep
(read-datum port)
(begin (read-datum port)
(next-token port)))))
;;; #t / #true / #f / #false. `val' is the boolean; consume any ;;; #t / #true / #f / #false. `val' is the boolean; consume any
;;; trailing name characters and validate ;;; trailing name characters and validate
(define (read-bool port val) (define (read-bool port val)

View File

@@ -93,17 +93,9 @@
(else (sex-error sex-form "unknown top level form" sex-form)))) (else (sex-error sex-form "unknown top level form" sex-form))))
(define (process-imports module-public-forms acc) (define (process-imports module-public-forms acc)
;; Recursively process imports: register public macros, cons all ;; consume (import ...) form and process imports so
;; other public things to our acc ;; data types end up in types db
(if (null? module-public-forms) acc (fold match-sex-form acc module-public-forms))
(match (car module-public-forms)
(('defmacro . rest)
(defmacro rest)
(process-imports (cdr module-public-forms) acc))
(else
(process-imports (cdr module-public-forms)
(cons (car module-public-forms)
acc))))))
(define (macro-expand form) (define (macro-expand form)
"Walk the form recursively and expand all macros, until none is left." "Walk the form recursively and expand all macros, until none is left."

View File

@@ -640,16 +640,22 @@
(define (c-union . args) (apply c-struct/aux "union" args)) (define (c-union . args) (apply c-struct/aux "union" args))
(define (c-class . args) (apply c-struct/aux "class" args)) (define (c-class . args) (apply c-struct/aux "class" args))
;; MODIFIED FROM UPSTREAM fmt-c: an enum may also be named without
;; being defined -- `enum color m;' -- exactly as c-struct/aux
;; already allows `struct point p;'. Upstream assumed a value list
;; was always present and mapped over whatever stood in its place.
(define (c-enum x . o) (define (c-enum x . o)
(define (c-enum-one x) (define (c-enum-one x)
(if (pair? x) (cat (car x) " = " (c-expr (cadr x))) (dsp x))) (if (pair? x) (cat (car x) " = " (c-expr (cadr x))) (dsp x)))
(let* ((name (if (null? o) (if (or (symbol? x) (string? x)) x #f) x)) (let* ((name (if (null? o) (if (or (symbol? x) (string? x)) x #f) x))
(vals (if name (car o) x))) (vals (if name (if (null? o) #f (car o)) x)))
(c-wrap-stmt (if vals
(cat (c-wrap-stmt
(c-braced-block (cat
(if name (cat "enum " name) (dsp "enum")) (c-braced-block
(c-in-expr (apply c-begin (map c-enum-one vals)))))))) (if name (cat "enum " name) (dsp "enum"))
(c-in-expr (apply c-begin (map c-enum-one vals))))))
(c-wrap-stmt (cat "enum " name)))))
(define (c-attribute . args) (define (c-attribute . args)
(cat "__attribute__ ((" (fmt-join c-expr args ", ") "))")) (cat "__attribute__ ((" (fmt-join c-expr args ", ") "))"))
@@ -743,7 +749,13 @@
(if (and (pair? (cadr type)) (eq? '%array (caadr type))) (if (and (pair? (cadr type)) (eq? '%array (caadr type)))
(c-paren name) (c-paren name)
name)))) name))))
((enum) (apply c-enum name (cdr type))) ;; MODIFIED FROM UPSTREAM fmt-c: upstream passed the
;; declarator's name to c-enum as the enum's tag, so
;; `(var m (enum color))' emitted `enum m {...}' and lost the
;; variable. Enums are laid out like structs: the type, then
;; the name being declared.
((enum)
(cat (apply c-enum (cdr type)) (if name (cat " " name) "")))
((struct union class) ((struct union class)
(cat (apply c-struct/aux (car type) (cdr type)) (if name (cat " " name) ""))) (cat (apply c-struct/aux (car type) (cdr type)) (if name (cat " " name) "")))
(else (fmt-join/last c-expr (lambda (x) (c-type x name)) type " ")))) (else (fmt-join/last c-expr (lambda (x) (c-type x name)) type " "))))

View File

@@ -14,6 +14,10 @@
(define +persistent-module-paths+ (list)) (define +persistent-module-paths+ (list))
;;; for guarding against multiple imports (sort of mandatory #pragma
;;; once)
(define +imported-modules+ (list))
(define (get-modules-public-forms module-list) (define (get-modules-public-forms module-list)
;; Module list is a list of symbols ;; Module list is a list of symbols
;; How Sex handles modules: ;; How Sex handles modules:
@@ -29,7 +33,11 @@
(let ((module-path (locate-module name))) (let ((module-path (locate-module name)))
(assert module-path (fmt #f "Failed to find module " name " in " (assert module-path (fmt #f "Failed to find module " name " in "
(get-module-paths))) (get-module-paths)))
(read-public-interface module-path))) (if (member module-path +imported-modules+)
(list)
(begin
(set! +imported-modules+ (cons module-path +imported-modules+))
(read-public-interface module-path)))))
(define (get-module-paths) (define (get-module-paths)
(cons (current-directory) (cons (current-directory)
@@ -75,7 +83,7 @@
;; A variable becomes an `extern' declaration ;; A variable becomes an `extern' declaration
((var) ((var)
(cons (copy-form-source! form (cons 'extern (take (cdr form) 3))) acc)) (cons (copy-form-source! form (cons 'extern (take (cdr form) 3))) acc))
((define defmacro import include struct typedef union) ((define defmacro enum import include struct typedef union)
(cons (copy-form-source! form (cdr form)) acc)) (cons (copy-form-source! form (cdr form)) acc))
(else (sex-error form "pub must be followed by a definition" form)))) (else (sex-error form "pub must be followed by a definition" form))))
(else acc))) (else acc)))

View File

@@ -2,12 +2,14 @@
(scheme base) ; call/cc (scheme base) ; call/cc
brev-separate brev-separate
(chicken base) (chicken base)
(chicken condition) ; handle-exceptions
(chicken file) (chicken file)
(chicken plist) (chicken plist)
(chicken pretty-print) (chicken pretty-print)
(chicken process) (chicken process)
(chicken process-context) (chicken process-context)
(chicken port) (chicken port)
(chicken string) ; string-split
fmt fmt
fmt-c-writer fmt-c-writer
getopt-long getopt-long
@@ -30,6 +32,17 @@
(required #f) (required #f)
(value #f) (value #f)
(single-char #\c)) (single-char #\c))
(features ,(fmt #f "Comma-separated feature names, added to the host's own" nl
(pad padding) "for #+ and #- feature expressions. May be given" nl
(pad padding) "more than once")
(required #f)
(value #t)
(single-char #\f))
(no-platform-features
,(fmt #f "Leave out the host's own features. With --features," nl
(pad padding) "this reads a file the way another platform would")
(required #f)
(value #f))
(emit-c "Emit C code" (emit-c "Emit C code"
(required #f) (required #f)
(value #f) (value #f)
@@ -97,6 +110,15 @@
((equal? v "none") 'none) ((equal? v "none") 'none)
(else (error "--line-directives must be statement, toplevel or none, got" v))))) (else (error "--line-directives must be statement, toplevel or none, got" v)))))
;;; --features may be given more than once, and each may name several.
;;; Collect all of them
(define (cli-features args)
(append-map (lambda (entry)
(map string->symbol (string-split (cdr entry) ",")))
(filter (lambda (entry) (eq? (car entry) 'features)) args)))
(define (get-input-file args) (define (get-input-file args)
(let ((rest-args (get-rest-args args))) (let ((rest-args (get-rest-args args)))
(if (null? rest-args) (if (null? rest-args)
@@ -117,15 +139,22 @@
(emit-c sex-forms))))) (emit-c sex-forms)))))
(define (compile-to-file sex-forms output args cc-args) (define (compile-to-file sex-forms output args cc-args)
"Hand the generated C to the C compiler. Returns the compiler's exit
status, which is ours to pass on."
(let ((compiler (or (get-arg args 'c-compiler #f) (let ((compiler (or (get-arg args 'c-compiler #f)
(get-env-var "SEX_CC") (get-env-var "SEX_CC")
"cc")) "cc"))
(out-file (if (eq? output 'default) (out-file (if (eq? output 'default)
"a.out" "a.out"
output))) output))
;; The generated C goes to a temporary .c file rather than the ;; The generated C goes to a temporary .c file rather than the
;; compiler's stdin ;; compiler's stdin. It is removed however we leave -- emit-c
(let ((c-file (create-temporary-file "c"))) ;; can throw, and used to leave the file behind when it did
(c-file (create-temporary-file "c")))
;; An unhandled error ends the process without unwinding, so the
;; cleanup cannot be left to dynamic-wind
(handle-exceptions exn
(begin (delete-file* c-file) (abort exn))
(with-output-to-file c-file (with-output-to-file c-file
(lambda () (emit-c sex-forms))) (lambda () (emit-c sex-forms)))
(let ((proc (process compiler (append (list "-o" out-file) (let ((proc (process compiler (append (list "-o" out-file)
@@ -135,9 +164,9 @@
(list c-file) (list c-file)
cc-args)))) cc-args))))
(call-with-values (lambda () (process-wait proc)) (call-with-values (lambda () (process-wait proc))
(lambda status (lambda (pid normal-exit? status)
(delete-file* c-file) (delete-file* c-file)
(apply values status))))))) (if normal-exit? status 1)))))))
(define (semantic-process-forms raw-forms input-source) (define (semantic-process-forms raw-forms input-source)
(if (eq? input-source 'stdin) (if (eq? input-source 'stdin)
@@ -173,6 +202,14 @@
(when help (when help
(print-help) (print-help)
(return #f)) (return #f))
;; Read time comes before everything, so the features have to be
;; in place before the first form is read
(current-features
(append (if (get-arg args 'no-platform-features #f)
(list)
(platform-features))
(cli-features args)))
(when (get-arg args 'public-interface #f) (when (get-arg args 'public-interface #f)
(assert (not (eq? input 'stdin)) "Error: --public-interface requires file argument") (assert (not (eq? input 'stdin)) "Error: --public-interface requires file argument")
@@ -198,5 +235,7 @@
(get-arg args 'emit-c #f)) (get-arg args 'emit-c #f))
;; Emit processed and macro-expanded sex code, or emit C code ;; Emit processed and macro-expanded sex code, or emit C code
(emit-c-or-sex sex-forms output args) (emit-c-or-sex sex-forms output args)
;; Compile file! ;; Compile file! The C compiler's status is ours too
(compile-to-file sex-forms output args cc-args))))))) (let ((status (compile-to-file sex-forms output args cc-args)))
(unless (zero? status)
(exit status)))))))))

View File

@@ -46,3 +46,5 @@ clean:
rm -f *.import.scm rm -f *.import.scm
rm -f *.link rm -f *.link
rm -f sex-tests rm -f sex-tests
.PHONY: clean

View File

@@ -141,6 +141,28 @@ compiles."
(test-assert "a comment in a body stays in the body" (test-assert "a comment in a body stays in the body"
(emits? (in-fn "(while (< a b) ;; inside\n (g 1))") (emits? (in-fn "(while (< a b) ;; inside\n (g 1))")
"while (a < b) {"))) "while (a < b) {")))
(test-group "pub enum"
(test-assert "is emitted"
(emits? "(pub enum color (red green blue))" "enum color"))
(test-assert "with its values"
(emits? "(pub enum color (red green blue))" "red"))
(test-assert "and a non-pub enum still is too"
(emits? "(enum color (red green blue))" "enum color"))
;; Naming an enum as a type, rather than defining it, had no
;; walk-enum clause and died with `(match) no matching pattern'
(test-assert "and it can then be used as a type"
(emits? "(enum color (red green blue)) (fn f () void (var m (enum color) red))"
"enum color m = red"))
(test-assert "a malformed enum is rejected with its location"
(reports? "(enum)" "codegen.sex:1:"))
;; c-type handed the declarator's name to c-enum as the enum tag,
;; so this emitted `enum m { up, down }' with no variable at all
(test-assert "an anonymous enum keeps the variable"
(emits? "(fn f () void (var e (enum (up down)) up))" "} e = up"))
(test-assert "and a named definition keeps both"
(emits? "(fn f () void (var n (enum named (a b)) a))" "enum named{")))
;; `|', `||' and `|=' read as ordinary symbols -- our own reader has ;; `|', `||' and `|=' read as ordinary symbols -- our own reader has
;; no |symbol| syntax for them to collide with -- but fmt-c cannot ;; no |symbol| syntax for them to collide with -- but fmt-c cannot
;; dispatch on a symbol whose name it cannot write in Scheme source, ;; dispatch on a symbol whose name it cannot write in Scheme source,

26
tests/exit-code/Makefile Normal file
View File

@@ -0,0 +1,26 @@
# The failure paths.
#
# Both need a process to show themselves, so neither fits the unit
# suite: sexc has to fail when cc fails -- exiting 0 after a failed
# compile makes every driver, sextest included, read it as success --
# and it has to clean up its temporary .c when its own emission throws.
#
# Everything is built inside a scratch TMPDIR, so nothing is left here
# to clean up.
SEXC ?= ../../sexc
check:
@d=`mktemp -d`; \
TMPDIR=$$d $(SEXC) nested-pointer.sex -o $$d/out >/dev/null 2>&1; \
if [ -n "`find $$d -name '*.c'`" ]; then \
echo "exit code FAILED: the temporary .c survived a failed emission"; \
rm -rf $$d; exit 1; \
fi; \
if $(SEXC) hello.sex -o $$d/out -- -no-such-cc-flag >/dev/null 2>&1; then \
echo "exit code FAILED: sexc reported success after cc failed"; \
rm -rf $$d; exit 1; \
fi; \
rm -rf $$d; echo "exit code ok"
.PHONY: check

View File

@@ -0,0 +1,4 @@
;;; Compiles cleanly, so the only way the build can fail is the bogus
;;; flag handed to cc -- which is the point.
(pub fn main () int
(return 0))

View File

@@ -0,0 +1,4 @@
;;; Rejected by the writer, so emit-c throws: there is a temporary .c
;;; by then, and it must not survive.
(fn f () void
(var p (* (* char))))

View File

@@ -7,10 +7,14 @@
# module's object survives being passed after `--'. Get any of them # module's object survives being passed after `--'. Get any of them
# wrong and this fails to link -- or, in the `pub var' case, links and # wrong and this fails to link -- or, in the `pub var' case, links and
# quietly counts into a private copy. # quietly counts into a private copy.
#
# 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.
SEXC ?= ../../sexc SEXC ?= ../../sexc
EXPECTED = hello, world\nhello, sex\n2 greetings EXPECTED = hello, world\nhello, sex\n2 greetings\ntext times \ntext times \nmood 1
check: check:
@$(SEXC) greet.sex -c -o greet.o @$(SEXC) greet.sex -c -o greet.o

View File

@@ -7,6 +7,9 @@
;;; as a prototype and `greet-count' as an extern. Both keep external ;;; as a prototype and `greet-count' as an extern. Both keep external
;;; linkage, so they refer to the one definition in greet.o rather than ;;; linkage, so they refer to the one definition in greet.o rather than
;;; to private copies. ;;; to private copies.
;;;
;;; Also check that import populates type-database, by means of
;;; describe-fields macro, which should work on imported type.
(include stdio.h) (include stdio.h)
@@ -16,4 +19,11 @@
(greet "world") (greet "world")
(greet "sex") (greet "sex")
(printf "%d greetings\n" greet-count) (printf "%d greetings\n" greet-count)
(describe-fields greeting)
(printf "\n")
;; ...and through the imported typedef for it
(describe-fields greeting-t)
(printf "\n")
(var m (enum mood) grumpy)
(printf "mood %d\n" m)
(return 0)) (return 0))

View File

@@ -9,6 +9,20 @@
(pub var greet-count int 0) (pub var greet-count int 0)
(pub struct greeting ((text (* const char)) (times int)))
(pub enum mood (cheerful grumpy))
(pub typedef greeting-t (struct greeting))
;;; Compile-time reflection across the module boundary: both the macro
;;; and the struct it asks about are exported, and the importing unit
;;; has to know the struct's fields to expand this.
(pub defmacro (describe-fields type)
`(do ,@(map-fields type
(lambda (name field-type)
`(printf "%s " ,(symbol->string name))))))
(pub fn greet ((name (* const char))) void (pub fn greet ((name (* const char))) void
(++ greet-count) (++ greet-count)
(printf "hello, %s\n" name)) (printf "hello, %s\n" name))

View File

@@ -1,6 +1,14 @@
(import (chicken port) (import (chicken port)
reader) reader)
(define-syntax feature-test
(syntax-rules ()
((feature-test result features string)
(test result
(parameterize ((current-features 'features))
(with-input-from-string string
(lambda () (read-raw-forms 'stdin))))))))
(define-syntax reader-test (define-syntax reader-test
(syntax-rules () (syntax-rules ()
((reader-test result string) ((reader-test result string)
@@ -38,4 +46,41 @@
(reader-test '((a b) (comment " t")) "(a b) ; t") (reader-test '((a b) (comment " t")) "(a b) ; t")
;; a `;' inside a string is not a comment ;; a `;' inside a string is not a comment
(reader-test '("a;b") "\"a;b\"") (reader-test '("a;b") "\"a;b\"")
;; #+ / #- feature expressions. What does not apply is read and
;; dropped, so it never reaches the compiler at all
(feature-test '((a)) (linux) "#+linux (a)")
(feature-test '() (macosx) "#+linux (a)")
(feature-test '() (linux) "#-linux (a)")
(feature-test '((a)) (macosx) "#-linux (a)")
;; the guarded datum can be anything, not only a list
(feature-test '(42) (x) "#+x 42")
(feature-test '("s") (x) "#+x \"s\"")
;; and / or / not
(feature-test '((a)) (unix linux) "#+(and unix linux) (a)")
(feature-test '() (unix) "#+(and unix linux) (a)")
(feature-test '((a)) (unix) "#+(or linux unix) (a)")
(feature-test '() (bsd) "#+(or linux unix) (a)")
(feature-test '((a)) (unix) "#+(and unix (not macosx)) (a)")
(feature-test '() (unix macosx) "#+(and unix (not macosx)) (a)")
;; (and) is true and (or) is false, as they are in CL
(feature-test '((a)) () "#+(and) (a)")
(feature-test '() () "#+(or) (a)")
;; a guard inside a form, including as the last element -- dropping
;; continues with the next token, so the closing paren still arrives
(feature-test '((f 1 3)) (a) "(f #+a 1 #-a 2 3)")
(feature-test '((f 2 3)) (b) "(f #+a 1 #-a 2 3)")
(feature-test '((f 1)) (a) "(f #+a 1 #+b 2)")
(feature-test '((f)) (b) "(f #+a 1)")
;; ...and as the last form in the file
(feature-test '((a)) (x) "(a) #+y (b)")
;; guards nest
(feature-test '((a)) (x y) "#+x #+y (a)")
(feature-test '((b)) (x) "#+x #+y (a) (b)")
;; a feature the program was not given is simply absent
(feature-test '() () "#+anything (a)")
) )

View File

@@ -0,0 +1,23 @@
(compilation "--features=test-on")
(input)
(output "selected" "on" "and-not")
(return 0)
;;; #+ and #- pick what the compiler gets to see. `test-on' is handed
;;; to sexc by the (compilation ...) form above, so this program reads
;;; the same way on every platform.
(include stdio.h)
#+test-on (define GREETING "on")
#-test-on (define GREETING "off")
#-test-on (pub fn main () int (puts "the whole function is dropped") (return 1))
(pub fn main () int
;; ...and inside a form, not only at toplevel
(puts #+test-on "selected" #-test-on "rejected")
(puts GREETING)
#+(and test-on (not test-off)) (puts "and-not")
#-test-on (puts "never printed")
(return 0))

View File

@@ -1,3 +1,14 @@
(module types (module types
* (add-struct
add-union
add-enum
add-typedef
add-define
type-match
map-fields
get-type-info
get-fields
get-underlying-type)
"../types.scm") "../types.scm")

View File

@@ -54,6 +54,42 @@
#f #f
(get-underlying-type 't-point)) (get-underlying-type 't-point))
;; Reflection through an alias. Both spellings of the target occur:
;; (typedef point-t point) and (typedef point-t (struct point)).
(add-typedef 't-point-t '(typedef t-point-t t-point))
(test "a typedef to a struct has the struct's fields"
'((x int) (y int))
(get-fields 't-point-t))
(add-typedef 't-point-s '(typedef t-point-s (struct t-point)))
(test "written the other way round too"
'((x int) (y int))
(get-fields 't-point-s))
(add-typedef 't-point-2 '(typedef t-point-2 t-point-t))
(test "and through a chain of them"
'((x int) (y int))
(get-fields 't-point-2))
(test "map-fields follows an alias as well"
'((x int) (y int))
(map-fields 't-point-t (lambda (name type) (list name type))))
(add-typedef 't-color-t '(typedef t-color-t t-color))
(test "an aliased enum is still named as it was declared"
'((red (enum t-color)) (green (enum t-color)) (blue (enum t-color)))
(map-fields 't-color-t (lambda (name type) (list name type))))
(test "a typedef to a primitive has no fields"
#f
(get-fields 't-u8))
;; A typedef can be written to lead back to itself. Resolving it must
;; stop rather than spin
(add-typedef 't-loop-a '(typedef t-loop-a t-loop-b))
(add-typedef 't-loop-b '(typedef t-loop-b t-loop-a))
(test "a typedef cycle terminates"
't-loop-a
(get-underlying-type 't-loop-a))
(test "and has no fields"
#f
(get-fields 't-loop-a))
;; A #define keeps its value forms -- there can be more than one. ;; A #define keeps its value forms -- there can be more than one.
(add-define 't-maxn '(define t-maxn 8)) (add-define 't-maxn '(define t-maxn 8))
(test "defines are recorded" (test "defines are recorded"

View File

@@ -1,3 +1,6 @@
(module reader (read-from-file (module reader (read-from-file
read-raw-forms) read-raw-forms
current-features
platform-features)
"../../reader.scm") "../../reader.scm")

View File

@@ -7,36 +7,63 @@
(chicken port) (chicken port)
(chicken process) (chicken process)
(chicken process-context) (chicken process-context)
(chicken string) ; string-split
fmt fmt
getopt-long getopt-long
reader ; read-raw-forms, shared with sexc reader ; read-raw-forms, shared with sexc
srfi-1) srfi-1
srfi-13) ; string-prefix?
(define (print-help) (define (print-help)
(fmt #t "Usage: sextest [options] filename" nl (fmt #t "Usage: sextest [options] filename" nl
"Options:" nl "Options:" nl
(usage opts-grammar) nl)) (usage opts-grammar) nl))
(define (process-file target-path) (define (split-settings contents)
(let ((contents (read-raw-forms target-path))) (foldl (lambda (acc elt)
(foldl (lambda (acc elt) (case (car elt)
(case (car elt) ((compilation input output return)
((compilation input output return) (cons
(cons (append (car acc) (list elt))
(append (car acc) (list elt)) (cdr acc)))
(cdr acc))) (else
(else (cons
(cons (car acc)
(car acc) (append (cdr acc) (list elt))))))
(append (cdr acc) (list elt)))))) (cons (list) (list))
(cons (list) (list)) contents))
contents)))
(define (compile src flags sexc) ;;; --features from the (compilation ...) form, which we have to honour
;;; ourselves: the program is read here and printed back out for sexc,
;;; so #+ and #- are resolved on this side
(define (compilation-features settings)
(let ((compilation (assoc 'compilation settings)))
(if compilation
(append-map (lambda (flag)
(if (string-prefix? "--features=" flag)
(map string->symbol
(string-split (substring flag 11) ","))
(list)))
(string-split (cadr compilation)))
(list))))
(define (process-file target-path)
(let* ((first-pass (split-settings (read-raw-forms target-path)))
(features (compilation-features (car first-pass))))
(if (null? features)
first-pass
(parameterize ((current-features (append (platform-features) features)))
(split-settings (read-raw-forms target-path))))))
(define (compile src compilation sexc)
(let ((compiler (or (let ((compiler (or
(and sexc (cdr sexc)) (and sexc (cdr sexc))
(get-environment-variable "SEXC") (get-environment-variable "SEXC")
"sexc")) "sexc"))
;; (compilation "--features=x -- -O2") -- one string
(flags (if compilation
(string-split (cadr compilation))
(list)))
(compiled-file (create-temporary-file))) (compiled-file (create-temporary-file)))
;; `process' returns one record; `process-input-port' is named from ;; `process' returns one record; `process-input-port' is named from
;; the child's side, so it is the port we write to. ;; the child's side, so it is the port we write to.
@@ -110,7 +137,7 @@
(let* ((settings-and-src (process-file path)) (let* ((settings-and-src (process-file path))
(settings (car settings-and-src)) (settings (car settings-and-src))
(src (cdr settings-and-src)) (src (cdr settings-and-src))
(compiled-file (compile src (assoc 'compile settings) sexc))) (compiled-file (compile src (assoc 'compilation settings) sexc)))
(if (not compiled-file) (if (not compiled-file)
(begin (fmt #t "Failed to compile " path nl) (begin (fmt #t "Failed to compile " path nl)
#f) #f)

View File

@@ -76,21 +76,44 @@
;;; ((name type) ...) for a struct or union, #f for anything else -- ;;; ((name type) ...) for a struct or union, #f for anything else --
;;; including a name that was never declared. Callers give the better ;;; including a name that was never declared. Callers give the better
;;; error, since they know what they wanted it for. ;;; error, since they know what they wanted it for.
;;;
;;; A typedef is followed to what it stands for, so reflection over an
;;; alias works exactly as it does over the name it aliases.
(define (get-fields name) (define (get-fields name)
(let ((info (get-type-info name))) (let ((info (resolve-type-info name)))
(and info (and info
(memq (car info) '(struct union)) (memq (car info) '(struct union))
(caddr info)))) (caddr info))))
;;; Follow a typedef chain to the name it ultimately stands for. #f if ;;; Follow a typedef chain to the name it stands for. #f if NAME is
;;; NAME is not a typedef. ;;; not a typedef. A typedef that leads back to itself stops rather
;;; than spinning: nothing prevents one from being written.
(define (get-underlying-type name) (define (get-underlying-type name)
(let follow ((name name) (seen (list)))
(and (not (member name seen))
(let ((info (get-type-info name)))
(and info
(eq? (car info) 'typedef)
(let ((target (caddr info)))
(or (and (symbol? target) (follow target (cons name seen)))
target)))))))
;;; The declaration NAME ultimately names. For a typedef that is the
;;; entry of whatever it stands for, and for anything else it is
;;; NAME's own info.
(define (resolve-type-info name)
(let ((info (get-type-info name))) (let ((info (get-type-info name)))
(and info (and info
(eq? (car info) 'typedef) (if (eq? (car info) 'typedef)
(let ((target (caddr info))) (let* ((target (get-underlying-type name))
(or (and (symbol? target) (get-underlying-type target)) (tag (cond ((symbol? target) target)
target))))) ((and (pair? target)
(pair? (cdr target))
(symbol? (cadr target)))
(cadr target))
(else #f))))
(and tag (get-type-info tag)))
info))))
;;; Type matcher macro ;;; Type matcher macro
;;; (type-match type ;;; (type-match type
@@ -119,15 +142,16 @@
;;; ;;;
;;; Returns #f if nothing of that name was declared ;;; Returns #f if nothing of that name was declared
(define (map-fields struct-union-enum fn) (define (map-fields struct-union-enum fn)
(let ((info (get-type-info struct-union-enum))) (let ((info (resolve-type-info struct-union-enum)))
(and info (and info
(case (car info) (case (car info)
((struct union) ((struct union)
(map (lambda (field) (fn (car field) (cadr field))) (map (lambda (field) (fn (car field) (cadr field)))
(caddr info))) (caddr info)))
;; An enumerator's type is the enum itself. ;; An enumerator's type is the enum itself -- named as it was
;; declared, since `enum some-typedef' is not a C type.
((enum) ((enum)
(let ((type (list 'enum struct-union-enum))) (let ((type (list 'enum (cadr info))))
(map (lambda (value) (fn value type)) (map (lambda (value) (fn value type))
(caddr info)))) (caddr info))))
(else #f))))) (else #f)))))