The low-level language with a high-level soul.
Cicili is a powerful metaprogramming system built on the expressive foundation of Lisp. It empowers developers to design domain-specific languages (DSLs), generate efficient C code through macro expansion, and build high-performance web and system applications. With Cicili, you can develop modular software components — from dynamic web servers and API pipelines to automation scripts and embedded systems — while enjoying near-native execution speed and highly maintainable code.
Lisp C Compiler aka. 'Cicili' programming language compiles Lisp-like syntax to C code, plus extra features like lambda, closures, deferred execution, RAII-style cleanup, and function-like macros.
**Lisp is a language for doing what you've been told is impossible.
— Kent Pitman** CAVEMAN2
That's such an inspiring quote by Kent Pitman! Lisp truly stands apart from many other languages by giving you the power to redefine your tools and even the language itself. Its homoiconicity — the idea that code and data share the same structure — means you can manipulate code with code. This opens the door to metaprogramming, macros, and the creation of powerful domain-specific languages that can do things others say are impossible.
This document covers the C half of Cicili: every clause that maps onto C, plus the
C-level power features built on top of it — lambda, closure, defer, alloc, auto,
multi-value returns. The functional layer (fn, data, match, type classes,
Functor / Applicative / Monad) lives in README.md and the test/haskell
folder. Read this file first: everything in the functional layer is built out of the clauses
below.
- Cicili uses an
IR(Intermediate Representation) to handle its clauses and features. - The macro system lets you code in extremely high-order syntax that produces low-level C. See builtins and macro.cicili.
lambdawrites an in-place function to pass as an argument or as adeferdestructor. See function.cicili.deferis a variable attribute available inletandvar. It sets how a variable is destructed — a lambda or a named function receiving a pointer to the variable. Useful for freeing structs or any resource stored inside one. See memory.cicili.- Auto-deferral releases memory allocated by
allocwhen the variable leaves itsletscope. Note that only functions with a declaration in aheaderand a definition in asource, or astaticfunction in asource, can usedefer*-style capturing deferment. closuregives a high-level, Lisp-style syntax for the complex C plumbing that closures require, making a powerful pattern accessible while still generating efficient C.- The
autotype simplifies lambda and function-pointer variables;typeofreuses another variable's (or expression's) type. - Inline structs can appear in a variable declaration, a function parameter, or a function return type — which is how a function returns multiple values. See function.cicili.
funcin type position declares a function pointer.- See aggregate.cicili for struct samples and control.cicili for control structures.
Every clause below is a list whose head is the clause name. Anything whose head is not a known clause is compiled as a function call.
| clause | what it does | section |
|---|---|---|
source |
a target that produces, compiles and links a .c file |
Program Structure |
header |
a target that produces a .h file; never compiled or linked |
Program Structure |
import |
loads a macro file at read time | Import |
include |
#include |
Include |
guard |
#ifndef / #define / #endif wrapper |
Guard |
@… |
any preprocessor directive: @define, @ifdef, @else, @endif, … |
Preprocessor Forms |
code |
raw C text, passed through untouched | Raw C |
DEFMACRO |
defines a Cicili macro (Common Lisp) | Macros |
macrolet |
defines macros for one body only | Macros |
generic / <> |
type-parameterised code, and the name-joining operator | Macros |
$$$ |
splices a macro's multiple result forms into the enclosing body | Macros |
| clause | what it does | section |
|---|---|---|
var |
a global or file-scope variable | Variable |
let / letn |
scoped variables; letn also returns a value |
Scoped Variables |
func |
a function — or, in type position, a function pointer | Function |
out |
a function's return type; first form after the parameter list | Function |
struct |
a structure | Structure |
union |
a union | Union |
enum |
an enumeration | Enum |
member |
one field of a struct or union | Structure |
declare |
the declarator(s) of an anonymous nested struct/union | Structure |
typedef |
a type alias | Typedef |
| clause | what it does | section |
|---|---|---|
if |
two- or three-part conditional | Decision Making |
cond |
if / else if chain |
Decision Making |
switch / case / default |
C switch |
Decision Making |
while / do / for |
loops | Loops |
break / continue |
loop and switch control | Loops |
block |
{ … } compound statement |
Blocks |
progn |
({ … }) statement expression — has a value |
Blocks |
set |
assignment, one or many pairs | Assignment |
return |
function return | Function |
| clause | C equivalent | section |
|---|---|---|
nth |
base[index] — index first |
Array |
? |
cond ? a : b |
Operators |
cast |
(type)expr |
Type Casting |
sizeof / typeof |
sizeof(…) / typeof(…) |
Operators |
aof / cof |
&x / *x |
Operators |
$ |
a.b — value member access |
Structure |
-> |
p->b — member access through a pointer |
Structure |
=> |
calls a function stored in a member | Struct-associated functions |
lambda / lambda* |
a lifted top-level function | Lambda |
'(closure) / '(closure*) |
captures the enclosing locals — GCC only, clang rejects it | Closures |
(closure) / def-closure |
captures by value, portable | Closures |
alloc |
malloc / calloc + automatic free |
Dynamic Memory Allocation |
An attribute is a parenthesised form written in front of the clause it modifies, and it
applies to the next clause only. Several may be stacked: (extern) (decl) (func …).
| attribute | applies to | effect |
|---|---|---|
decl |
func, struct |
declaration only, no body |
static |
func, var, let binding |
static |
extern |
func, var |
extern |
inline |
func |
__attribute__((weak)) |
auto |
func |
auto storage class |
register |
var, let binding |
register |
volatile |
func, var, let binding |
volatile |
thread-local |
var, let binding |
__thread |
atomic |
var, let binding |
_Atomic |
defer |
var, let binding |
__attribute__((__cleanup__(…))) |
non-copy |
struct |
the type may only be moved, never copied |
tested in
test/c/variable.cicili
A Cicili name must be a valid C identifier: it starts with a letter or _ and continues
with letters, digits or _. Two characters get special treatment before that check:
-
_joins the parts of a generic name.(<> free rc a)is the symbolfree_rc_a, and that is what reaches C unchanged.It used to be
^, folded to_on the way out. Two names for one thing is two chances to disagree, and they did: a declaration folded while a$member access did not, so($ b (<> find int))emittedb . find_int— which no C compiler accepts — and the symbol-table lookup missed as well. Nothing noticed becauselib/stdonly ever reached its specialisations as free functions, where the folding happened to line up.
Source is read with case preserved, so Employee and employee are different names, and
Common Lisp forms inside macro files are conventionally written in upper case (DEFMACRO,
LET*) to keep them visually distinct from Cicili clauses.
(var int amount)
(var double total)
(var double * total2)
int amount;
double total;
double * total2;tested in
test/c/types.cicili,test/c/string.cicili
(var const int SIDE . 10)
(var const int * SIDE1 . #'(aof SIDE))
(var const int * const SIDE2 . #'(aof SIDE1))
const int SIDE = 10;
const int * SIDE1 = &SIDE;
const int * const SIDE2 = &SIDE1;. introduces an initializer. #'( … ) marks the initializer as an expression rather
than more type words — without it the reader cannot tell (var int x . f) (initialize with
the variable f) from a call.
tested in
test/c/operator.cicili
| cicili | C |
|---|---|
+ |
+ |
- |
- |
* |
* |
/ |
/ |
% |
% |
Binary operators are n-ary: (+ a b c) is (a + b + c ). Every operator expression is
emitted fully parenthesised, so C precedence never surprises you.
(set total (+ total amount))
(let ((int i . 3)
(int j . 7)
(int k))
(set k (+ i j)))
total = total + amount;
{
int i = 3;
int j = 7;
int k;
k = i + j;
}| cicili | C |
|---|---|
++ |
prefix ++ |
-- |
prefix -- |
1+ |
postfix ++ |
1- |
postfix -- |
1+and1-are postfix++/--, not Lisp's "add one". Both families take exactly one operand.
(source "main.c" ()
(include <stdio.h>)
(func main ()
(let ((int a . 5)
(int b . 5))
;; Print them, decrementing each time.
;; Postfix for a, prefix for b.
(printf "\n%d %d" (1- a) (-- b))
(printf "\n%d %d" (1- a) (-- b))
(printf "\n%d %d" (1- a) (-- b)))))
#include <stdio.h>
int main()
{
{
int a = 5;
int b = 5;
/* Print them, decrementing each time. */
/* Postfix for a, prefix for b. */
printf("\n%d %d", a--, --b);
printf("\n%d %d", a--, --b);
printf("\n%d %d", a--, --b);
}
}| cicili | C |
|---|---|
== |
== |
!= |
!= |
> |
> |
< |
< |
>= |
>= |
<= |
<= |
| cicili | C |
|---|---|
and |
&& |
or |
|| |
not |
! |
| cicili | C |
|---|---|
<< |
<< |
>> |
>> |
~ |
~ |
bitand |
& |
bitor |
| |
xor |
^ |
^ |
^ |
| cicili | C |
|---|---|
set |
= |
= |
= |
+= |
+= |
-= |
-= |
*= |
*= |
/= |
/= |
%= |
%= |
<<= |
<<= |
>>= |
>>= |
The compound forms take exactly three elements — (+= x 1) — and are statements. Use
set in the ordinary case; see Assignment.
| cicili | C |
|---|---|
? |
?: |
? takes exactly four elements; both arms are mandatory.
(set a (? (== b 2) 20 30))
a = ((b == 2) ? 20 : 30);| cicili | C |
|---|---|
sizeof |
sizeof() |
typeof |
typeof() |
aof |
& |
cof |
* |
nth |
[] |
sizeof reads its argument two ways: if it is a list it is an expression,
(sizeof (nth 0 digits)); otherwise it is a type descriptor, (sizeof int),
(sizeof const char *).
(printf "%zu %zu\n" (sizeof int) (sizeof (nth 0 digits)))
printf("%zu %zu\n", sizeof(int), sizeof(digits[0]));Cicili keeps C's split between statements and expressions. set, the compound assignments,
if, while, for, do, cond, switch, return, let and block are statements
— they cannot appear where a value is required.
When you need a value out of several forms, use progn or letn. They are the two
block forms that produce a value. See Blocks.
tested in
test/c/types.cicili
ANSI C provides three kinds of data type:
- Primary (built-in):
void,int,char,double,float. - Derived: array, pointer, function pointer.
- User defined: structure, union, enumeration.
Cicili supports declaration and definition of all of them.
| cicili | C |
|---|---|
nil |
NULL |
void |
void |
bool |
bool |
char |
char |
uchar |
unsigned char |
short |
short |
ushort |
unsigned short |
int |
int |
uint |
unsigned int |
long |
long |
ulong |
unsigned long |
llong |
long long |
ullong |
unsigned long long |
i8 |
int8_t |
u8 |
uint8_t |
i16 |
int16_t |
u16 |
uint16_t |
i32 |
int32_t |
u32 |
uint32_t |
i64 |
int64_t |
u64 |
uint64_t |
i128 |
__int128 |
u128 |
unsigned __int128 |
float |
float |
double |
double |
real |
long double |
auto |
__auto_type |
Any other symbol in type position is passed through to C unchanged, so size_t, FILE,
pthread_t and your own typedefs all work. lib/std/c declares the C standard library and
POSIX so type inference knows them — see lib/std/c/README.md.
Every place a type appears — a variable, a parameter, a struct member, a return type, a cast — uses one grammar, read positionally:
[const] TYPE [modifier] [const|restrict] [name] [array]
- modifier is one of
*,**,***,&(C++ reference),move,ref. - the pointer qualifier slot takes
constorrestrict, and requires a*modifier. - array is
[],[N]or[N][M]; at most two dimensions. - the name may be omitted, which is how you declare an unnamed parameter.
(const char * restrict format) ; const char * restrict format
(char * const argv []) ; char * const argv []
(int) ; int -- unnamed parameter
(Employee ** emp) ; Employee ** emp
move and ref are ownership markers used by the functional layer: move emits nothing at
all, ref emits * restrict.
tested in
test/c/variable.cicili
(source "main.c" ()
(func main ()
(let ((double price . 500.4) ; atom initialization
(double price_array [] . '{100.2 230.7 924.8}) ; list initialization
(double price_calc . #'(calculate_price)) ; from a function call
(auto identity . '(lambda ((int x)) (out int) (return x))))))) ; lambda
int __ciciliL_178 (int x) {
return x ;
}
int main () {
{
double price = 500.4;
double price_array[] = {100.2, 230.7, 924.8};
double price_calc = calculate_price ();
__auto_type identity = __ciciliL_178 ;
}
}Four initializer forms:
| form | meaning |
|---|---|
. 500.4 |
an atom |
. '{ a b c } |
a brace list — { a, b, c } |
. #'( … ) |
an expression, usually a call |
. '(lambda … ) |
a lambda; the value is the generated function's name |
Inside a brace list, an element written $field becomes a designated initializer:
'{ $a x $b y } is { .a = x, .b = y }.
A free variable is a global; use let for variables inside a function. Attributes:
(static)(extern)(register)(volatile)(thread-local)(atomic)(defer …)
(register) (var int height . 5)
(var char letter . #\A)
(var float age)
(extern) (var float area)
(static) (var double d)
(thread-local) (var int slot)
;; actual initialization
(set age 26.5)
register int height = 5;
char letter = 'A';
float age;
extern float area;
static double d;
__thread int slot;
/* actual initialization */
age = 26.5;
(auto)is not a variable attribute. In Ciciliautois a type —__auto_type— not a storage class. It is accepted as an attribute onfunconly.
let opens a scope and declares variables in it. Attribute markers appear as bare lists
inside the binding list and apply to the binding that follows:
(static)(register)(volatile)(thread-local)(atomic)(defer …)— a destructor, see Deferred cleanup
(source "main.c" ()
(func main ()
(let ((static) (int width . 3)
(register) (int height . 4)
(defer () (free (-> emp Name))
(free emp)
(printf "from defer, emp is freed\n"))
(Employee * emp . #'(alloc (sizeof Employee))))
(printf "area: %d" (* width height)))))
static void __ciciliL_105 (Employee ** emp_ptr) {
Employee * emp = (*emp_ptr);
free ((emp -> Name));
free (emp);
printf ("from defer, emp is freed\n");
}
int main () {
{ /* cicili#Let104 */
static int width = 3;
register int height = 4;
Employee * emp __attribute__((__cleanup__(__ciciliL_105))) = ((Employee *)malloc (sizeof(Employee)));
// ----------
printf ("area: %d", (width * height));
}
}Note what the destructor looks like: the parameter is a pointer to the variable, and the compiler rebinds the variable's own name and type on the first line, so the body you write reads exactly like the code around it.
let vs letn — same bindings, same scoping, one difference:
let |
letn |
|
|---|---|---|
| C form | { … } |
({ … }) — a GCC statement expression |
| has a value | no | yes: the last body form |
| usable as an expression | no | yes |
letn is how you introduce locals in the middle of an expression:
(printf "%d\n" (letn ((int a . 2) (int b . 3)) (* a b)))
printf ("%d\n", ({ int a = 2; int b = 3; (a * b); }));Generated code carries provenance comments —
{ /* cicili#Let104 */,/* cicili#Block106 */,/* cicili#Progn123 */— and a// ----------line between alet's declarations and its body. They are harmless, and they make the mapping from C back to Cicili obvious when you read the output.
auto asks the compiler to infer a variable's type from its initializer. When inference
succeeds the real type is written into the C output; when it cannot, __auto_type is
emitted and GCC/Clang finishes the job. Use it for function pointers, lambdas and closures,
whose types are tedious or impossible to spell.
typeof names a type by pointing at an expression. The expression is never evaluated, so
dummy arguments are idiomatic.
(var auto u2 . 1)
(var (typeof u2) u3 [1])
(var int u4 [] . '{ 2 3 })
(var (typeof (nth 0 u4)) u5 [2] . '{ 4 5 })
int u2 = 1;
int u3 [1];
int u4 [] = { 2, 3 };
typeof(u4[0]) u5 [2] = { 4, 5 };An auto variable may only take a named function as its defer destructor, and a
function declared (out auto) must contain a return.
(set width 60)
(set age 35)
(set width 65 age 40) ; multi assignment
width = 60;
age = 35;
width = 65;
age = 40;set takes any even number of arguments and assigns them in order — (set a b b a)
does not swap.
(source "main.c" ()
(include <stdio.h>)
(func main ()
(let ((int age . 33))
(printf "I am %d years old.\n" age))))
#include <stdio.h>
int main()
{
{
int age = 33;
printf("I am %d years old.\n", age);
}
}tested in
test/c/types.cicili,test/c/aggregate.cicili
(source "main.c" ()
(include <stdio.h>)
(func main ()
(let ((float a))
(set a (cast float (/ 15 6)))
(printf "%f" a))))
#include <stdio.h>
int main ()
{
{
float a;
a = ((float)(15 / 6));
printf("%f", a);
}
}The type slot accepts a full type descriptor, so (cast (Employee *) p),
(cast (const char * const) s) and (cast (typeof x) y) all work. A code clause is
accepted too, for a type Cicili has no spelling for.
tested in
test/c/shared.cicili,test/c/library.cicili
A Cicili program is one or more targets. Each target names a C file and a list of
features, and translates its clauses into that file. header targets compile their content
and are never handed to the C compiler; source targets are
resolved, compiled and linked.
(source "path/to/file.c" (:key value …) clause…)
(header "path/to/file.h" (:key value …) clause…)
The feature list must have an even number of elements. It is macro-expanded first, so a
macro may produce it — see the make macro in builtins.cicili.
Every feature may be omitted. #t selects the default behaviour, #f does nothing.
:std— writes the standard library includes at the top of the file.
(source "main.c"
(:std #t)
;; some forms
)
#include <stdio.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>Under :cpp #t it emits <string> and <iostream> instead.
:compile— compiles the target file. The default is-c target.c. A string or list is passed to the compiler configured inconfig.lisp. A custom:compilemust contain-cor--compile; the argument right after it is replaced with the target file name.:link— links the target as a library or an executable. There is no default behaviour; a string or list is passed to the linker fromconfig.lisp.:linkonly runs if:compileappears before it in the feature list and succeeded.:cpp— use the C++ compiler and linker fromconfig.lisp. See C++ Compiler.:haskell— includeshaskell.hand adds the runtime's include/link flags.
Two placeholders are substituted in :compile and :link:
{$CWD}— the current working directory.{$CCL}— the Cicili installation directory.
;; MyMath library declaration
(header "mymath.h"
(:compile #f)
(guard __MYMATH_H__
(decl) (func obj1_does ((int) (int)) (out int))
(decl) (func obj2_does ((int) (int)) (out int))
(decl) (func obj3_does ((int) (int)) (out int))))
;; Default compilation
(source "obj1.c"
(:compile #t)
(include "mymath.h")
(func obj1_does ((int x) (int y)) (out int)
(return (+ x y))))
;; Custom compilation
(source "obj2.c"
(:compile "-c obj2.c -o objmul.lo")
(include "mymath.h")
(func obj2_does ((int x) (int y)) (out int)
(return (* x y))))
;; Library creation and linking
(source "obj3.c"
(:compile #t :link "-o libMyMath.la -L{$CWD} obj1.lo objmul.lo obj3.lo")
(include "mymath.h")
(func obj3_does ((int x) (int y)) (out int)
(return (obj1_does (obj2_does x y) (obj2_does x y)))))
;; Executable creation and linking
(source "main.c"
(:std #t :compile #t :link "-o CompileTest -L{$CWD} main.lo -lMyMath")
(include "mymath.h")
(func main ((int argc) (char * argv []))
(if (!= argc 3)
(block
(printf "two digits needed!")
(return EXIT_FAILURE)))
(let ((int x . #'(atoi (nth 1 argv)))
(int y . #'(atoi (nth 2 argv))))
(printf "MyMath lib outputs: %d\n" (obj3_does x y)))
(return EXIT_SUCCESS)))
cicili % sbcl --script cicili.lisp test/test.cicili
software type: "Darwin"
arg specified: test/mylib.cicili
cicili: specifying target mymath.h
cicili: resolving target mymath.h
cicili: specifying target obj1.c
cicili: resolving target obj1.c.run1.c
run out 1 > glibtool: compile: clang -g -O "" -c obj1.c.run1.c -o obj1.c.run1.o
cicili: compiling target obj1.c
glibtool: compile: clang -g -O "" -c obj1.c -o obj1.o
cicili: specifying target obj2.c
cicili: compiling target obj2.c
glibtool: compile: clang -g -O "" -c obj2.c -o objmul.o
cicili: specifying target obj3.c
cicili: compiling target obj3.c
glibtool: link: ar cr .libs/libMyMath.a .libs/obj1.o .libs/objmul.o .libs/obj3.o
cicili: specifying target main.c
cicili: compiling target main.c
glibtool: link: clang -g -O "" -o CompileTest .libs/main.o -L{$CWD} libMyMath.a
- Documentation — anything after
;. The convention is;;;;for a file,;;;for a target,;;for a block, and a trailing;for one form.
;;;; about a cicili file
;;; author, licence and/or documentation about each target
(var long height) ; description of a form
(func sqr ((double a))
(out double)
;; some commented code or documentation inside code
(return (* a a)))
- Preprocessor forms — see Preprocessor Forms.
- Main function — every program has exactly one.
(main …)and(main* …)are macros for the two usual shapes:
(main (printf "hello\n") (return 0))
(main* (printf "%s\n" (nth 0 argv)) (return 0))
int main () { printf ("hello\n"); return 0; }
int main (int argc, char * argv []) { printf ("%s\n", argv[0]); return 0; }tested in
test/c/shared.cicili
include takes one or more headers. A symbol prints bare, a string prints quoted.
(include <stdio.h> <stdlib.h> "basic.h")
#include <stdio.h>
#include <stdlib.h>
#include "basic.h"tested in
test/c/macro.cicili
import loads a macro file — a file of DEFMACRO, DEFUN and generic definitions,
plus further imports. It runs while your file is being read, before any target is specified.
(import "lib/std/prelude.cicili") ; from the cicili installation directory
(import "./mymacros.cicili") ; relative to this file
(import "/opt/shared/macros.cicili"); absolute
Path resolution is decided by the first character: . means relative to the importing file,
/ means an absolute path, anything else is resolved against the Cicili installation
directory.
A second argument names the import, and each macro in the file is then registered as
<prefix>.<name>:
(import "./helpers.cicili" :util "from macro test") ; -> (util.half 84)
The third argument is passed to the file's init function at import time. Without a
prefix the macros are registered under their bare names.
Every library under lib/ declares its own package, and a library that spans several
files shares one across all of them:
(DEFPACKAGE :std
(:USE :COMMON-LISP)
(:IMPORT-FROM :COMMON-LISP-USER "import" "generic" "cicili"))
(IN-PACKAGE :std)
The DEFPACKAGE is evaluated as the file is read, so the IN-PACKAGE below it has a
package to enter. The options are upper case and the package name is not: a macro file is
read with the case preserved, so :use would read as :|use| and DEFPACKAGE would
reject it.
:IMPORT-FROM is not decoration. import, generic and cicili are Lisp definitions
builtins.cicili makes in CL-USER, and a macro file is also CL:LOADed — so a file
that has entered a package of its own and then writes (import "./other.cicili") reads
import as a fresh symbol of its own and dies on an undefined function. Every library
carries the same clause whether or not it uses one today.
What the package is for, and what it is not. It owns the library's Lisp definitions
— its helper DEFUNs, its parameters — so two libraries with a helper of the same name no
longer overwrite each other in CL-USER. It does not hide the library's macros:
those are registered in a table keyed by symbol name and reached that way from any
package, which is what lets a prefix be applied to the registered name rather than to the
symbol.
The prefix is the importer's, not the library's. A library that declares :parsi can
still be imported as :zz, :p, or with no prefix at all; the package name and the
prefix are unrelated.
A library may export a Lisp function for the importer to call — lib/cpp/memory.cicili
exports shared-ptr< so a target's init-macro can splice declarations. An import that
takes no prefix copies each exported definition onto the importing package's own
symbol of the same name, so the function is callable unqualified. An import that takes a
prefix leaves it where it is, and reaches it as memory:shared-ptr<. Macro names are
never exported: they arrive through the prefix mechanism instead.
Compare symbols by name inside a library. CICILI:KEY-EQ compares two symbols by
SYMBOL-NAME; EQL compares identity. A library that inspects a form its caller wrote
— (IF (EQL (CAAR body) 'out) …) — is comparing the caller's out with its own, and once
the library has a package of its own those are two symbols with one name. Use KEY-EQ.
For the same reason, expanding a form the caller wrote goes through
expand-form< rather than CL:MACROEXPAND, which asks for the macro function of the
symbol in hand.
A package also lets a library name a macro after a Common Lisp symbol, by shadowing it
first — (DEFPACKAGE :parsi (:USE :COMMON-LISP) (:SHADOW "CLASS")). Lower-case names
avoid the question entirely and are what lib/parsi uses; see below.
Name a macro in lower case. Cicili dispatches a target's forms through a table keyed
by symbol name, so an unprefixed macro is found inside a target whatever package holds
it — including one called CLASS. A form Common Lisp evaluates is different:
compile-ast hands a top-level form it does not recognise to CL:EVAL, which resolves by
symbol identity, and a name owned by COMMON-LISP or SB-ALIEN cannot be interned in the
importing file's package to be found there. An upper-case CLASS, TYPE, SEQUENCE or
ENUM therefore needs a :shadow to define at all, and even then works only inside a
target — not from a DEFPARAMETER.
A macro file is read with the case preserved, so lower case avoids all of it:
class is simply not CL:CLASS, and lib/parsi/parsi.cicili names its twelve object
macros that way for exactly this reason. Import them with a prefix or without; both work.
The other reason to use one is hygiene: unprefixed names are registered globally and apply to every file compiled in the same process.
The prefix and the namespace are independent, so nil in the second position means
"namespace, no prefix":
(import "lib/parsi/parsi.cicili" :parsi "demo") ; -> (parsi.PAGE …), demo::hello
(import "lib/parsi/parsi.cicili" nil "site") ; -> (PAGE …), site::hello
example/parsi-prefixed.cicili and example/parsi-unprefixed.cicili are the same page written both ways.
tested in
test/c/shared.cicili
(guard __STUDENT_H__
(struct Student
(member char name [50])
(member char family [50])
(member int class_no)))
#ifndef __STUDENT_H__
#define __STUDENT_H__
typedef struct Student {
char name [50];
char family [50];
int class_no;
} Student;
#endif /* __STUDENT_H__ */A guard body accepts everything a target accepts, so a whole header can live inside one.
tested in
test/c/preprocess.cicili
Any clause whose head starts with @ becomes a preprocessor directive of the same name.
It takes at most two arguments, and the payload is normally a code clause, because code
passes text through untouched.
(@define (code "SHA1_ROTL(bits, word) (((word) << (bits)) | ((word) >> (32-(bits))))"))
(struct SHA512Context
(@ifdef USE_32BIT_ONLY)
(member uint32_t Intermediate_Hash[(/ SHA512HashSize 4)]) ; Message Digest
(member uint32_t Length[4]) ; Message length in bits
(@else) ; !USE_32BIT_ONLY
(member uint64_t Intermediate_Hash[(/ SHA512HashSize 8)]) ; Message Digest
(member uint64_t Length_High)
(member uint64_t Length_Low) ; Message length in bits
(@endif) ; USE_32BIT_ONLY
(member int_least16_t Message_Block_Index) ; Message_Block array index
(member uint8_t Message_Block[SHA512_Message_Block_Size]) ; 1024-bit message blocks
(member int Computed) ; Is the hash computed?
(member int Corrupted)) ; Cumulative corruption code
#define SHA1_ROTL(bits, word) (((word) << (bits)) | ((word) >> (32-(bits))))
typedef struct SHA512Context {
#ifdef USE_32BIT_ONLY
uint32_t Intermediate_Hash [SHA512HashSize / 4];
uint32_t Length [4];
#else
uint64_t Intermediate_Hash [SHA512HashSize / 8];
uint64_t Length_High;
uint64_t Length_Low;
#endif
int_least16_t Message_Block_Index;
uint8_t Message_Block [SHA512_Message_Block_Size];
int Computed;
int Corrupted;
} SHA512Context;Preprocessor forms are legal at target level and inside guard, struct, union and
function bodies.
tested in
test/c/preprocess.cicili
code emits its argument verbatim. It is valid as an expression, as a statement, and in
type position.
(code "__builtin_unreachable()")
(cast (code "struct sockaddr *") p)
tested in
test/c/control.cicili
if takes a condition, a then-form, and an optional else-form. Each branch is exactly one
form — use block for several.
(let ((int a . 5)
(int b . 6))
(if (> a b)
(printf "a is greater")
(printf "maybe b is greater")))
{
int a = 5;
int b = 6;
if (a > b)
printf("a is greater");
else
printf("maybe b is greater");
}(let ((int a . 5)
(int b . 6))
(if (> a b)
(block
(printf "a is greater")
(set a (* a b)))
(block
(printf "maybe b is greater")
(set b (* b a)))))
{
int a = 5;
int b = 6;
if (a > b) {
printf("a is greater");
a = a * b;
} else {
printf("maybe b is greater");
b = b * a;
}
}cond is an if / else if chain. Unlike if, each clause body may hold any number of
forms, and every body is braced.
(cond ((== x 1) (printf "x is 1\n"))
((== x 2) (printf "x is 2\n")
(set x 0))
(#t (printf "x is ?\n")))
if (x == 1) {
printf ("x is 1\n");
}
else if (x == 2) {
printf ("x is 2\n");
x = 0;
}
else if (true) {
printf ("x is ?\n");
}
condhas no default clause. A trailing(#t …)compiles toelse if (true), which needs<stdbool.h>— i.e.:std #t.
(let ((int a))
(printf "Please enter a number between 1 and 5: ")
(scanf "%d" (aof a))
(switch a
(case 1 (printf "You chose One") break)
(case 2 (printf "You chose Two") break)
(case 3 (printf "You chose Three") break)
(case 4 (printf "You chose Four") break)
(case 5 (printf "You chose Five") break)
(default (printf "Invalid Choice."))))
{
int a;
printf("Please enter a number between 1 and 5: ");
scanf("%d", &a);
switch (a) {
case 1:
printf("You chose One");
break;
case 2:
printf("You chose Two");
break;
case 3:
printf("You chose Three");
break;
case 4:
printf("You chose Four");
break;
case 5:
printf("You chose Five");
break;
default:
printf("Invalid Choice");
}
}Every child of a switch must be a case or a default. No break is inserted for
you — cases fall through exactly as in C, which is why every branch above ends with an
explicit break.
tested in
test/c/control.cicili
(let ((int n . 1)
(int times . 5))
(while (<= n times)
(printf "cicili while loops: %d\n" n)
(1+ n)))
{
int n = 1;
int times = 5;
while (n <= times) {
printf("cicili while loops: %d\n", n);
n++;
}
}(let ((int n . 1)
(int times . 5))
(do
(printf "cicili do loops: %d\n" n)
(1+ n)
(<= n times))) ; the LAST form of do is the condition
{
int n = 1;
int times = 5;
do {
printf("cicili do loops: %d\n", n);
n++;
} while (n <= times);
}The last form of
dois the loop condition, not a body statement.
for takes an initializer list, a test, a list of step forms, and a body.
(let ((int n)
(int times))
(for ((n . 1)
(times . 5)) ; initialize
(<= n times) ; test
((1+ n)) ; step -- a list of forms
(printf "cicili for loop: %d\n" n)))
(for ((int n . 1)
(int times . 2)) ; initialize, declaring as you go
(<= n times) ; test
((1+ n)) ; step
(printf "another initialization for loop: %d\n" n))
{
int n;
int times;
for (n = 1, times = 5; (n <= times); (n ++)) {
printf ("cicili for loop: %d\n", n);
}
}
for (int n = 1, times = 2; (n <= times); (n ++)) {
printf ("another initialization for loop: %d\n", n);
}An initializer written (name . value) assigns to an existing variable; written
(type name . value) it declares a new one. The step list accepts only simple forms:
atoms, unary operators, assignments, set, and calls.
Both are bare symbols, usable anywhere C allows them.
(while (!= c EOF)
(if (== c #\Space) continue)
(if (== c #\Newline) break)
(1+ count))
tested in
test/c/control.cicili
block |
progn |
|
|---|---|---|
| C form | { … } compound statement |
({ … }) GCC statement expression |
| has a value | no | yes — the last form |
| usable as an expression | no | yes |
| portability | ISO C | GCC / Clang extension |
(block
(printf "step one\n")
(printf "step two\n"))
(printf "%d\n" (progn (printf "side effect\n") 42))
{
printf ("step one\n");
printf ("step two\n");
}
printf ("%d\n", ({ printf ("side effect\n"); 42; }));block is what you reach for to put several statements in an if branch. progn and
letn are what you reach for when an expression needs several steps.
tested in
test/c/function.cicili
Points to know:
outsets the return type and must be the first form after the parameter list. A function with nooutreturnsvoid— exceptmain, which returnsint.- Attributes are set at declaration time, each in its own parentheses:
(decl)— declaration only, no body(static)(inline)— emits__attribute__((weak))(extern)(auto)(volatile)(resolve #f)— do not resolve this function
(source "main.c"
(:std #t :compile #t :link #t)
;; function declaration
(decl) (func addition ((int * a) (int * b)) (out int))
(func main ()
;; local variable definition
(let ((int answer)
(int num1 . 10)
(int num2 . 5)
(func aFuncPtr ((int * _) (int * _)) (out int) . addition)) ; function pointer
;; calling a function to get addition value
(set answer (addition (aof num1) (aof num2)))
(printf "The addition of two numbers is: %d\n" answer)
(set answer (aFuncPtr (aof num1) (aof num2)))
(printf "The addition of two numbers by function pointer is: %d\n" answer))
(return 0))
;; function returning the addition of two numbers
(func addition ((int * a) (int * b))
(out int)
(return (+ (cof a) (cof b)))))
#include <stdio.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
int addition (int * a, int * b);
int main () {
{
int answer;
int num1 = 10;
int num2 = 5;
int (*aFuncPtr) (int * , int * ) = addition;
answer = addition ((&num1), (&num2));
printf ("The addition of two numbers is: %d\n", answer);
answer = aFuncPtr ((&num1), (&num2));
printf ("The addition of two numbers by function pointer is: %d\n", answer);
}
return 0;
}
int addition (int * a, int * b) {
return ((*a) + (*b));
}A parameter is a type descriptor. Three shapes are worth naming:
(func f ((int) (int)) (out int)) ; unnamed parameters, for a declaration
(func g ((const char * restrict fmt) ($$$))) ; ($$$) is C's ...
(func h ((func cmp ((int a) (int b)) (out int)))) ; a function-pointer parameter
int f (int, int);
void g (const char * restrict fmt, ...);
void h (int (*cmp) (int a, int b));A func clause in type position is a function pointer. It is spelled exactly like a
function declaration, and works as a parameter, a struct member, or a variable:
(var func handler ((int sig)) . my_handler)
(member func resolve ((char * prob)) (out char *))
(cast (func _ ((void * args)) (out int)) p)
void (*handler) (int sig) = my_handler;
char * (*resolve) (char * prob);
((int (*) (void * args))p)Use _ as the pointer's name to get an unnamed (*).
For an array of function pointers the [] goes after the name and before the
parameter list, the same place it sits on any other variable:
(let ((func ops [] ((int a) (int b)) (out int) . '{ add sub }))
(printf "%d\n" ((nth 1 ops) 20 22)))
int (*ops[]) (int a, int b) = { add, sub };
printf ("%d\n", ops [1](20, 22));An inline struct in out position lets a function return several values. Its members
are bare declarators — no member keyword.
(source "main.c" (:std #t :compile #t :link #t)
(static) (func aMultiReturnFunc ((int x) (int y)) (out '{(int a) (int b)})
(return '{ x y }))
(static) (func aMultiReturnFuncS ((int x) (int y)) (out '{(int a) (int b)})
(let (((typeof (aMultiReturnFuncS x y)) s . '{ x y }))
(return s)))
(func main ()
(let ((int n . 3)
(int t . 4)
((typeof (aMultiReturnFunc 1 1)) mr)
((typeof (aMultiReturnFuncS 1 1)) mrt))
(set mr (aMultiReturnFunc n t))
(printf "a: %d, b: %d\n" ($ mr a) ($ mr b))
(set mrt (aMultiReturnFuncS (++ n) (++ t)))
(printf "a: %d, b: %d\n" ($ mrt a) ($ mrt b)))))
typedef struct __ciciliS_aMultiReturnFunc_ {
int a;
int b;
} __ciciliS_aMultiReturnFunc_;
static struct __ciciliS_aMultiReturnFunc_ aMultiReturnFunc (int x, int y) {
return ((struct __ciciliS_aMultiReturnFunc_){x , y});
}
typedef struct __ciciliS_aMultiReturnFuncS_ {
int a;
int b;
} __ciciliS_aMultiReturnFuncS_;
static struct __ciciliS_aMultiReturnFuncS_ aMultiReturnFuncS (int x, int y) {
{
typeof(aMultiReturnFuncS (x , y)) s = {x , y};
return ((struct __ciciliS_aMultiReturnFuncS_)s);
}
}
int main () {
{
int n = 3;
int t = 4;
typeof(aMultiReturnFunc (1, 1)) mr;
typeof(aMultiReturnFuncS (1, 1)) mrt;
mr = aMultiReturnFunc (n , t);
printf ("a: %d, b: %d\n", (mr . a), (mr . b));
mrt = aMultiReturnFuncS ((++n ), (++t ));
printf ("a: %d, b: %d\n", (mrt . a), (mrt . b));
}
}Two rules come with it:
- You cannot spell the generated type, so you name it with
typeofapplied to a call. The arguments insidetypeofare dummies — nothing is evaluated. - A multi-returning function must be
(static)in a source target, or declared(decl)in a header target and defined in a source target. Otherwise the generated struct never reaches the callers.
tested in
test/c/aggregate.cicili
A function that belongs to a type is an ordinary func that takes the value as its first
parameter. The name ties it to the type, and <> builds that name out of parts so the same
code can be generated for many types:
(struct Employee
(member int id)
(member char * name))
(func (<> toString Employee) ((Employee * employee) (FILE * file))
(fprintf file "#%d %s\n" (-> employee id) (-> employee name)))
(main
(let ((Employee e . '{ $id 1 $name "Ada" }))
((<> toString Employee) (aof e) stdout)))
void toString_Employee (Employee * employee, FILE * file) {
fprintf (file, "#%d %s\n", (employee -> id), (employee -> name));
}
int main () {
{
Employee e = { .id = 1, .name = "Ada" };
toString_Employee ((&e), stdout);
}
}(<> toString Employee) is the symbol toString_Employee, and that is the C name — see
Macros. Inside a generic the type part is a parameter, which is how
lib/std/vector.cicili writes one free that works for (<> vector int),
(<> vector char) and everything else:
(generic decl-vector (a)
(struct (<> vector a)
(member (<> rc (<> array a)) vec)
(member size_t low)
(member size_t high))
(inline)
(func (<> free (<> vector a)) (((<> vector a) * vector))
((<> free rc (<> array a)) (aof (-> vector vec)))))
Three access operators:
| operator | meaning |
|---|---|
($ obj member) |
obj.member — value member access; chains: ($ a b c) is a.b.c |
(-> ptr member) |
ptr->member |
(=> obj member arg…) |
calls a function stored in a member |
=> is for a member that holds a function pointer — (member func resolve ((char * prob)) (out char *))
is called with (=> emp resolve "why"). It is also how C++ member functions are called under
:cpp #t.
=> takes one level of access, and it resolves the member name in the type of the
object you give it. To reach a member of a nested struct, hand it that struct:
(=> ($ emp duty) describe 6) ; not (=> emp duty describe 6)
tested in
test/c/function.cicili
A lambda is a quoted lambda form. It is lifted to a top-level C function named
__ciciliL_<n>, and the expression's value is that function's name — so it works as an
argument, an initializer, or a defer destructor.
(let ((auto inc . '(lambda ((int x)) (out int) (return (+ x 1)))))
(printf "%d\n" (inc 41)))
int __ciciliL_1042 (int x) {
return (x + 1);
}
…
{
int (*inc) (int x) = __ciciliL_1042;
printf ("%d\n", inc (41));
}auto reads the binding two ways, and the difference is whether the lambda is called:
| written | inc is |
C type |
|---|---|---|
(auto inc . '(lambda ((int x)) (out int) …)) |
the function | int (*inc) (int x) |
(auto n . #'('(lambda ((int x)) (out int) …) 41)) |
what the call returned | int n |
You can always write the pointer type out instead of auto, which is the same thing:
(let ((func inc ((int x)) (out int) . '(lambda ((int x)) (out int) (return (+ x 1)))))
(printf "%d\n" (inc 41)))
'(lambda* NAME (params) …) is the same thing with a name you choose.
Because a lambda is lifted to file scope it cannot capture the enclosing function's locals. When you need capture, use a closure.
tested in
test/c/function.cicili
'(closure (param…) (out TYPE)? body…)
'(closure* NAME (param…) (out TYPE)? body…)
GCC only. A quoted closure becomes a GCC nested function, an extension clang has never implemented — it answers
error: function definition is not allowed here. On a clang toolchain (which is whatconfig.lispnames on macOS) these two forms cannot be used at all. Everything else in this section, and theclosuremacro below, is portable.
A closure becomes a GCC nested function inside a statement expression, so it can read the enclosing function's locals:
(let ((int n . 10)
(auto add_n . '(closure ((int x)) (out int) (return (+ x n)))))
(printf "%d\n" (add_n 5)))
{
int n = 10;
__auto_type add_n = ({
int __ciciliC_1121 (int x) {
return (x + n);
}
__ciciliC_1121 ;
});
printf ("%d\n", add_n (5));
}Unquoted, closure is a different thing entirely, and the one to reach for on clang:
(closure (NAME :capture value …) (out TYPE)? body…)
It is a named lambda at file scope, called immediately with the captured values passed as ordinary arguments — each parameter's type is inferred from the value it captures, so only the name is written:
(let ((int base . 40))
(printf "%d\n" (closure ((<> cl demo) :base (aof base))
(out int)
(return (+ (cof base) 2)))))
It expands to ('(lambda* NAME (inferred params…) body…) values…). Capture is by value —
capture an address, as above, when the body must reach the original. This is what every
let^… / take^… macro in lib/std is built on, and it is covered by
function.cicili.
For a closure that must outlive its scope — handed to a thread, stored in an event loop —
use def-closure, which copies the captured values into a struct:
(def-closure ((int * state_counter))
'(lambda ((Coroutine * ctx)) (out int)
(++ (cof state_counter))
(return 0)))
It generates a context struct holding a routine function pointer plus the captures, and
evaluates to a value of that struct. Call it with (exec-closure c args…), which expands to
c.routine(&c, args…). Because it is a plain struct value it can be memcpy'd onto the
heap — this is exactly how lib/std/pthread implements go.
tested in
test/c/string.cicili
(var double amount [5])
(var int digits [] . '{ 1 2 3 4 5 })
(var char hw [][6] . '{ "Hello" "World" })
int digits[] = {1, 2, 3, 4, 5};
char hw[][6] = { "Hello", "World" };nth takes the index first, then the base — the reverse of C.
(var int myArray [5])
;; Initializing elements of the array separately
(for ((int n . 0))
(< n (/ (sizeof myArray) (sizeof int)))
((1+ n))
(set (nth n myArray) n))
int myArray[5];
/* Initializing elements of the array separately */
for (int n = 0; n < sizeof(myArray) / sizeof(int); n++)
{
myArray[n] = n;
}tested in
test/c/string.cicili
(var char name [6] . '{#\C #\l #\o #\u #\d #\Null})
(var char name [] . "Cloud")
(var char * name . "Cloud")
char name[6] = {'C', 'l', 'o', 'u', 'd', '\0'};
char name[] = "Cloud";
char * name = "Cloud";C escape sequences inside a string literal pass through unchanged, so "%d\n" means what
you expect.
| cicili | C |
|---|---|
#\Null |
'\0' |
#\Space |
' ' |
#\Newline |
'\n' |
#\Linefeed |
'\n' |
#\Return |
'\r' |
#\Tab |
'\t' |
#\Backspace |
'\b' |
#\Page |
'\v' |
#\Rubout |
'\x7F' |
Any other character prints as itself: #\A is 'A'.
tested in
test/c/memory.cicili
(var int * width)
(var char * letter)
int *width;
char *letter;(source "main.c" ()
(include <stdio.h>)
(func main ((int argc) (char * argv []))
(let ((int n . 20)
(int * pntr)) ; actual and pointer variable declaration
(set pntr (aof n)) ; store address of n in the pointer variable
(printf "Address of n variable: %x\n" (aof n))
;; address stored in the pointer variable
(printf "Address stored in pntr variable: %x\n" pntr)
;; access the value using the pointer
(printf "Value of *pntr variable: %d\n" (cof pntr)))
(return 0)))
#include<stdio.h>
int main (int argc, char *argv[])
{
{
int n = 20;
int *pntr; /* actual and pointer variable declaration */
pntr = &n; /* store address of n in pointer variable */
printf("Address of n variable: %x\n", &n);
/* address stored in pointer variable */
printf("Address stored in pntr variable: %x\n", pntr);
/* access the value using the pointer */
printf("Value of *pntr variable: %d\n", *pntr);
}
return 0;
}tested in
test/c/memory.cicili,test/std/defer.cicili
C's malloc(), calloc(), realloc() and free() are available directly. On top of them
Cicili adds alloc, which casts for you and installs an automatic free, and defer,
which lets you attach any destructor to any variable.
(let ((char * mem_alloc . #'(malloc (* 15 (sizeof char))))) ; allocated by hand
(if (== mem_alloc nil) (printf "Couldn't allocate the requested memory\n"))
(free mem_alloc))
{
char * mem_alloc = malloc(15 * sizeof(char)); /* allocated by hand */
if (mem_alloc == NULL) {
printf("Couldn't allocate the requested memory\n");
}
free(mem_alloc);
}| form | C |
|---|---|
#'(alloc SIZE) |
((T *)malloc(SIZE)) |
#'(alloc COUNT SIZE) |
((T *)calloc(COUNT, SIZE)) |
The cast is built from the variable's own declared type, so it always matches. An
alloc-initialized variable with no explicit defer automatically gets one that frees
it at the end of the scope.
(func main ()
(let ((defer () (printf "x was %d\n" (cof x)))
(int x . 6)
(int * ax . #'(alloc 5 (sizeof int))))
(printf "x is %d\n" x)))
void __ciciliL_178 (int * x) {
printf("x was %d\n", (*x));
}
void __ciciliL_179 (int ** ax) {
free (((void *)(*ax)));
}
int main () {
{
int x __attribute__((__cleanup__(__ciciliL_178))) = 6;
int * ax __attribute__((__cleanup__(__ciciliL_179))) = ((int *)calloc(5, sizeof(int)));
printf("x is %d\n", x);
}
}(let ((int n_rows . 4)
(int n_columns . 5)
(int ** matrix . #'(alloc (* (* n_rows n_columns) (sizeof int)))))
(printf "Matrix allocated\n"))
void __ciciliL_178 (int *** matrix) {
free (((void *)(*matrix)));
}
int main () {
{
int n_rows = 4;
int n_columns = 5;
int ** matrix __attribute__((__cleanup__(__ciciliL_178))) = ((int **)malloc(((n_rows * n_columns) * sizeof(int))));
printf ("Matrix allocated\n");
}
}tested in
test/c/memory.cicili,test/std/defer.cicili
defer is an attribute on the next binding. Three spellings, and only three:
| form | meaning |
|---|---|
(defer #t) |
pure free — generate a destructor that frees the variable |
(defer () func_name) |
use an already-defined function as the destructor |
(defer () form…) |
generate a destructor whose body is form… |
C's __cleanup__ hands the destructor a pointer to the variable — one more level of
indirection than the variable itself. That matters when you write the
(defer () func_name) form, because you have to declare func_name with the deeper type:
| variable | destructor parameter |
|---|---|
(int x) |
(int * x) |
(int * p) |
(int ** p) |
(int ** m) |
(int *** m) |
With the generated form, (defer () form…), you never see that. Cicili rebinds the
variable's own name and type on the first line of the destructor, so the body reads exactly
like the surrounding code:
(func file_close ((FILE ** file_ptr))
(printf "file closed\n")
(fclose (cof file_ptr)))
(main
(let ((defer () file_close)
(FILE * f . #'(fopen "notes.txt" "r"))
(defer () (printf "emp id is %d\n" (-> emp Id))
(free emp))
(Employee * emp . #'(alloc (sizeof Employee))))
(printf "working\n")))
static void __ciciliL_105 (Employee ** emp_ptr) {
Employee * emp = (*emp_ptr); /* the rebinding Cicili writes for you */
printf ("emp id is %d\n", (emp -> Id));
free (emp);
}
int main () {
{ /* cicili#Let104 */
FILE * f __attribute__((__cleanup__(file_close))) = fopen ("notes.txt", "r");
Employee * emp __attribute__((__cleanup__(__ciciliL_105))) = ((Employee *)malloc (sizeof(Employee)));
// ----------
printf ("working\n");
}
}(defer #t) generates the same shape with a fixed body:
static void __ciciliL_107 (Employee ** empOther) {
free (((void *)(*empOther)));
}Providing your own defer suppresses alloc's automatic free — you become responsible
for the memory. An auto-typed variable may only use the (defer () func_name) form.
tested in
test/c/memory.cicili,test/std/defer.cicili,test/std/thread.cicili
defer* defers a block to the end of the current scope, capturing the values it names at
the point the defer* runs:
(defer* ((FILE * file) (char * message))
(fprintf file "%s\n" message)
(fclose file))
It snapshots the named variables into a hidden struct and attaches a destructor that unpacks them again:
typedef struct __ciciliS_133 {
FILE * file ;
char * message ;
} __ciciliS_133;
static void __ciciliL_134 (struct __ciciliS_133 * ciciliDefer131_ptr) {
FILE * file = (ciciliDefer131_ptr -> file);
char * message = (ciciliDefer131_ptr -> message);
fprintf (file, "%s\n", message);
fclose (file);
}
…
struct __ciciliS_133 ciciliDefer131
__attribute__((__cleanup__(__ciciliL_134))) = { file , message };The capture is by value at the point the defer* runs, so later reassignment of file
does not change what gets closed. Because defer* introduces an inline struct into the
function, the same rule as multi-value returns applies: the function must be declared in a
header and defined in a source, or be static in a source.
tested in
test/c/aggregate.cicili
Use $ for a struct member and -> for a member of a pointer to a struct. A function that
belongs to the type is a plain func taking the value as its first parameter — see
Struct-associated functions.
declare names the variable(s) of an anonymous nested struct or union; it is only legal
there.
(header "course.h" ()
(struct Course
(member char WebSite [50])
(member char Subject [50])
(member int Price))
(decl) (func printCourse ((Course co))))
(source "course.c" (:std #t :compile #t :link #t)
(include "course.h")
(var Course c1 . '{"domain.com" "Compilers" 100})
(var Course * pc1 . #'(aof c1))
(func printCourse ((Course co))
(printf "Course: %s in %s for %d$\n"
($ co Subject)
($ co WebSite)
($ co Price)))
(func main ()
(printCourse c1)
(printCourse (cof pc1))))
// course.h
typedef struct Course {
char WebSite[50];
char Subject[50];
int Price;
} Course;
void printCourse (Course co);
// course.c
Course c1 = {"domain.com", "Compilers", 100};
Course * pc1 = (&c1);
void printCourse (Course co) {
printf ("Course: %s in %s for %d$\n", (co . Subject), (co . WebSite), (co . Price));
}
int main () {
printCourse(c1);
printCourse((*pc1));
}(struct Employee
(member int id)
(member char * name)
(union
(member int tag_id)
(member char * custom_tag)
(declare tag))
(struct
(member int role_id)
(member func resolve ((char * prob)) (out char *))
(declare role)))
typedef struct Employee {
int id;
char * name;
union {
int tag_id;
char * custom_tag;
} tag;
struct {
int role_id;
char * (*resolve) (char * prob);
} role;
} Employee;Anonymous struct and union members are legal only nested, and they need a declare.
(decl) on a struct emits only the typedef — the body you write is used for type inference
but never printed. This is how you declare an opaque type:
(decl) (struct FILE)
typedef struct FILE FILE;(decl)— forward declaration, as above.(non-copy)— values of this type may only be moved, never copied. Used by the functional layer's ownership model.
tested in
test/c/aggregate.cicili
declare names the variable(s) of an anonymous nested union. Unions support the
like structs.
(union Mixed
(member int x)
(member float y))
(struct USHAContext
(member int whichSha) ; which SHA is being used
(union
(member SHA1Context sha1Context)
(member SHA224Context sha224Context)
(member SHA256Context sha256Context)
(member SHA384Context sha384Context)
(member SHA512Context sha512Context)
(declare ctx)))
typedef union Mixed {
int x;
float y;
} Mixed;
typedef struct USHAContext {
int whichSha;
union {
SHA1Context sha1Context;
SHA224Context sha224Context;
SHA256Context sha256Context;
SHA384Context sha384Context;
SHA512Context sha512Context;
} ctx;
} USHAContext;A union nested inside a struct should be written anonymously, with
declare. Unions take no attributes, so there is no forward-declaration form for one.
tested in
test/c/aggregate.cicili
Each constant is a dotted pair: (NAME . value), or (NAME) to continue the sequence.
(NAME value) is a syntax error.
(enum
(shaSuccess . 0)
(shaNull) ; Null pointer parameter
(shaInputTooLong) ; input data too long
(shaStateError) ; called Input after FinalBits or Result
(shaBadParam)) ; passed a bad parameter
(enum COLORS (RED . 0) (GREEN) (BLUE))
enum {
shaSuccess = 0,
shaNull,
shaInputTooLong,
shaStateError,
shaBadParam
};
typedef enum COLORS {
RED = 0,
GREEN,
BLUE
} COLORS;An anonymous enum is legal anywhere; a named one becomes a typedef. enum takes no
attributes.
tested in
test/c/preprocess.cicili,test/c/shared.cicili
(guard __STUDENT_H__
(struct Student
(member char name [50])
(member char family [50])
(member int class_no)))
#ifndef __STUDENT_H__
#define __STUDENT_H__
typedef struct Student {
char name [50];
char family [50];
int class_no;
} Student;
#endif /* __STUDENT_H__ */tested in
test/c/types.cicili
(typedef int * intptr_t)
(typedef FILE * cfile_t)
(typedef func handler_t ((int sig)))
typedef int * intptr_t;
typedef FILE * cfile_t;
typedef void (*handler_t) (int sig);The last element of the descriptor is the new name; (typedef int) and (typedef int *)
are errors. typedef takes no attributes.
tested in
test/c/macro.cicili
Cicili's macros are Common Lisp macros that run at specify time and produce Cicili forms. They are why the functional layer can exist at all.
(DEFMACRO swap (a b)
(LET ((tmp (GENSYM "tmp")))
`(let ((auto ,tmp . ,a))
(set ,a ,b)
(set ,b ,tmp))))
- A macro whose expansion starts with
$$$splices all its forms into the enclosing body instead of producing one form.`($$$ )expands to nothing at all — that is how thesyslog!/debug!/warn!/info!logging macros vanish below their debug level. macroletdefines macros for one body:
(macrolet ((twice (x) `(* ,x 2)))
(printf "%d\n" (twice 21)))
printf ("%d\n", (21 * 2));genericdefines a macro parameterised by type, and<>joins name parts:
(generic decl-crate (a)
(struct (<> crate a)
(member a value)))
(decl-crate int) ; -> (struct crate_int (member int value))
typedef struct crate_int {
int value;
} crate_int;(<> crate int) is the symbol crate_int, which is the C name too. Writing crate_int by hand
is identical.
- Macro files are loaded with
import;--macrosprints every macro a file defines, and--macroexpandprints each expansion as it happens.
sbcl --script {--dynamic-space-size=4096MB}? /path/to/cicili.lisp {arg}* {/path/to/file.cicili}+
| flag | effect |
|---|---|
--debug |
print details of specifying, resolving and compiling |
--verbose |
add -v to the C compiler / libtool commands — useful when linking many libraries |
--macros |
print all macros defined in a macro file when it is imported |
--macroexpand |
print every macro use and its expansion |
--only-link |
do not compile any target, only link |
--separate |
write each pass but the last to its own .run#.c file; the last still writes the real target |
--dump |
print the output of the C compiler's dump command |
--info / --warn / --debug / --syslog |
raise the level at which the info! / warn! / debug! / syslog! macros expand to real code |
--no-debug |
silence all of them |
{$CWD} (working directory) and {$CCL} (cicili installation directory) are available in
every target's :compile and :link arguments.
Setting :cpp #t in a target's features selects the C++ compiler and linker configured in
config.lisp, and enables:
&as a parameter modifier, for pass by reference.- Default values for struct members.
funcinside a struct, defining a member function. Call it with$:(($ emp Sign) aDoc).$$for namespace resolution:($$ std vector).t<>for templates:(t<> initializer_list int).using, with or withoutnamespace:(using std string),(using namespace std).extern-cto build a C-callable library from C++, so Cicili can use it:(extern-c (func identity ((int id)) (out int) (return id))).
Under :cpp #t, :std #t emits <string> and <iostream> instead of the C headers.
- test/c — one runnable file per clause family, all green. Start here:
- aggregate.cicili — structs, unions, enums, designated initializers
- control.cicili — every control structure in one file
- variable.cicili — every variable and initializer form
- function.cicili — function pointers, multi-value returns, lambda, closures
- memory.cicili — pointers,
alloc,defer - shared.cicili — a
headertarget included by asource
- test/std — the standard library in use, one runnable file per type:
- array.cicili —
array+maybe, opened withmatch/matchn - cell.cicili — owned heap values:
let_cell,take_cell - rc.cicili — shared ownership:
clone_rcand the count - vector.cicili —
push/appendand amortised growth - defer.cicili — the
deferattribute anddefer*together - thread.cicili —
go/join/detach/cancel/exit-self
- array.cicili —
- doc/test.md — how to run the suite, and the clauses with known gaps
- builtins.cicili — the macro layer, and the best source of idiomatic Cicili
- lib/std/c/README.md — C standard library and POSIX declarations
- doc/FUNCTIONAL.md — the functional layer: ADTs, pattern matching, Functors, Monads
Cicili is the bridge between vision and execution — where ideas transform into structured reality, and code bends to your creativity, unlocking limitless potential in software engineering. 🚀