Skip to content
Back to skills

cicili

ASecurity

Use when writing, reading, debugging, reviewing, or porting Cicili code (C/C++ in S-expressions), including its clauses, macros, targets, type descriptors, ownership, standard library, and C/C++ bindings. Start here and follow the linked reference docs for exact emitted C.

  • 50 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 7, 2026
developmentpythongoc++sqlexpressdebugginggitapidocumentation

Works with

  • api

Security analysis

A100/100

Pro scans all 19 files and shows the line behind each finding

Scanned November 7, 2026

npx -y skills add saman-pasha/cicili --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of cicili?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for cicili
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/saman-pasha-cicili/badge)](https://www.skillsdirectory.com/skills/saman-pasha-cicili)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: cicili
description: Use when writing, reading, debugging, reviewing, or porting Cicili code (C/C++ in S-expressions), including its clauses, macros, targets, type descriptors, ownership, standard library, and C/C++ bindings. Start here and follow the linked reference docs for exact emitted C.
---

# SKILL — working in Cicili

**This file is for every agent that writes, reads, debugs, or reviews Cicili code.**
Read it before changing a `.cicili` file. It is a map of the language and its
build, not a replacement for the two reference documents:

- [`doc/DOC-C.md`](doc/DOC-C.md) — the complete C core, clause by clause, with
  the generated C.
- [`doc/DOC-CPP.md`](doc/DOC-CPP.md) — the C++ additions; read DOC-C first.
- [`lib/README.md`](lib/README.md) — the standard library, the C/POSIX
  declarations, and the binding index.
- [`CLAUDE.md`](CLAUDE.md) — the repository's build and change rules.

When this summary and the docs disagree, **the docs are the reference** and the
generated C is the ground truth. Version at the time of writing: **3.2.0**.

---

## 1. The one-sentence model

Cicili is **C in S-expressions**: a form is a list whose head decides its
meaning. A known clause head (`func`, `let`, `struct`, `if`, …) emits C; a head
that is not a clause is an ordinary function call. Common Lisp macros run at
compile time and emit more Cicili forms. The result is compiled by the
configured C/C++ compiler.

```
your.cicili → reader → forms → specify (types) → macro expansion
            → generated C → C compiler → object/link
```

There is **no runtime and no garbage collector**. A Cicili program is a C
program; ownership, `defer`, and standard-library cells are compile-time and
C-level mechanisms.

---

## 2. Invocation and targets

A `.cicili` file contains one or more **targets**:

```cicili
(source "hello.c" (make :std #t :compile #t :link #t)
  (main
    (printf "hello\n")
    (return 0)))

(header "thing.h" ()
  (guard THING_H
    (struct Thing (member int value))))
```

- `source PATH FEATURES BODY…` writes, compiles, and optionally links `PATH`.
- `header PATH FEATURES BODY…` writes a header; it is never compiled by the
  target itself.
- `(make :std #t :compile #t :link #t)` is the usual feature macro. A raw
  feature list may also be written: `(:std #t :compile #t :link #t)`.
- `:std #t` emits the standard C includes; with `:cpp #t` it emits `<string>`
  and `<iostream>` instead.
- `:cpp #t` selects the C++ compiler and enables the C++ clauses.
- `:compile` defaults to `-c TARGET`; a custom value must contain `-c` or
  `--compile`. `:link` has no default and runs only after a successful compile
  listed before it.
- `{$CWD}` and `{$CCL}` are substituted in `:compile`/`:link` arguments.
- `main` and `main*` are macros for the two usual C `main` shapes.

Build:

```sh
sbcl --script cicili.lisp FILE.cicili          # debug (-g -O0)
sbcl --script cicili.lisp --release FILE.cicili # release (-O3)
sbcl --script cicili.lisp --version            # bare version on stdout
```

Useful flags: `--debug`, `--macroexpand`, `--macros`, `--separate`,
`--only-link`, `--syslog`, `--info`/`--warn`/`--debug`/`--no-debug`.

After changing the compiler or standard library, run the whole suite:

```sh
sh test/run.sh          # must end red: 0
```

---

## 3. The reading rules that prevent most mistakes

1. **A type is one token.** Write `size_t`, `ubyte_t`, `Employee`, or a typedef.
   `(cast unsigned char x)` is invalid; use a typedef or mask.
2. **Pointer casts are parenthesised:** `(cast (void **) p)`,
   `(cast (ubyte_t *) p)`. A return type is `(out T *)`, not `(out (T *))`.
3. **Initializer markers:** `. value` is an atom, `.' { … }` is a brace list,
   `.#' ( … )` is an expression, `.'(lambda …)` is a lambda value.
4. **Arrays at most two dimensions.** `[]`, `[N]`, `[N][M]`; no more.
5. **`let` declares locals; `block` does not.** A declaration inside `block`
   is not a binding.
6. **`nth` is index-first:** `(nth i array)` is `array[i]`.
7. **Member access:** `($ obj member)` is `obj.member` and chains;
   `(-> ptr member)` is `ptr->member`; `(=> obj member args…)` calls a
   function stored in a member.
8. **Bitwise operators are words:** `bitand`, `bitor`, `xor`; `break` and
   `continue` are bare symbols.
9. **`for` takes a binding list, a test, a parenthesised list of steps:**
   `(for ((int i . 0)) (< i n) ((++ i)) body)`.
10. **`if` branches are exactly one form each; use `block` for several.**
    `cond`, `while`, `for`, `do`, and `switch` bodies may hold several.
11. **`do`'s last form is the condition**, not a body statement.
12. **`switch` does not insert `break`.** Cases fall through exactly as in C.
13. **Strings are raw C strings.** `"\n"` reaches C as backslash-n. A literal
    may not contain a real newline or end in a backslash.
14. **A float literal is a C double, all its digits.** A macro computing a
    float must use a double (`sqrt 2.0`, not `sqrt 2`).
15. **`#\Space`, `#\Newline`, `#\Null`, …** are character literals; an
    arbitrary character is written as itself (`#\A`).
16. **A macro must emit one form** unless it expands with `($$$ …)` to splice
    several. A macro is read with case preserved; Lisp forms in macro files
    are conventionally uppercase.
17. **`#define` is invisible to Cicili.** Constants from headers are declared
    as `(typedef int NAME)`; a number you need in a form should be written out
    or exposed through a binding. `(@define (code "NAME C-TEXT"))` is the
    escape for a C typedef/constant that Cicili cannot spell.
18. **`nil` is C `NULL`.** Prefer `nil` in Cicili forms.

### The type descriptor

Every type position uses one positional grammar:

```
[const] TYPE [modifier] [const|restrict] [name] [array]
modifier:  *  **  ***  &  move  ref
array:     []  [N]  [N][M]
```

- `move` emits nothing; it is an ownership marker.
- `ref` emits `* restrict`.
- An omitted name is an unnamed parameter.
- Any other type symbol passes through to C, so `size_t`, `FILE`, and your
  typedefs work.

---

## 4. The complete C core clause index

A **clause** is a list whose head is in Cicili's clause table. A list with any
other head is a C function call. Some familiar names are macros built on the
clauses (`main`, `main*`, `letin`, the logging macros) rather than emitter
clauses; `--macroexpand` is the authority when the distinction matters.

This is the full C-core index from `doc/DOC-C.md`. Each link there shows the
emitted C and the runnable test.

### Program structure

| clause | meaning |
|---|---|
| `source` | a target that produces/compiles/links a `.c` file |
| `header` | a target that produces a `.h`; never compiled |
| `import` | load a macro file at read time |
| `include` | emit `#include` |
| `guard` | `#ifndef` / `#define` / `#endif` wrapper |
| `@…` | a preprocessor form: `@define`, `@ifdef`, `@else`, `@endif`, … |
| `code` | raw C text, passed through untouched |
| `DEFMACRO` | define a Common Lisp/Cicili macro |
| `macrolet` | define local macros for one body |
| `generic` / `<>` | type-parameterised code and name joining |
| `$$$` | splice several macro expansion forms into the caller |

### Declarations

| clause | meaning |
|---|---|
| `var` | file-scope/global variable |
| `let` / `letn` | scoped variables; `letn` is an expression with a value |
| `func` | a function, or a function pointer in type position |
| `out` | return type; first form after the parameter list |
| `struct` | structure |
| `union` | union |
| `enum` | enumeration |
| `member` | a struct/union field |
| `declare` | name(s) of an anonymous nested struct/union |
| `typedef` | type alias |

Function attributes go before the `func`, each in its own list: `(decl)`,
`(static)`, `(extern)`, `(inline)`, `(weak)`, `(cold)`, `(auto)`,
`(volatile)`, `(resolve #f)`. Attributes apply to the next clause only.

### Statements

| clause | meaning |
|---|---|
| `if` | condition, then-form, optional else-form (one form each) |
| `cond` | `if`/`else if` chain; each body may hold several forms |
| `switch` / `case` / `default` | C switch; no implicit breaks |
| `while` / `do` / `for` | loops; `do`'s last form is its condition |
| `break` / `continue` | bare symbols |
| `block` | `{ … }`, no value |
| `progn` | `({ … })`, last form is the value |
| `set` | assignment, one or many pairs in order |
| `return` | function return |

### Expressions

| clause | C |
|---|---|
| `nth` | `base[index]` — **index first** |
| `?` | `cond ? a : b` (four elements) |
| `cast` | `(type)expr` |
| `sizeof` / `typeof` | `sizeof(…)` / `typeof(…)` |
| `aof` / `cof` | `&x` / `*x` |
| `$` | value member access; chains |
| `->` | pointer member access |
| `=>` | call a function stored in a member |
| `lambda` / `lambda*` | lift a function to file scope |
| `'(closure)` / `'(closure*)` | GCC nested-function closure; **clang rejects** |
| `(closure)` / `def-closure` | portable capture-by-value closure; `exec-closure` calls it |
| `alloc` | `malloc`/`calloc` with the variable's cast and automatic free |

### Operators

Arithmetic `+ - * / %` (n-ary, fully parenthesised), relational
`== != > < >= <=`, logical `and or not`, bitwise `<< >> ~ bitand bitor xor ^`,
assignment `set = += -= *= /= %= <<= >>=`, prefix `++ --`, and postfix `1+ 1-`.
`sizeof` and `typeof` are expressions. Statements are not expressions.

---

## 5. Functions, structs, memory and cleanup

### Functions

```cicili
(static) (func add ((int a) (int b)) (out int)
  (return (+ a b)))
```

- `out` must be the first form after the parameters. No `out` means `void`,
  except `main`, which is `int`.
- Use `(decl)` for a prototype; a `(func …)` with a body is a definition.
- Function pointer in type position:

  ```cicili
  (var func handler ((int sig)) . my_handler)
  (member func resolve ((char * prob)) (out char *))
  ```
- Unnamed parameters: `(int)`; variadic: `($$$)`.
- A lambda is `'(lambda ((int x)) (out int) …)`; a named lambda is
  `'(lambda* NAME …)`. It cannot capture locals. Use the portable `closure`
  macro (on clang) or `def-closure` for a closure that outlives its scope.
- Multiple return values use an inline struct in `out` and a `'{}` return;
  the function must be `(static)` in a source or declared in a header.

### `let`, `letn`, `block`, `progn`, `defer`

- `let` binds locals in a scope. Attributes appear as bare lists in the
  binding list and apply to the following binding: `(static)`, `(register)`,
  `(volatile)`, `(thread-local)`, `(atomic)`, `(defer …)`.
- `letn` is the same scope but its last form is the resulting value.
- `block` is a statement block; `progn` is a GCC/Clang statement expression
  with a value.
- `defer` is an attribute on the next binding. It forms a C
  `__attribute__((cleanup))` destructor. Three spellings:
  `(defer #t)` (free), `(defer () func_name)`, or `(defer () body…)`.
  The destructor receives a **pointer to the variable**; the generated form
  rebinds the variable's name/type for you.
- An `alloc`-initialized variable gets an automatic free unless you provide a
  `defer`. A custom `defer` suppresses the automatic free.
- `defer*` snapshots named values and runs a block at scope exit; it uses an
  inline struct, so the function must be static or declared in a header.

### Structs, unions, enums

- `struct` members use `(member TYPE NAME)`; `union` is analogous.
- Anonymous nested structs/unions need `(declare NAME)`.
- `(decl) (struct FILE)` emits only the forward typedef.
- `(non-copy)` on a struct makes values move-only (no copy).
- Enums are `(enum NAME (CONST . value) (NEXT) …)`; `(CONST)` continues the
  sequence. `(CONST value)` is invalid.
- `typedef` takes the full type descriptor, with the new name last.

---

## 6. Macros, generics, imports and packages

Cicili's macro layer is Common Lisp. A macro runs at specify time and returns
Cicili forms; the normal type checker then checks the result.

```cicili
(DEFMACRO swap (a b)
  (LET ((tmp (GENSYM "tmp")))
    `(let ((auto ,tmp . ,a))
       (set ,a ,b)
       (set ,b ,tmp))))
