#+title: csh #+author: cashmere #+property: header-args:c3 :mkdirp yes :comments no * Overview A C3 playground. =c3c= compiles source files directly, with no =project.json=. =just= drives the builds, =shell.nix= pins the toolchain through =npins=. The code blocks in this file tangle to =src/= with =org-babel-tangle=. * Quick start #+begin_src sh nix-shell # c3c, just, valgrind, pinned via npins just run # tangle, then compile and run src/main.c3 #+end_src * Layout #+begin_example . ├── README.org # source: prose and c3 blocks ├── shell.nix # toolchain: c3c, just, valgrind, pinned via npins ├── npins/ # nixpkgs pin (same revision as the nixos config) ├── .justfile # tangle, build, run, sanitizer targets ├── src/ # tangled c3 (generated, gitignored) └── build/ # output binaries (gitignored) #+end_example * Targets | Recipe | What it does | |----------------+----------------------------------------------------------| | just tangle | regenerate src/*.c3 from this README | | just run | tangle, then compile and run src/main.c3 | | just build | safe debug build (-O0 -g, bounds, null, overflow checks) | | just release | -O5, all checks off, no backtrace | | just asan | run under AddressSanitizer (use-after-free, leaks, OOB) | | just tsan | run under ThreadSanitizer (data races) | | just example X | compile and run a named example, e.g. just example tcp | | just valgrind | build, then run under valgrind | | just clean | remove build/ and the generated src/ | * The program =src/main.c3= covers iteration and error handling. Module and imports: #+begin_src c3 :tangle src/main.c3 module csh; import std::io; #+end_src ** foreach and ranges =foreach= gives the index first, the value second. =&= binds the value by reference, =foreach_r= iterates backwards. There is no numeric range loop, so a macro with a trailing =@body= block adds one: #+begin_src c3 :tangle src/main.c3 macro @range(usz start, usz end; @body(i)) { for (usz i = start; i < end; i++) @body(i); } macro @step(usz start, usz end, usz by; @body(i)) { for (usz i = start; i < end; i += by) @body(i); } #+end_src Packaged as a =.c3l= library, helpers like these carry across projects. ** Errors Each function returns an Optional (=T?=) and propagates failure with =!=. A single handler at the top sinks it. =~= turns a fault into an Optional, =??= supplies a default. #+begin_src c3 :tangle src/main.c3 faultdef BAD_PORT; fn int? parse_port(String s) { int port = s.to_int()!; if (port < 1 || port > 65535) return BAD_PORT~; return port; } #+end_src =tour= propagates, it never inspects an error itself: #+begin_src c3 :tangle src/main.c3 fn void? tour() { String[] protocols = { "irc", "ssh", "tcp" }; foreach (i, p : protocols) io::printfn("%d: %s", i, p); @range(0, 3; usz n) io::printfn("range %d", n); @step(0, 10, 2; usz n) io::printfn("step %d", n); int port = parse_port("6667")!; io::printfn("port %d ok", port); } #+end_src =main= is the single sink. Replace =6667= with =99999= or =nope= to take the failure path: #+begin_src c3 :tangle src/main.c3 fn int main() { if (catch err = tour()) { io::eprintfn("error: %s", err); return 1; } return 0; } #+end_src * Iterating your own types =foreach= works on any type implementing =@operator(len)= and =@operator([])=. Add =@operator(&[])= for =foreach (&x : ...)=. #+begin_src c3 :tangle src/iter.c3 module csh::iter; import std::io; struct Ring { int[4] data; } fn usz Ring.len(&self) @operator(len) => 4; fn int Ring.get(&self, usz i) @operator([]) => self.data[i]; fn void main() { Ring r = { { 10, 20, 30, 40 } }; foreach (i, v : r) io::printfn("ring[%d] = %d", i, v); int[3] xs = { 1, 2, 3 }; foreach (&x : xs) *x *= 10; foreach_r (x : xs) io::printfn("rev %d", x); } #+end_src * Collections Generic, allocator explicit, freed with =defer=. Pass =mem= for the heap or =tmem= for the temp arena. #+begin_src c3 :tangle src/store.c3 module csh::store; import std::io; import std::collections::list; import std::collections::map; fn void main() { List{String} names; names.init(mem); defer names.free(); names.push("cashmere"); names.push("tsoding"); foreach (n : names) io::printfn("name: %s", n); HashMap{String, int} ages; ages.init(mem); defer ages.free(); ages["cashmere"] = 27; io::printfn("age: %d", ages["cashmere"]!!); } #+end_src * Crypto and random =std::math::random= covers PRNGs, =std::hash::sha256= covers digests. Both ship in the standard library. #+begin_src c3 :tangle src/digest.c3 module csh::digest; import std::io; import std::hash::sha256; import std::math::random; fn void main() { int roll = random::rand(6) + 1; io::printfn("dice: %d", roll); char[32] sum = sha256::hash("cashmere"); io::print("sha256(cashmere) = "); foreach (b : sum) io::printf("%02x", b); io::printn(""); } #+end_src * Networking A one-shot TCP echo server on =std::net::tcp=. Client and server both live in the standard library. Run =just example tcp=, then talk to it with =nc 127.0.0.1 6667=. This is the skeleton for an IRC or telnet bot. #+begin_src c3 :tangle src/tcp.c3 module csh::echo; import std::io; import std::net::tcp; fn void? serve() { TcpServerSocket server = tcp::listen("127.0.0.1", 6667, 10)!; io::printn("listening on 127.0.0.1:6667"); TcpSocket sock = tcp::accept(&server)!; char[256] buf; usz n = sock.read(&buf)!; sock.write(buf[:n])!; sock.close()!; } fn int main() { if (catch err = serve()) { io::eprintfn("error: %s", err); return 1; } return 0; } #+end_src * Safety C3 does not check memory safety at compile time. Bugs are caught at runtime through sanitizers, and prevented through convention, the way curl stays correct: strict conventions and CI that runs every sanitizer. The sanitizers are LLVM backed and one flag away: #+begin_src sh c3c compile-run src/main.c3 --sanitize=address # use-after-free, double-free, OOB, leaks c3c compile-run src/main.c3 --sanitize=thread # data races c3c compile-run src/main.c3 --sanitize=memory # reads of uninitialised memory #+end_src AddressSanitizer includes LeakSanitizer, so =just asan= covers both use-after-free and leaks. =--safe=yes= (the default at =-O0= and =-O1=) adds bounds, null, overflow and contract checks. Develop in safe mode. Gate CI on =just asan= and =just tsan=. Keep =tmem= and =@pool= allocations inside their scope, and pair every =mem= allocation with =defer free()=. * Standard library C3 ships these: | Need | Module | |-------------+-----------------------------------------------------------------| | random | std::math::random (PCG, Xoshiro), std::crypto::entropy (CSPRNG) | | crypto | std::crypto (AES, ChaCha20, Ed25519, Argon2, Keccak) | | hashing | std::hash (sha256, sha512, blake3, hmac, siphash, crc32) | | tcp/udp | std::net::tcp, std::net::udp (client and server, blocking) | | json/base64 | std::encoding | | collections | std::collections (list, hashmap, deque, channels) | | threads | std::threads (pools and channels) | No HTTP and no TLS ship in std. For those, bind a C library. * C interop C3 is ABI compatible with C. Bind a library with =extern fn= declarations and a =-l= link flag. #+begin_src c3 extern fn void puts(char*); extern fn void init_window(int w, int h, char* t) @extern("InitWindow"); #+end_src #+begin_src sh c3c compile-run src/main.c3 -l raylib -l m -l dl -l pthread -l GL -l X11 #+end_src * Notes Targets =c3c= 0.8.x. C3 is pre-1.0, so fault and Optional syntax shifts between releases. If a block stops compiling, check it against the docs for the pinned version.