```

- `($$$ …)` in an expansion splices several forms into the enclosing body.
- `macrolet` defines macros for one body only.
- `generic` defines a type-parameterised macro; instantiate it explicitly:

  ```cicili
  (generic decl-crate (a)
    (struct (<> crate a) (member a value)))
  (decl-crate int)          ; emits crate_int
  ```

  `(<> crate int)` is the symbol `crate_int`, and the C name. The convention in
  `lib/std` is a `decl-`/`impl-` pair per library; a target calls both.
- `import "path"` loads a macro file. A path starting with `.` is relative to
  the importing file, `/` is absolute, otherwise it is relative to the Cicili
  installation. A second argument is a prefix: `(import "lib/net/curl.cicili" :cu)`
  gives `(cu.get …)`. A third argument is passed to the file's init function.
- Macro libraries use their own `DEFPACKAGE`/`IN-PACKAGE` and
  `:IMPORT-FROM :COMMON-LISP-USER "import" "generic" "cicili"`.
- Inside a macro library, compare symbols by **name** with `CICILI:KEY-EQ`,
  not `EQL`; expand caller forms with `expand-form<`, not `CL:MACROEXPAND`.
- Lower-case macro names avoid shadowing Common Lisp names.

A macro file **declares** through `init-macro` and emits real code through a
separate `impl-` macro. An `init-macro` is for declarations and types; it emits
nothing.

---

## 7. `lib/` is the declaration source — never redeclare it

**Rule zero: before writing a `(decl) (func …)`, `(struct …)`, `(typedef …)`,
`@define`, or a `(code …)` for a C/POSIX declaration, inspect `lib/`.**

The library files are the single source of truth for what Cicili already
declares. Do not copy their declarations into a target, a module, a project,
or this skill. If a declaration is missing, add it to `lib/` as a binding or
to `lib/std/c/` as the corresponding declaration; do not paste a second copy
into the consumer.

Useful checks from the Cicili checkout:

```sh
sed -n '1,180p' lib/README.md
grep -R "func NAME\|struct NAME\|typedef NAME" lib
```

### 7.1 What the prelude already loads

`cicili.lisp` imports `lib/std/prelude.cicili` before reading a target. No
import is needed for:

| library | what it provides |
|---|---|
| [`std/prelude.cicili`](lib/std/prelude.cicili) | the load order and the automatically available layer |
| [`std/array.cicili`](lib/std/array.cicili) | fixed-length indexable memory |
| [`std/vector.cicili`](lib/std/vector.cicili) | a growable array that owns its headroom |
| [`std/string.cicili`](lib/std/string.cicili) | `(<> vector char)` under the name `string` |
| [`std/cell.cicili`](lib/std/cell.cicili) | one-owner heap value |
| [`std/rc.cicili`](lib/std/rc.cicili) | reference-counted cell, including atomic `arc` |
| [`std/maybe.cicili`](lib/std/maybe.cicili) | a value that may be absent |
| [`std/either.cicili`](lib/std/either.cicili) | one of two answers |
| [`std/btree.cicili`](lib/std/btree.cicili) | ordered map with logarithmic operations |

It also imports [`std/c/c.cicili`](lib/std/c/c.cicili) and
[`std/c/posix.cicili`](lib/std/c/posix.cicili), which carry the C17 and POSIX
declarations as typed Cicili declarations:

- C17: [`assert`](lib/std/c/assert.cicili), [`ctype`](lib/std/c/ctype.cicili),
  [`errno`](lib/std/c/errno.cicili), [`fenv`](lib/std/c/fenv.cicili),
  [`float`](lib/std/c/float.cicili), [`inttypes`](lib/std/c/inttypes.cicili),
  [`limits`](lib/std/c/limits.cicili), [`locale`](lib/std/c/locale.cicili),
  [`math`](lib/std/c/math.cicili), [`setjmp`](lib/std/c/setjmp.cicili),
  [`signal`](lib/std/c/signal.cicili), [`stdarg`](lib/std/c/stdarg.cicili),
  [`stdbool`](lib/std/c/stdbool.cicili), [`stddef`](lib/std/c/stddef.cicili),
  [`stdint`](lib/std/c/stdint.cicili), [`stdio`](lib/std/c/stdio.cicili),
  [`stdlib`](lib/std/c/stdlib.cicili), [`string`](lib/std/c/string.cicili),
  [`time`](lib/std/c/time.cicili), [`wchar`](lib/std/c/wchar.cicili),
  [`wctype`](lib/std/c/wctype.cicili).
- POSIX: [`dirent`](lib/std/c/posix/dirent.cicili),
  [`dlfcn`](lib/std/c/posix/dlfcn.cicili),
  [`fcntl`](lib/std/c/posix/fcntl.cicili),
  [`netdb`](lib/std/c/posix/netdb.cicili),
  [`poll`](lib/std/c/posix/poll.cicili),
  [`pthread`](lib/std/c/posix/pthread.cicili),
  [`socket`](lib/std/c/posix/socket.cicili),
  [`sys-mman`](lib/std/c/posix/sys-mman.cicili),
  [`sys-stat`](lib/std/c/posix/sys-stat.cicili),
  [`sys-time`](lib/std/c/posix/sys-time.cicili),
  [`sys-types`](lib/std/c/posix/sys-types.cicili),
  [`sys-wait`](lib/std/c/posix/sys-wait.cicili),
  [`unistd`](lib/std/c/posix/unistd.cicili).

The **structs, members, functions and constants** are in those files. Read them
there; do not copy a POSIX header into the target. The one usual missing piece
is a C typedef for a struct that has no typedef in the real header, e.g.
`(@define (code "pollfd struct pollfd"))`; it must use the exact name under
which std declares the members.

The algebraic layer is also preloaded:
[`std/haskell/data.cicili`](lib/std/haskell/data.cicili),
[`match.cicili`](lib/std/haskell/match.cicili),
[`class.cicili`](lib/std/haskell/class.cicili),
[`core.cicili`](lib/std/haskell/core.cicili),
[`haskell.cicili`](lib/std/haskell/haskell.cicili),
[`prelude.cicili`](lib/std/haskell/prelude.cicili),
[`maybe.cicili`](lib/std/haskell/maybe.cicili),
[`either.cicili`](lib/std/haskell/either.cicili),
[`bool.cicili`](lib/std/haskell/bool.cicili),
[`ordering.cicili`](lib/std/haskell/ordering.cicili),
[`tuple.cicili`](lib/std/haskell/tuple.cicili),
[`list.cicili`](lib/std/haskell/list.cicili),
[`ownlist.cicili`](lib/std/haskell/ownlist.cicili),
[`range.cicili`](lib/std/haskell/range.cicili),
[`persistent-btree.cicili`](lib/std/haskell/persistent-btree.cicili),
[`monoid.cicili`](lib/std/haskell/monoid.cicili),
[`functor.cicili`](lib/std/haskell/functor.cicili),
[`applicative.cicili`](lib/std/haskell/applicative.cicili),
[`monad.cicili`](lib/std/haskell/monad.cicili),
[`arc.cicili`](lib/std/haskell/arc.cicili).

### 7.2 Opt-in libraries and bindings

These are **not** loaded by the prelude. Import the file, include the real
header, and add the link flags. The import prefix is yours: with `:cu`,
`(cu.get …)` is the front-end form.

| import | needs | what it is |
|---|---|---|
| [`std/pthread/pthread.cicili`](lib/std/pthread/pthread.cicili) | `<pthread.h>` | pthread macros: `go`, `join`, `detach`, `cancel`, `exit-self`; the raw declarations are already in `std/c/posix` |
| [`db/sqlite.cicili`](lib/db/sqlite.cicili) | `<sqlite3.h>`, `-lsqlite3` | SQLite3 with `open`/`exec`/`run`/`query`/`close`; prepare/bind/step/finalize is one form |
| [`db/postgres.cicili`](lib/db/postgres.cicili) | `<libpq-fe.h>`, `-lpq` | libpq, same five-macro shape as SQLite |
| [`net/curl.cicili`](lib/net/curl.cicili) | `<curl/curl.h>`, `-lcurl` | libcurl easy interface; typed option words (`timeout`, `ca-info`, `pin`, `tls-version`, `verify-peer`, `tls-time`) |
| [`python/python.cicili`](lib/python/python.cicili) | `<Python.h>` first, `-export-dynamic -ldl` | CPython embedding API and numpy C API |
| [`python/cpython.cicili`](lib/python/cpython.cicili), [`python/numpy.cicili`](lib/python/numpy.cicili) | as above | the two halves of the Python binding, importable alone |
| [`python/module.cicili`](lib/python/module.cicili) | CPython | the other direction: a `.so` `import` loads, with functions written in Cicili |
| [`cpp/torch/torch.cicili`](lib/cpp/torch/torch.cicili) | `<torch/torch.h>`, `:cpp #t` | libtorch, plus the network DSL; its parts are [`tensor`](lib/cpp/torch/tensor.cicili), [`nn`](lib/cpp/torch/nn.cicili), [`optim`](lib/cpp/torch/optim.cicili), [`data`](lib/cpp/torch/data.cicili), [`dsl`](lib/cpp/torch/dsl.cicili) |
| [`cpp/memory.cicili`](lib/cpp/memory.cicili) | `<memory>`, `:cpp #t` | `std::shared_ptr` and `std::make_shared` |
| [`cpp/dl.cicili`](lib/cpp/dl.cicili) | `<dlfcn.h>`, `:cpp #t` | `dlopen`, `dlsym`, `dladdr`, `Dl_info` |
| [`cpp/zigurat/zigurat.cicili`](lib/cpp/zigurat/zigurat.cicili) | ZiguratIP `home/include`, `:cpp #t` | ZiguratIP behind one import; parts are [`stream`](lib/cpp/zigurat/stream.cicili), [`bigint`](lib/cpp/zigurat/bigint.cicili), [`sha`](lib/cpp/zigurat/sha.cicili), [`aes`](lib/cpp/zigurat/aes.cicili), [`der`](lib/cpp/zigurat/der.cicili), [`x509`](lib/cpp/zigurat/x509.cicili), [`tls`](lib/cpp/zigurat/tls.cicili) |
| [`cpp/opencv/opencv.cicili`](lib/cpp/opencv/opencv.cicili) | `<opencv2/*.hpp>`, `:cpp #t`, pkg-config `opencv4` | OpenCV 4 declarations: `cv::Mat`, containers, imgcodecs, imgproc, drawing, dnn, objdetect, features2d, photo, video, videoio, calib3d, constants |
| [`parsi/parsi.cicili`](lib/parsi/parsi.cicili) | ZiguratIP | macros for Parsi's seven objects; the import takes a third argument, the domain: `(import "lib/parsi/parsi.cicili" :parsi "demo")` |

The **public surface and modes** of each binding are in its file and in
[`lib/README.md`](lib/README.md); read the binding before using it. This skill
does not restate those declarations.

### 7.3 If something is missing

- Add the declaration to `lib/std/c` or a new binding under `lib/`, with the
  real header included by the target and the correct link flags.
- A binding follows a `decl-`/`impl-` shape:
  1. its own `DEFPACKAGE`;
  2. `(DEFMACRO init-macro () \`($$$ …))` with the declarations and
     `(typedef int NAME)` for constants, emitting no code;
  3. a front-end macro for a C API with a dangerous or repetitive shape;
  4. a separate `impl-` macro for code that must emit.
- Do not copy a declaration from a consumer into `lib/`, and do not copy
  `lib/` declarations into a consumer. That is how the two drift.
- Use `(code …)` only for a typedef/constant the real C header exposes but
  Cicili cannot spell, a genuinely unexposed declaration, or a compiler
  builtin. It is not a place to re-declare a library whose declarations
  already exist in `lib/`.

---

## 8. C++

Turn on with `:cpp #t`. The C core still applies.

Additional clauses:

| clause | meaning |
|---|---|
| `a::b::c` | qualified C++ name; rewritten internally to `($$ a b c)` |
| `&` / `&&` | lvalue/rvalue reference type modifier |
| `t<>` | template-id: `(t<> std::vector int)` |
| `template` | template declaration |
| `inherits` | public base classes |
| `method` | member function; `this` is implicit |
| `ctor` / `dtor` / `init` | constructor, destructor, member initializer list |
| `module` / `using` | namespace and `using` |
| `extern-c` | `extern "C" { … }` for C-callable linkage |
| `letin*` | binding whose lifetime is C++'s (destructor at scope exit) |
| `new*` / `delete*` / `delete[]*` | C++ allocation |
| `static-cast` / `dynamic-cast` / `const-cast` / `reinterpret-cast` | C++ casts |
| `try` / `catch` / `throw*` | C++ exceptions |

Method attributes: `(virtual)`, `(explicit)`, `(const)`, `(noexcept)`,
`(override)` — each applies to the next `method`.

Rules:

- C++ has no `class` here: use `struct`; members and bases are public.
- Use `letin*` for C++ objects, not `letin`.
- One signature per name; no `operator` clause (write the function form).
- A `code` result has no inferred type; prefer real clauses.
- For a loadable C++ module, the SDK's C prototypes must be declared with C
  linkage before the Cicili declarations. The cicolog SDK provides
  `coco-sdk`; never import `lib/module.cicili` and `lib/sdk.cicili` into the
  same target.
- `extern-c` is for building a C-callable library from C++; it is not a
  substitute for the SDK's raw C declarations.

---

## 9. `(code …)` — the fire escape

`code` emits its text verbatim. It is valid as an expression, a statement, and
in type position. It is **not type-checked** and has no inferred type.

Use it only for:

- a `@define` C typedef/constant that Cicili cannot spell
  (`(@define (code "pollfd struct pollfd"))`);
- a declaration a C library genuinely does not expose (`glob`, `realpath`,
  a platform-only header);
- a compiler builtin (`__builtin_add_overflow`, `__builtin_unreachable`);
- a deliberate C++ type/alias with no Cicili spelling (rare; document why).

Do **not** use it for ordinary calls, arithmetic, control flow, struct access,
or function bodies that Cicili can express. A `code` block is C that the
front end, ownership checker, and type inference cannot see.

When a C struct is already declared by `lib/std/c/posix`, do not bind it again.
The missing piece is usually only the C typedef; add one
`@define (code "name struct real_name")` and use the members Cicili already
knows.

---

## 10. Errors, debugging, and the build loop

- The line **under** `Unhandled …` names the cause; the rest is a Lisp
  backtrace. For `The value 0 is not of type SEQUENCE`, read frames 1–7: the
  offending form is printed in full, and it is usually a malformed `for`.
- `run err 1 > …` lines are the C compiler's diagnostics. They are the real
  error when Cicili got through.
- Cicili treats unrecognised compiler chatter as **fatal**. Put a new compiler
  warning's suppression in the target's own `:compile` list; do not paper over
  it.
- `--separate` keeps each pass's C; `--macroexpand` prints expansions;
  `--macros` lists macro definitions; `--debug` prints target resolution.
- Read the generated `.c` when learning a clause. It is formatted and is the
  ground truth.
- Run `sh test/run.sh` after a compiler/library change; the required final line
  is `red: 0`.
- A change to Cicili is an input to downstream repositories (cocolog and
  others). The burden of proof is the full suite plus a rebuild of the
  downstream tree on the new compiler.
- Version bumps: patch by default; minor for a new clause/library/macro a
  target can reach; major for a source incompatibility or changed emitted C.
  A documentation-only change does not bump.

---

## 11. Agent pre-flight checklist

Before writing or editing:

- [ ] I know whether this is a C or C++ target (`:cpp #t`).
- [ ] I know the target(s): `source`/`header`, output path, `:compile`/`:link`.
- [ ] I know every type I use is a **single token** or a typedef.
- [ ] I have checked `lib/std` and `lib/std/c/posix` before declaring POSIX
      structs or functions.
- [ ] I am adding a clause, not pasting C into `(code …)`.
- [ ] If I need macros, they emit one form (or use `($$$)`); names are lower
      case; package helpers use `KEY-EQ`.
- [ ] If I bind a C library: real header in the target, import with a prefix,
      declarations in `init-macro`, code emission in `impl-`.
- [ ] If I bind C++: `:cpp #t`, one signature per name, `letin*`, no operator
      overloading, C linkage where the SDK requires it.
- [ ] I know the ownership/lifetime: `let`, `letn`, `defer`, `alloc`, `cell`,
      `rc`, `letin*`.
- [ ] I have a runnable test (or the existing suite covers the clause) and will
      read the generated C.

After editing:

- [ ] `sbcl --script cicili.lisp --release FILE.cicili` succeeds.
- [ ] Generated C is reviewed where the change is non-obvious.
- [ ] `sh test/run.sh` ends `red: 0` if the compiler or std library changed.
- [ ] The downstream repository that consumes this change still builds.

---

## 12. Where to read next

| need | file |
|---|---|
| every C clause, with emitted C | [`doc/DOC-C.md`](doc/DOC-C.md) |
| every C++ addition | [`doc/DOC-CPP.md`](doc/DOC-CPP.md) |
| language design and agent rationale | [`README.md`](README.md) |
| current repository rules and failure notes | [`CLAUDE.md`](CLAUDE.md) |
| standard library and bindings | [`lib/README.md`](lib/README.md) |
| C/POSIX declarations | [`doc/lib-std-c.md`](doc/lib-std-c.md) |
| runnable clause examples | [`test/c/`](test/c) |
| runnable standard-library examples | [`test/std/`](test/std) |
| functional layer | [`doc/FUNCTIONAL.md`](doc/FUNCTIONAL.md) |

If a form is not in this file and not in DOC-C/DOC-CPP, do not guess: read the
generated C of the nearest existing use, or ask the compiler with
`--macroexpand`/`--separate`.

Files in this skill

  • CLAUDE.md11.5 KB
  • SKILL.md27.3 KB
  • art/banner.png78.2 KB
  • art/banner.svg5.1 KB
  • authority.lisp60.6 KB
  • backend.lisp56.6 KB
  • benchmark/README.md5.7 KB
  • benchmark/bench.py18.5 KB
  • benchmark/concurrent-btree.cicili13.6 KB
  • benchmark/release-only.cicili1.7 KB
  • benchmark/std-btree-bench.cicili4.9 KB
  • benchmark/std-vector-bench.cicili17.1 KB
  • benchmark/word-count-bench.cicili5.8 KB
  • benchmark/word_count.hs2.1 KB
  • body.lisp24.2 KB
  • builtins.cicili35.1 KB
  • cicili.asd1.2 KB
  • cicili.lisp4.2 KB
  • compiler.lisp31.6 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.