Compare commits

..

1 Commits

Author SHA1 Message Date
Aryadev Chavali
140f00d76d LICENSE: Unlicense -> GPL Version 2 2026-02-04 22:37:23 +00:00
35 changed files with 591 additions and 2613 deletions

View File

@@ -1,8 +1,6 @@
;;; Directory Local Variables -*- no-byte-compile: t -*- ;;; Directory Local Variables -*- no-byte-compile: t -*-
;;; For more information see (info "(emacs) Directory Variables") ;;; For more information see (info "(emacs) Directory Variables")
((nil . ((compile-command . "make MODE=debug test examples") ((nil . ((compile-command . "make MODE=debug")
(+license/license-choice . "GNU General Public License Version 2"))) (+license/license-choice . "GNU General Public License Version 2")))
(c-mode . ((mode . clang-format))) (c-mode . ((mode . clang-format))))
("test" .
((nil . ((compile-command . "make MODE=full test"))))))

View File

@@ -5,22 +5,19 @@ OUT=$(DIST)/alisp.out
TEST=$(DIST)/test.out TEST=$(DIST)/test.out
LDFLAGS= LDFLAGS=
GFLAGS=-Wall -Wextra -Wswitch-enum -Wpedantic -Werror -std=c23 -I./include/ GFLAGS=-Wall -Wextra -Wpedantic -std=c23 -I./include/
DFLAGS=-ggdb -fsanitize=address -fsanitize=undefined DFLAGS=-ggdb -fsanitize=address -fsanitize=undefined
RFLAGS=-O3 RFLAGS=-O3
MODE=release MODE=release
ifeq ($(MODE), release) ifeq ($(MODE), release)
CFLAGS=$(GFLAGS) $(RFLAGS) CFLAGS=$(GFLAGS) $(RFLAGS)
else ifeq ($(MODE), debug) else
CFLAGS=$(GFLAGS) $(DFLAGS) -DVERBOSE_LOGS=1 CFLAGS=$(GFLAGS) $(DFLAGS)
else ifeq ($(MODE), full)
CFLAGS=$(GFLAGS) $(DFLAGS) -DVERBOSE_LOGS=2 -DTEST_VERBOSE=1
endif endif
# Units to compile # Units to compile
UNITS=src/sv.c src/vec.c src/string.c src/stream.c src/symtable.c src/lisp.c \ UNITS=src/sv.c src/vec.c src/stream.c src/symtable.c src/tag.c src/lisp.c
src/allocator.c src/sys.c src/reader.c
OBJECTS:=$(patsubst src/%.c, $(DIST)/%.o, $(UNITS)) OBJECTS:=$(patsubst src/%.c, $(DIST)/%.o, $(UNITS))
TEST_UNITS=test/main.c TEST_UNITS=test/main.c
@@ -64,9 +61,6 @@ run: $(OUT)
test: $(TEST) test: $(TEST)
./$^ ./$^
examples: $(OUT)
./$^ ./examples/hello-world.lisp
clean: clean:
rm -rf $(DIST) rm -rf $(DIST)

229
alisp.org
View File

@@ -3,17 +3,49 @@
#+date: 2025-08-20 #+date: 2025-08-20
#+filetags: :alisp: #+filetags: :alisp:
* Notes
** Overview
~alisp.h~ is a single header for the entire runtime. We'll also have a
compiled shared library ~alisp.so~ which one may link against to get
implementation. That's all that's necessary for one to write C code
that targets our Lisp machine.
We'll have a separate header + library for the compiler since that's
not strictly necessary for transpiled C code to consume. This will
transpile Lisp code into C, which uses the aforementioned ~alisp~
header and library to compile into a native executable.
** WIP How does transpiled code operate?
My current idea is: we're transpiling into C for the actual Lisp code.
User made functions can be transpiled into C functions, which we can
mangle names for. Macros... I don't know, maybe we could have two
function pointer tables so we know how to execute them?
Then, we'll have an associated "descriptor" file which describes the
functions we've transpiled. Bare minimum, this file has to have a
"symbol name" to C mangled function name dictionary. We can also add
other metadata as we need.
*** TODO Deliberate on whether we compile into a shared library or not
If we compile these C code objects into shared libraries, the
descriptor needs to concern itself with code locations. This might be
easier in a sense, since the code will already be compiled.
** WIP How do we call native code?
When we're calling a natively compiled function, we can use this
metadata mapping to call the C function. This native code will use
our Lisp runtime, same as any other code, so it should be pretty
seamless in that regard. But we'll need to set a calling convention
in order to make calling into this seamless from a runtime
perspective.
* Tasks * Tasks
** String views :strings: ** TODO Capitalise symbols (TBD) :optimisation:design:
[[file:include/alisp/sv.h::/// String Views]] Should we capitalise symbols? This way, we limit the symbol table's
*** DONE sv_substr possible options a bit (potentially we could design a better hashing
Takes an index and a size, returns a string view to that substring. algorithm?) and it would be kinda like an actual Lisp.
*** DONE sv_chop_left and sv_chop_right ** TODO Design Strings
Super obvious.
*** TODO Design Strings for the Lisp :api:
We have ~sv_t~ so our basic C API is done. We just need pluggable We have ~sv_t~ so our basic C API is done. We just need pluggable
functions to construct and deconstruct strings as lisps. functions to construct and deconstruct strings as lisps.
** Reader system :reader: ** WIP Reader system
We need to design a reader system. The big idea: given a "stream" of We need to design a reader system. The big idea: given a "stream" of
data, we can break out expressions from it. An expression could be data, we can break out expressions from it. An expression could be
either an atomic value or a container. either an atomic value or a container.
@@ -51,103 +83,46 @@ easier. We're not going to do anything more advanced than the API
i.e. no parsing. i.e. no parsing.
**** DONE Design the tagged union **** DONE Design the tagged union
**** DONE Design the API **** DONE Design the API
*** WIP Figure out the possible parse errors
*** DONE Design what a "parser function" would look like *** DONE Design what a "parser function" would look like
The general function is something like ~stream -> T | Err~. What The general function is something like ~stream -> T | Err~. What
other state do we need to encode? other state do we need to encode?
*** DONE Write a parser for integers *** WIP Write a parser for integers
*** DONE Write a parser for symbols *** TODO Write a parser for symbols
*** DONE Write a parser for lists *** TODO Write a parser for lists
*** DONE Write a parser for vectors *** TODO Write a parser for vectors
*** TODO Write a parser for strings *** TODO Write a generic parser that returns a generic expression
Requires [[*Design Strings for the Lisp]] to be complete first. ** TODO Test system registration of allocated units :test:
*** TODO Write the general parser In particular, does clean up work as we expect? Do we have situations
** Design :design: where we may double free or not clean up something we should've?
*** TODO Design Big Integers :api: ** TODO Design garbage collection scheme :design:gc:
We currently have 62 bit integers implemented via immediate values
embedded in a pointer. We need to be able to support even _bigger_
integers. How do we do this?
*** TODO Capitalise symbols (TBD) :optimisation:
Should we capitalise symbols? This way, we limit the symbol table's
possible options a bit (potentially we could design a better hashing
algorithm?) and it would be kinda like an actual Lisp.
*** TODO Consider reader macros :reader:
Common Lisp has so-called "reader macros" which allows users to write
Lisp code that affects further Lisp code reading. It's quite
powerful.
Scheme doesn't have it. Should we implement this?
** Allocator :allocator:
*** Some definitions
- Managed objects are allocations that are generated as part of
evaluating user code i.e. strings, vectors, conses that are all made
as part of evaluating code.
- Unmanaged objects are allocations we do as part of the runtime.
These are things that we expect to have near infinite lifetimes
(such as the symbol table, vector of allocated objects, etc).
*** DONE Design an allocator
We'll need an allocator for all our managed objects. Requirements:
- Stable pointers (memory that has already been allocated should be
free to utilise via the same pointer for the lifetime of the
allocator)
- Able to tag allocations as unused (i.e. "free") and able to reuse
these allocations
- This will link into the garbage collector, which should yield a
sequence of objects that were previously tagged as unfree and
should be "freed".
- Able to allocate all the managed types we have
**** DONE Design allocation data structures
**** DONE Design allocation methods for different lisp types
- Strings (when implemented)
***** DONE Conses
***** DONE Vectors
**** DONE Design allocation freeing method
*** TODO Design garbage collection scheme :gc:
Really, regardless of what I do, we need to have some kind of garbage Really, regardless of what I do, we need to have some kind of garbage
collection header on whatever managed objects we allocate. We need to collection header on whatever we allocate e.g. references if we
perform garbage collection against the managed objects, and leave the reference count for GC.
unmanaged objects to the runtime. *** TODO Mark stage
**** TODO Mark stage When some item is being used by another, we need a way to adjust the
We need to mark all objects that are currently accessible from the metadata such that the system is aware of it being used.
environment. This means we need to have a root environment which we
mark all our accessible objects from. Any objects that aren't marked
by this obviously are inaccessible, so we can then sweep them.
How do we store this mark on our managed objects? I think the For example, say I have X, Y as random allocated objects. Then I
simplest approach would be to allocate an extra 8 bytes just before construct CONS(X, Y). Then, ref(X) and ref(Y) need to be incremented
any managed object we allocate i.e. [8 byte buffer] <object>. Then, to say I'm using them.
during the mark phase, we can walk back those 8 bytes and *** TODO Sweep
inspect/mutate the mark. Say I have an object that I construct, C. If ref(C) = 0, then C is no
**** TODO Sweep longer needed, and is free.
Once we've marked all objects that are accessible, we need to
investigate all the objects that aren't. We do have
[[file:alisp.h::vec_t memory;][this]] which provides a global map of
all the stuff we've allocated so far ([[file:alisp.h::void
sys_register(sys_t *, lisp_t *);][sys_register]] is used to add to
this, and any managed object is expected to register).
We can iterate through the map and collect all the unmarked objects. There are two components to this:
What do we do with these? - we need a way of decrementing references if an object is no longer needed.
- we need a way of running through everything we've allocated so far
to figure out what's free to take away.
1) They are technically freestanding objects allocated through Once we've filtered out what we don't need anymore, what should we do
~calloc~, so we could just free them. with them? Naive approach would be to just actually ~free~ the cells
2) Manage some collection of previous allocations to reuse in our next in question. But I think the next item may be a better idea.
allocation. *** TODO Use previous allocations if they're free to use
If we have no references to a cell, this cell is free to use. In
other words, if I later allocate something of the same type, instead
of allocating a new object, why not just use the one I've already got?
Option (1) is obvious and relatively clean to setup in our current
idea:
- Say at index I we have an object that is unmarked
- Free the associated object at index I
- Swap the end of the array with the cell at index I, then decrement
the size of the container
This is an O(1) time operation.
Option (2) is also relatively straightforward, but we need another
counter in order to make it work:
- Say at index I we have an object that is unmarked
- Swap the end of the array with the cell at index I, then decrement
the size of the container
**** TODO Use previous allocations if they're free to use
This way, instead of deleting the memory or forgetting about it, we This way, instead of deleting the memory or forgetting about it, we
can reuse it. We need to be really careful to make sure our ref(X) is can reuse it. We need to be really careful to make sure our ref(X) is
actually precise, we don't want to trample on the user's hard work. actually precise, we don't want to trample on the user's hard work.
@@ -176,62 +151,14 @@ Latter approach time complexity:
Former approach is better time complexity wise, but latter is way Former approach is better time complexity wise, but latter is way
better in terms of simplicity of code. Must deliberate. better in terms of simplicity of code. Must deliberate.
** Unit tests :tests: ** TODO Design Big Integers
*** TODO Test streams :streams: We currently have 62 bit integers implemented via immediate values
**** DONE Test file init embedded in a pointer. We need to be able to support even _bigger_
[[file:test/test_stream.c::void stream_test_file(void)]] integers. How do we do this?
***** DONE Test successful init from real files ** DONE Test value constructors and destructors :test:
Ensure stream_size is 0 i.e. we don't read anything on creation.
Also ensure stream_eoc is false.
***** DONE Test failed init from fake files
**** DONE Test peeking and next
[[file:test/test_stream.c::void stream_test_peek_next(void)]]
- Peeking with bad streams ('\0' return)
- Peeking with good streams (no effect on position)
- Next with bad streams ('\0' return, no effect on position)
- Next with good streams (effects position)
- Peeking after next (should just work)
**** DONE Test seeking
[[file:test/test_stream.c::void stream_test_seek(void)]]
- Seeking forward/backward on a bad stream (should stop at 0)
- Seeking forward/backward too far (should clamp)
- Seeking forward/backward zero sum via relative index (stream_seek)
**** DONE Test substring
[[file:test/test_stream.c::void stream_test_substr(void)]]
- Substr on bad stream (NULL sv)
- Substr on bad position/size (NULL sv)
- Substr relative/absolute (good SV)
**** TODO Test till
[[file:test/test_stream.c::void stream_test_till(void)]]
- till on a bad stream (NULL SV)
- till on an ended stream (NULL SV)
- till on a stream with no items in search string (eoc)
- till on a stream with all items in search string (no effect)
- till on a stream with prefix being all search string (no effect)
- till on a stream with suffix being all search string (stops at
suffix)
**** TODO Test while
[[file:test/test_stream.c::void stream_test_while(void)]]
- while on a bad stream (NULL SV)
- while on an ended stream (NULL SV)
- while on a stream with no items in search string (no effect)
- while on a stream with all items in search string (eoc)
- while on a stream with prefix being all search string (effect)
- while on a stream with suffix being all search string (no effect)
**** TODO Test line_col
[[file:test/test_stream.c::void stream_test_line_col(void)]]
- line_col on bad stream (no effect on args)
- line_col on eoc stream (should go right to the end)
- line_col on random points in a stream
*** TODO Test reader :reader:
*** DONE Test system registration of allocated units
In particular, does clean up work as we expect? Do we have situations
where we may double free or not clean up something we should've?
** Completed
*** DONE Test value constructors and destructors :test:
Test if ~make_int~ works with ~as_int,~ ~intern~ with ~as_sym~. Test if ~make_int~ works with ~as_int,~ ~intern~ with ~as_sym~.
Latter will require a symbol table. Latter will require a symbol table.
*** DONE Test containers constructors and destructors :test: ** DONE Test containers constructors and destructors :test:
Test if ~make_vec~ works with ~as_vec~, ~cons~ with ~as_cons~ AND Test if ~make_vec~ works with ~as_vec~, ~cons~ with ~as_cons~ AND
~CAR~, ~CDR~. ~CAR~, ~CDR~.

Binary file not shown.

View File

@@ -1,14 +0,0 @@
;;; hello-world.lisp - 2026-02-04
;; Copyright (C) 2026 Aryadev Chavali
;; This program is distributed in the hope that it will be useful, but WITHOUT
;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
;; FOR A PARTICULAR PURPOSE. See the GNU General Public License Version 2 for
;; details.
;; You may distribute and modify this code under the terms of the GNU General
;; Public License Version 2, which you should have received a copy of along with
;; this program. If not, please go to <https://www.gnu.org/licenses/>.
(display "Hello, world!")

View File

@@ -8,13 +8,13 @@
#ifndef ALISP_H #ifndef ALISP_H
#define ALISP_H #define ALISP_H
#include <alisp/allocator.h>
#include <alisp/base.h> #include <alisp/base.h>
#include <alisp/lisp.h> #include <alisp/lisp.h>
#include <alisp/reader.h> #include <alisp/reader.h>
#include <alisp/stream.h> #include <alisp/stream.h>
#include <alisp/sv.h> #include <alisp/sv.h>
#include <alisp/symtable.h> #include <alisp/symtable.h>
#include <alisp/tag.h>
#include <alisp/vec.h> #include <alisp/vec.h>
#endif #endif

View File

@@ -1,61 +0,0 @@
/* allocator.h: Lisp Allocator
* Created: 2026-02-12
* Author: Aryadev Chavali
* License: See end of file
* Commentary:
*/
#ifndef ALLOCATOR_H
#define ALLOCATOR_H
#include <alisp/lisp.h>
#include <alisp/vec.h>
#define ALLOC_PAGE_DEFAULT_SIZE 512
typedef struct
{
u64 padding : 56;
tag_t tag : 8;
u64 references;
} alloc_metadata_t;
static_assert(sizeof(alloc_metadata_t) == 16,
"16 byte metadata required for alignment purposes");
typedef struct
{
alloc_metadata_t metadata;
u8 data[];
} alloc_node_t;
typedef struct
{
vec_t data;
} page_t;
typedef struct
{
vec_t pages;
vec_t free_vec;
} alloc_t;
lisp_t *alloc_make(alloc_t *, tag_t type);
void alloc_delete(alloc_t *, lisp_t *);
u64 alloc_cost(alloc_t *);
void alloc_free(alloc_t *);
#endif
/* Copyright (C) 2026 Aryadev Chavali
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License Version 2 for
* details.
* You may distribute and modify this code under the terms of the GNU General
* Public License Version 2, which you should have received a copy of along with
* this program. If not, please go to <https://www.gnu.org/licenses/>.
*/

View File

@@ -19,20 +19,6 @@
#define FAIL(MSG) assert(false && "FAIL: " #MSG) #define FAIL(MSG) assert(false && "FAIL: " #MSG)
#define TODO(MSG) assert(false && "TODO: " #MSG) #define TODO(MSG) assert(false && "TODO: " #MSG)
#ifndef VERBOSE_LOGS
#define VERBOSE_LOGS 0
#endif
#if VERBOSE_LOGS
#define LOG(...) \
do \
{ \
printf(__VA_ARGS__); \
} while (0)
#else
#define LOG(...)
#endif
/// Numeric aliases /// Numeric aliases
typedef uint8_t u8; typedef uint8_t u8;
typedef uint16_t u16; typedef uint16_t u16;

View File

@@ -8,9 +8,7 @@
#ifndef LISP_H #ifndef LISP_H
#define LISP_H #define LISP_H
#include <stdio.h> #include <alisp/symtable.h>
#include <alisp/string.h>
#include <alisp/vec.h> #include <alisp/vec.h>
#define NIL 0 #define NIL 0
@@ -23,53 +21,34 @@ typedef struct
lisp_t *car, *cdr; lisp_t *car, *cdr;
} cons_t; } cons_t;
/// Tagging system /// System context
typedef enum Tag typedef struct
{ {
TAG_SMI = 0b00000001, // Atomic types vec_t memory;
TAG_SYM = 0b00000011, sym_table_t symtable;
TAG_NIL = 0b00000000, // Container types (0 LSB) } sys_t;
TAG_CONS = 0b00000010,
TAG_VEC = 0b00000100,
TAG_STR = 0b00000110,
NUM_TAGS = 6,
} tag_t;
// Some helper macros for tagging void sys_init(sys_t *);
#define SHIFT_TAG (8) void sys_register(sys_t *, lisp_t *);
#define MASK_TAG ((1 << SHIFT_TAG) - 1) void sys_cleanup(sys_t *);
#define TAG(PTR, TYPE) ((lisp_t *)((((u64)(PTR)) << SHIFT_TAG) | TAG_##TYPE)) /// Constructors and destructors
#define UNTAG(PTR) (((u64)PTR) >> SHIFT_TAG) lisp_t *make_int(i64);
#define GET_TAG(PTR) ((tag_t)(((u64)(PTR)) & MASK_TAG)) lisp_t *make_vec(sys_t *, u64);
#define IS_TAG(PTR, TYPE) (GET_TAG(PTR) == TAG_##TYPE) lisp_t *intern(sys_t *, sv_t);
lisp_t *cons(sys_t *, lisp_t *, lisp_t *);
#define INT_BITS ((sizeof(i64) * 8) - SHIFT_TAG) i64 as_int(lisp_t *);
#define INT_MAX ((((i64)1) << (INT_BITS - 1)) - 1)
#define INT_MIN (-(INT_MAX + 1))
tag_t tag_get(const lisp_t *);
u64 tag_sizeof(tag_t);
u64 lisp_sizeof(lisp_t *);
lisp_t *lisp_reset(lisp_t *);
void lisp_print(FILE *, lisp_t *);
lisp_t *tag_smi(const i64);
lisp_t *tag_sym(const char *);
lisp_t *tag_cons(const cons_t *);
lisp_t *tag_vec(const vec_t *);
lisp_t *tag_str(const str_t *);
lisp_t *tag_generic(void *, tag_t);
i64 as_smi(lisp_t *);
char *as_sym(lisp_t *); char *as_sym(lisp_t *);
cons_t *as_cons(lisp_t *); cons_t *as_cons(lisp_t *);
vec_t *as_vec(lisp_t *); vec_t *as_vec(lisp_t *);
str_t *as_str(lisp_t *);
#define CAR(L) (as_cons(L)->car) #define CAR(L) (as_cons(L)->car)
#define CDR(L) (as_cons(L)->cdr) #define CDR(L) (as_cons(L)->cdr)
lisp_t *car(lisp_t *);
lisp_t *cdr(lisp_t *);
#endif #endif
/* Copyright (C) 2026 Aryadev Chavali /* Copyright (C) 2026 Aryadev Chavali

View File

@@ -9,22 +9,12 @@
#define READER_H #define READER_H
#include <alisp/stream.h> #include <alisp/stream.h>
#include <alisp/sys.h>
typedef enum typedef enum
{ {
READ_ERR_OK = 0, READ_OK = 0,
READ_ERR_EOF,
READ_ERR_EXPECTED_CLOSED_BRACE,
READ_ERR_EXPECTED_CLOSED_SQUARE_BRACKET,
READ_ERR_EXPECTED_CLOSING_SPEECHMARKS,
READ_ERR_UNEXPECTED_CLOSED_BRACE,
READ_ERR_UNEXPECTED_CLOSED_SQUARE_BRACKET,
READ_ERR_UNKNOWN_CHAR,
} read_err_t; } read_err_t;
const char *read_err_to_cstr(read_err_t);
// Attempt to read an expression from the stream, storing it in a pointer, // Attempt to read an expression from the stream, storing it in a pointer,
// returning any errors if failed. // returning any errors if failed.
read_err_t read(sys_t *, stream_t *, lisp_t **); read_err_t read(sys_t *, stream_t *, lisp_t **);
@@ -39,11 +29,10 @@ read_err_t read_all(sys_t *, stream_t *, vec_t *);
* This program is distributed in the hope that it will be useful, but WITHOUT * This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License Version 2 for * FOR A PARTICULAR PURPOSE. See the Unlicense for details.
* details.
* You may distribute and modify this code under the terms of the GNU General * You may distribute and modify this code under the terms of the Unlicense,
* Public License Version 2, which you should have received a copy of along with * which you should have received a copy of along with this program. If not,
* this program. If not, please go to <https://www.gnu.org/licenses/>. * please go to <https://unlicense.org/>.
*/ */

View File

@@ -22,15 +22,13 @@ typedef enum
typedef enum typedef enum
{ {
STREAM_ERR_INVALID_PTR = -4,
STREAM_ERR_FILE_NONEXISTENT = -3,
STREAM_ERR_FILE_READ = -2,
STREAM_ERR_PIPE_NONEXISTENT = -1,
STREAM_ERR_OK = 0, STREAM_ERR_OK = 0,
STREAM_ERR_INVALID_PTR,
STREAM_ERR_FILE_NONEXISTENT,
STREAM_ERR_FILE_READ,
STREAM_ERR_PIPE_NONEXISTENT,
} stream_err_t; } stream_err_t;
const char *stream_err_to_cstr(stream_err_t);
typedef struct typedef struct
{ {
vec_t cache; vec_t cache;
@@ -40,7 +38,7 @@ typedef struct
typedef struct typedef struct
{ {
stream_type_t type; stream_type_t type;
const char *name; char *name;
u64 position; u64 position;
union union
{ {
@@ -51,13 +49,10 @@ typedef struct
#define STREAM_DEFAULT_CHUNK 64 #define STREAM_DEFAULT_CHUNK 64
stream_err_t stream_init_string(stream_t *, const char *, sv_t); stream_err_t stream_init_string(stream_t *, char *, sv_t);
stream_err_t stream_init_pipe(stream_t *, const char *, FILE *); stream_err_t stream_init_pipe(stream_t *, char *, FILE *);
// NOTE: stream_init_file will attempt to read all content from the FILE stream_err_t stream_init_file(stream_t *, char *, FILE *);
// descriptor. Use with caution. void stream_stop(stream_t *);
stream_err_t stream_init_file(stream_t *, const char *, FILE *);
void stream_reset(stream_t *);
void stream_free(stream_t *);
// End of Content (i.e. we've consumed all cached content/file) // End of Content (i.e. we've consumed all cached content/file)
bool stream_eoc(stream_t *); bool stream_eoc(stream_t *);
@@ -69,15 +64,9 @@ char stream_next(stream_t *);
// Peek current character, do not push position // Peek current character, do not push position
char stream_peek(stream_t *); char stream_peek(stream_t *);
// Move forward or backward in the stream, return success of operation // Move forward or backward in the stream, return success of operation
u64 stream_seek(stream_t *, i64); bool stream_seek(stream_t *, i64);
u64 stream_seek_forward(stream_t *, u64); bool stream_seek_forward(stream_t *, u64);
u64 stream_seek_backward(stream_t *, u64); bool stream_seek_backward(stream_t *, u64);
// Return a string view to stream data relative to the current position of the
// stream
sv_t stream_sv(stream_t *);
// Return a string view to data from the start of the stream
sv_t stream_sv_abs(stream_t *);
// Return a relative substring of a given size // Return a relative substring of a given size
sv_t stream_substr(stream_t *, u64); sv_t stream_substr(stream_t *, u64);
@@ -91,9 +80,6 @@ sv_t stream_till(stream_t *, const char *);
// present. // present.
sv_t stream_while(stream_t *, const char *); sv_t stream_while(stream_t *, const char *);
// Get the line and column of the stream at its current position.
void stream_line_col(stream_t *, u64 *line, u64 *col);
#endif #endif
/* Copyright (C) 2026 Aryadev Chavali /* Copyright (C) 2026 Aryadev Chavali

View File

@@ -1,35 +0,0 @@
/* string.h: String library
* Created: 2026-03-05
* Author: Aryadev Chavali
* License: See end of file
* Commentary:
*/
#ifndef STRING_H
#define STRING_H
#include <alisp/sv.h>
#include <alisp/vec.h>
typedef struct
{
vec_t data;
} str_t;
str_t string_make(sv_t sv);
sv_t string_sv(str_t *);
#endif
/* Copyright (C) 2026 Aryadev Chavali
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License Version 2 for
* details.
* You may distribute and modify this code under the terms of the GNU General
* Public License Version 2, which you should have received a copy of along with
* this program. If not, please go to <https://www.gnu.org/licenses/>.
*/

View File

@@ -14,12 +14,11 @@
typedef struct typedef struct
{ {
u64 size; u64 size;
const char *data; char *data;
} sv_t; } sv_t;
// String view macro constructor // String view macro constructor
#define SV(DATA, SIZE) ((sv_t){.data = (DATA), .size = (SIZE)}) #define SV(DATA, SIZE) ((sv_t){.data = (DATA), .size = (SIZE)})
#define SV_AUTO(DATA) ((sv_t){.data = (void *)(DATA), .size = sizeof(DATA) - 1})
// Pretty printers // Pretty printers
#define SV_FMT(SV) (int)(SV).size, (SV).data #define SV_FMT(SV) (int)(SV).size, (SV).data
#define PR_SV "%.*s" #define PR_SV "%.*s"
@@ -27,13 +26,6 @@ typedef struct
// String view functions // String view functions
sv_t sv_copy(sv_t); sv_t sv_copy(sv_t);
sv_t sv_chop_left(sv_t, u64 size);
sv_t sv_chop_right(sv_t, u64 size);
sv_t sv_truncate(sv_t, u64 newsize);
sv_t sv_substr(sv_t, u64 position, u64 size);
sv_t sv_till(sv_t, const char *reject);
sv_t sv_while(sv_t, const char *accept);
#endif #endif

View File

@@ -21,11 +21,8 @@ typedef struct
#define SYM_TABLE_INIT_SIZE (1 << 10) #define SYM_TABLE_INIT_SIZE (1 << 10)
void sym_table_init(sym_table_t *); void sym_table_init(sym_table_t *);
const char *sym_table_find(sym_table_t *, sv_t); char *sym_table_find(sym_table_t *, sv_t);
void sym_table_free(sym_table_t *); void sym_table_cleanup(sym_table_t *);
// Debugging function: total memory used by symbol table.
u64 sym_table_cost(sym_table_t *);
#endif #endif

View File

@@ -1,57 +0,0 @@
/* sys.h: System context and constructors
* Created: 2026-02-12
* Author: Aryadev Chavali
* License: See end of file
* Commentary:
*/
#ifndef SYS_H
#define SYS_H
#include <alisp/allocator.h>
#include <alisp/lisp.h>
#include <alisp/symtable.h>
/// System context
typedef struct
{
alloc_t memory;
sym_table_t symtable;
} sys_t;
void sys_init(sys_t *);
lisp_t *sys_alloc(sys_t *, tag_t type);
void sys_delete(sys_t *, lisp_t *);
void sys_free(sys_t *);
// Debugging function: provides total memory usage from system.
u64 sys_cost(sys_t *);
/// Constructors and general Lisp API
lisp_t *make_int(i64);
lisp_t *intern(sys_t *, sv_t);
lisp_t *cons(sys_t *, lisp_t *, lisp_t *);
lisp_t *make_list(sys_t *, lisp_t **, u64);
lisp_t *make_vec(sys_t *, u64);
lisp_t *make_str(sys_t *, u64);
lisp_t *car(lisp_t *);
lisp_t *cdr(lisp_t *);
void lisp_free(sys_t *, lisp_t *);
void lisp_free_rec(sys_t *, lisp_t *);
#endif
/* Copyright (C) 2026 Aryadev Chavali
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License Version 2 for
* details.
* You may distribute and modify this code under the terms of the GNU General
* Public License Version 2, which you should have received a copy of along with
* this program. If not, please go to <https://www.gnu.org/licenses/>.
*/

68
include/alisp/tag.h Normal file
View File

@@ -0,0 +1,68 @@
/* tag.h: Pointer tagging
* Created: 2026-02-04
* Author: Aryadev Chavali
* License: See end of file
* Commentary:
*/
#ifndef TAG_H
#define TAG_H
#include <alisp/lisp.h>
typedef enum Tag
{
TAG_NIL = 0b00000000, // Start of atomic types
TAG_INT = 0b00000001, // Special tag so we can encode 63 bit integers
TAG_SYM = 0b00000100,
TAG_CONS = 0b00000010, // Start of container types
TAG_VEC = 0b00000110,
NUM_TAGS = 5,
} tag_t;
static_assert(NUM_TAGS == 5, "Expected NUM_TAGS == 5 for enum SHIFT");
enum Shift
{
SHIFT_INT = 1,
SHIFT_SYM = 8,
SHIFT_CONS = 8,
SHIFT_VEC = 8,
};
static_assert(NUM_TAGS == 5, "Expected NUM_TAGS == 5 for enum MASK");
enum Mask
{
MASK_INT = 0b00000001,
MASK_SYM = 0b11111111,
MASK_CONS = 0b11111111,
MASK_VEC = 0b11111111,
};
// Some helper macros for tagging
#define TAG(PTR, TYPE) ((lisp_t *)(((PTR) << SHIFT_##TYPE) | TAG_##TYPE))
#define IS_TAG(PTR, TYPE) (((u64)(PTR) & MASK_##TYPE) == TAG_##TYPE)
#define UNTAG(PTR, TYPE) (((u64)PTR) >> SHIFT_##TYPE)
#define INT_MAX ((1L << 62) - 1)
#define INT_MIN (-(1L << 62))
tag_t get_tag(lisp_t *);
lisp_t *tag_int(i64);
lisp_t *tag_sym(char *);
lisp_t *tag_cons(cons_t *);
lisp_t *tag_vec(vec_t *);
#endif
/* Copyright (C) 2026 Aryadev Chavali
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License Version 2 for
* details.
* You may distribute and modify this code under the terms of the GNU General
* Public License Version 2, which you should have received a copy of along with
* this program. If not, please go to <https://www.gnu.org/licenses/>.
*/

View File

@@ -31,18 +31,12 @@ static_assert(sizeof(vec_t) == 64, "vec_t has to be 64 bytes as part of SBO");
#define VEC_GET(V, I, T) (((T *)vec_data(V))[I]) #define VEC_GET(V, I, T) (((T *)vec_data(V))[I])
#define VEC_SIZE(V, T) ((V)->size / (sizeof(T))) #define VEC_SIZE(V, T) ((V)->size / (sizeof(T)))
#define FOR_VEC(INDEX, V, T) \
for (size_t INDEX = 0; INDEX < VEC_SIZE(V, T); ++INDEX)
void vec_init(vec_t *, u64); void vec_init(vec_t *, u64);
void vec_free(vec_t *); void vec_free(vec_t *);
void vec_reset(vec_t *);
u8 *vec_data(vec_t *); u8 *vec_data(vec_t *);
// Append, possibly reallocating memory
void vec_append(vec_t *, const void *const, u64); void vec_append(vec_t *, const void *const, u64);
// Try to append without allocating memory
bool vec_try_append(vec_t *, const void *const, u64);
void vec_ensure_free(vec_t *, u64); void vec_ensure_free(vec_t *, u64);
void vec_clone(vec_t *, vec_t *); void vec_clone(vec_t *, vec_t *);

View File

@@ -1,256 +0,0 @@
/* allocator.c: Allocator implementations
* Created: 2026-02-12
* Author: Aryadev Chavali
* License: See end of file
* Commentary:
*/
#include <stdlib.h>
#include <alisp/allocator.h>
#include <alisp/lisp.h>
#include <alisp/vec.h>
#include <string.h>
page_t *make_page(u64 size)
{
page_t *page = calloc(1, sizeof(*page));
vec_init(&page->data, MAX(size, ALLOC_PAGE_DEFAULT_SIZE));
return page;
}
alloc_node_t *make_node(page_t *page, tag_t type)
{
alloc_node_t *node = NULL;
u64 size = sizeof(*node);
static_assert(NUM_TAGS == 6);
switch (type)
{
case TAG_CONS:
size += sizeof(cons_t);
break;
case TAG_VEC:
size += sizeof(vec_t);
break;
case TAG_STR:
size += sizeof(str_t);
break;
case TAG_NIL:
case TAG_SMI:
case TAG_SYM:
default:
FAIL("Unreachable");
return node;
}
// We must ensure size is a multiple of 8 for alignment purposes
size = (size & 0b111) == 0 ? size : size + (8 - (size & 0b111));
if (!vec_try_append(&page->data, NULL, size))
return NULL;
node = (alloc_node_t *)(vec_data(&page->data) + page->data.size - size);
node->metadata = (alloc_metadata_t){.references = 0, .tag = type};
return node;
}
alloc_node_t *lisp_to_node(lisp_t *lisp)
{
void *raw_ptr = NULL;
static_assert(NUM_TAGS == 6);
switch (tag_get(lisp))
{
case TAG_CONS:
raw_ptr = as_cons(lisp);
break;
case TAG_VEC:
raw_ptr = as_vec(lisp);
break;
case TAG_STR:
raw_ptr = as_str(lisp);
break;
case TAG_NIL: // These shouldn't be allocated
case TAG_SMI:
case TAG_SYM:
default:
FAIL("Unreachable");
return NIL;
}
alloc_node_t *node = raw_ptr;
return &node[-1];
}
lisp_t *alloc_make(alloc_t *alloc, tag_t type)
{
static_assert(NUM_TAGS == 6);
switch (type)
{
case TAG_CONS:
case TAG_VEC:
case TAG_STR:
break;
case TAG_NIL: // These shouldn't be allocated
case TAG_SMI:
case TAG_SYM:
default:
FAIL("Unreachable");
return NIL;
}
// We want to try to fill this node with an allocation of this type.
alloc_node_t *node = NULL;
// Try to get something from the free vector
u64 free_vec_size = VEC_SIZE(&alloc->free_vec, alloc_node_t *);
for (u64 i = 0; i < free_vec_size; ++i)
{
alloc_node_t **nodeptr = &VEC_GET(&alloc->free_vec, i, alloc_node_t *);
// Skip any nodes that don't have the right type.
if (nodeptr[0]->metadata.tag != type)
continue;
assert("Expected free node to have no references" &&
nodeptr[0]->metadata.references == 0);
// Pop this node off the free vector by swapping it with the last item and
// decrementing the size of the vector.
alloc_node_t **lastptr =
&VEC_GET(&alloc->free_vec, free_vec_size - 1, alloc_node_t *);
alloc_node_t *val = *nodeptr;
*nodeptr = *lastptr;
*lastptr = val;
// Decrement the size of the free vector
alloc->free_vec.size -= sizeof(val);
// Then use that valid (and now unused) node as our return.
node = *lastptr;
goto end;
}
// We couldn't get anything from the free vector, so try to allocate a fresh
// one against one of the pages.
FOR_VEC(i, &alloc->pages, page_t *)
{
page_t *page = VEC_GET(&alloc->pages, i, page_t *);
node = make_node(page, type);
if (node)
goto end;
}
// There aren't any pages we can allocate against, so we need to make a new
// page.
page_t *page = make_page(0);
vec_append(&alloc->pages, &page, sizeof(page));
node = make_node(page, type);
end:
if (!node)
FAIL("Unexpected issue with allocating to a verifiably good page");
return tag_generic(node->data, type);
}
void alloc_delete(alloc_t *alloc, lisp_t *lisp)
{
switch (tag_get(lisp))
{
case TAG_CONS:
case TAG_VEC:
case TAG_STR:
break;
case TAG_NIL: // These can't be deleted (not allocated)
case TAG_SMI:
case TAG_SYM:
default:
FAIL("Unreachable");
return;
}
alloc_node_t *node = lisp_to_node(lisp);
assert(node && node->metadata.references == 0);
// If already present in the free vector, stop.
FOR_VEC(i, &alloc->free_vec, alloc_node_t *)
{
alloc_node_t *other = VEC_GET(&alloc->pages, i, alloc_node_t *);
if (other == node)
{
return;
}
}
// Otherwise, add to the free vector.
lisp_reset(lisp);
vec_append(&alloc->free_vec, &node, sizeof(node));
}
u64 alloc_cost(alloc_t *alloc)
{
u64 total_size = alloc->pages.size;
FOR_VEC(i, &alloc->pages, page_t *)
{
page_t *page = VEC_GET(&alloc->pages, i, page_t *);
total_size += page->data.size;
}
return total_size;
}
void alloc_free(alloc_t *alloc)
{
FOR_VEC(i, &alloc->pages, page_t *)
{
page_t *page = VEC_GET(&alloc->pages, i, page_t *);
// Iterate through every alloc_node in this page (dynamic walk)
for (u64 j = 0; j < VEC_SIZE(&page->data, u8);)
{
alloc_node_t *node = (alloc_node_t *)(vec_data(&page->data) + j);
u64 next = sizeof(*node) + tag_sizeof(node->metadata.tag);
static_assert(NUM_TAGS == 6);
switch (node->metadata.tag)
{
case TAG_CONS:
// Do nothing - will be cleaned by overall vec free anyway
break;
case TAG_VEC:
vec_free((vec_t *)node->data);
break;
case TAG_STR:
vec_free(&((str_t *)node->data)->data);
break;
case TAG_NIL:
case TAG_SMI:
case TAG_SYM:
default:
FAIL("Unreachable");
}
j += next;
}
// Each page was allocated on the heap.
vec_free(&page->data);
free(page);
}
vec_free(&alloc->pages);
vec_free(&alloc->free_vec);
memset(alloc, 0, sizeof(*alloc));
}
/* Copyright (C) 2026 Aryadev Chavali
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License Version 2 for
* details.
* You may distribute and modify this code under the terms of the GNU General
* Public License Version 2, which you should have received a copy of along with
* this program. If not, please go to <https://www.gnu.org/licenses/>.
*/

View File

@@ -9,267 +9,105 @@
#include <string.h> #include <string.h>
#include <alisp/lisp.h> #include <alisp/lisp.h>
#include <alisp/tag.h>
lisp_t *tag_smi(i64 i) void sys_init(sys_t *sys)
{ {
return TAG(i, SMI); memset(sys, 0, sizeof(*sys));
} }
lisp_t *tag_sym(const char *str) void sys_register(sys_t *sys, lisp_t *ptr)
{ {
return TAG(str, SYM); // Simply append it to the list of currently active conses
vec_append(&sys->memory, &ptr, sizeof(&ptr));
} }
lisp_t *tag_vec(const vec_t *vec) void sys_cleanup(sys_t *sys)
{ {
return TAG(vec, VEC); static_assert(NUM_TAGS == 5);
}
lisp_t *tag_str(const str_t *str) sym_table_cleanup(&sys->symtable);
{ if (sys->memory.size == 0)
return TAG(str, STR);
}
lisp_t *tag_cons(const cons_t *cons)
{
return TAG(cons, CONS);
}
lisp_t *tag_generic(void *ptr, tag_t type)
{
static_assert(NUM_TAGS == 6);
switch (type)
{
case TAG_NIL:
return TAG(ptr, NIL);
case TAG_SMI:
return tag_smi((i64)ptr);
case TAG_SYM:
return tag_sym(ptr);
case TAG_CONS:
return tag_cons(ptr);
case TAG_VEC:
return tag_vec(ptr);
case TAG_STR:
return tag_str(ptr);
default:
FAIL("Unreachable");
return NIL;
}
}
tag_t tag_get(const lisp_t *lisp)
{
return GET_TAG(lisp);
}
i64 as_smi(lisp_t *obj)
{
assert(IS_TAG(obj, SMI));
u64 raw_obj = UNTAG(obj);
u64 msb = (NTH_BYTE(raw_obj, 6) & 0x80) >> 7;
msb = ((1LU << 8) - msb) << 56;
return (i64)(raw_obj | msb);
}
char *as_sym(lisp_t *obj)
{
assert(IS_TAG(obj, SYM));
return (char *)UNTAG(obj);
}
cons_t *as_cons(lisp_t *obj)
{
assert(IS_TAG(obj, CONS));
return (cons_t *)UNTAG(obj);
}
str_t *as_str(lisp_t *obj)
{
assert(IS_TAG(obj, STR));
return (str_t *)UNTAG(obj);
}
vec_t *as_vec(lisp_t *obj)
{
assert(IS_TAG(obj, VEC));
return (vec_t *)UNTAG(obj);
}
void lisp_print(FILE *fp, lisp_t *lisp)
{
if (!fp)
return; return;
static_assert(NUM_TAGS == 6);
switch (tag_get(lisp))
{
case TAG_NIL:
fprintf(fp, "NIL");
break;
case TAG_SMI:
#if VERBOSE_LOGS == 2
fprintf(fp, "INT[");
#endif
fprintf(fp, "%ld", as_smi(lisp));
#if VERBOSE_LOGS == 2
fprintf(fp, "]");
#endif
break;
case TAG_SYM:
#if VERBOSE_LOGS == 2
fprintf(fp, "SYM[");
#endif
fprintf(fp, "%s", as_sym(lisp));
#if VERBOSE_LOGS == 2
fprintf(fp, "]");
#endif
break;
case TAG_CONS:
{
#if VERBOSE_LOGS == 2
fprintf(fp, "LIST[");
#else
fprintf(fp, "(");
#endif
for (; lisp; lisp = CDR(lisp))
{
if (IS_TAG(lisp, CONS))
{
lisp_t *car = CAR(lisp);
lisp_t *cdr = CDR(lisp);
lisp_print(fp, car); // Iterate through each cell of memory currently allocated and free them
if (cdr && !IS_TAG(cdr, CONS)) for (size_t i = 0; i < VEC_SIZE(&sys->memory, lisp_t **); ++i)
{ {
fprintf(fp, " . "); lisp_t *allocated = VEC_GET(&sys->memory, i, lisp_t *);
} switch (get_tag(allocated))
else if (cdr)
{ {
fprintf(fp, " "); case TAG_CONS:
// Delete the cons
free(as_cons(allocated));
break;
case TAG_VEC:
{
vec_t *vec = as_vec(allocated);
vec_free(vec);
free(vec);
break;
}
case TAG_NIL:
case TAG_INT:
case TAG_SYM:
case NUM_TAGS:
// shouldn't be dealt with (either constant or dealt with elsewhere)
break;
} }
} }
// Free the container
vec_free(&sys->memory);
// Ensure no one treats this as active in any sense
memset(sys, 0, sizeof(*sys));
}
lisp_t *make_int(i64 i)
{
return tag_int(i);
}
lisp_t *cons(sys_t *sys, lisp_t *car, lisp_t *cdr)
{
cons_t *cons = calloc(1, sizeof(*cons));
cons->car = car;
cons->cdr = cdr;
lisp_t *lcons = tag_cons(cons);
sys_register(sys, lcons);
return lcons;
}
lisp_t *make_vec(sys_t *sys, u64 capacity)
{
vec_t *vec = calloc(1, sizeof(*vec));
vec_init(vec, capacity);
lisp_t *ptr = tag_vec(vec);
sys_register(sys, ptr);
return ptr;
}
lisp_t *intern(sys_t *sys, sv_t sv)
{
char *str = sym_table_find(&sys->symtable, sv);
return tag_sym(str);
}
lisp_t *car(lisp_t *lsp)
{
if (!IS_TAG(lsp, CONS))
return NIL;
else else
{ return CAR(lsp);
lisp_print(fp, lisp);
break;
}
}
#if VERBOSE_LOGS == 2
fprintf(fp, "]");
#else
fprintf(fp, ")");
#endif
break;
}
case TAG_VEC:
{
#if VERBOSE_LOGS == 2
fprintf(fp, "VEC[");
#else
fprintf(fp, "[");
#endif
vec_t *vec = as_vec(lisp);
FOR_VEC(i, vec, lisp_t *)
{
lisp_t *item = VEC_GET(vec, i, lisp_t *);
lisp_print(fp, item);
if (i < VEC_SIZE(vec, lisp_t *) - 1)
{
fprintf(fp, " ");
}
} }
#if VERBOSE_LOGS == 2 lisp_t *cdr(lisp_t *lsp)
fprintf(fp, "]");
#else
fprintf(fp, "]");
#endif
break;
}
case TAG_STR:
{ {
#if VERBOSE_LOGS == 2 if (!IS_TAG(lsp, CONS))
fprintf(fp, "STR["); return NIL;
#else else
fprintf(fp, "\""); return CDR(lsp);
#endif
sv_t sv = string_sv(as_str(lisp));
fprintf(fp, PR_SV, SV_FMT(sv));
#if VERBOSE_LOGS == 2
fprintf(fp, "]");
#else
fprintf(fp, "\"");
#endif
break;
}
default:
FAIL("Unreachable");
break;
}
}
u64 tag_sizeof(tag_t tag)
{
static_assert(NUM_TAGS == 6);
switch (tag)
{
case TAG_NIL:
return 0;
case TAG_SMI:
case TAG_SYM:
return sizeof(lisp_t *);
case TAG_CONS:
return sizeof(cons_t);
case TAG_VEC:
return sizeof(vec_t);
case TAG_STR:
return sizeof(str_t);
default:
FAIL("Unreachable");
return 0;
}
}
u64 lisp_sizeof(lisp_t *lisp)
{
return tag_sizeof(tag_get(lisp));
}
lisp_t *lisp_reset(lisp_t *lisp)
{
switch (tag_get(lisp))
{
case TAG_NIL:
case TAG_SMI:
case TAG_SYM:
// Nothing to "reset" here.
return lisp;
case TAG_CONS:
{
// Make `car` and `cons` NIL
CAR(lisp) = NIL;
CDR(lisp) = NIL;
return lisp;
}
case TAG_VEC:
{
vec_reset(as_vec(lisp));
return lisp;
}
case TAG_STR:
{
vec_reset(&as_str(lisp)->data);
return lisp;
}
default:
{
FAIL("Unreachable");
return lisp;
}
}
} }
/* Copyright (C) 2025, 2026 Aryadev Chavali /* Copyright (C) 2025, 2026 Aryadev Chavali

View File

@@ -11,111 +11,43 @@
#include <alisp/alisp.h> #include <alisp/alisp.h>
void usage(FILE *fp); const char *TOKEN_DELIM = "\n ";
int init_stream_on_args(int argc, char *argv[], FILE **pipe, stream_t *stream);
int main(int argc, char *argv[]) int main(void)
{ {
int ret = 0; sym_table_t table = {0};
FILE *pipe = NULL; sym_table_init(&table);
char filename[] = "./lorem.txt";
FILE *fp = fopen(filename, "r");
stream_t stream = {0}; stream_t stream = {0};
vec_t ast = {0}; stream_init_file(&stream, filename, fp);
sys_t sys = {0};
ret = init_stream_on_args(argc, argv, &pipe, &stream); for (u64 token_no = 1; !stream_eoc(&stream); ++token_no)
if (ret)
goto end;
LOG("[INFO]: Initialised stream for `%s`\n", stream.name);
{ {
read_err_t err = read_all(&sys, &stream, &ast); // Skip forward any delimiters
if (err) stream_while(&stream, TOKEN_DELIM);
{ // Get the token (up until delimiter)
u64 line = 0, col = 0; sv_t token = stream_till(&stream, TOKEN_DELIM);
stream_line_col(&stream, &line, &col); char *interned = sym_table_find(&table, token);
fprintf(stderr, "%s:%lu:%lu: ERROR: %s\n", stream.name, line, col, printf("%s[%lu] => `%s`\n", stream.name, token_no, interned);
read_err_to_cstr(err));
ret = 1;
goto end;
}
} }
LOG("[INFO]: Utilised %lu bytes in parsing\n", sys_cost(&sys)); printf("\nTable count=%lu\n", table.count);
LOG("[INFO]: Parsed %lu %s\n", VEC_SIZE(&ast, lisp_t *), for (u64 i = 0, j = 0; i < table.capacity; ++i)
VEC_SIZE(&ast, lisp_t *) == 1 ? "expr" : "exprs");
{ {
FOR_VEC(i, &ast, lisp_t *) sv_t token = VEC_GET(&table.entries, i, sv_t);
{ if (!token.data)
#if VERBOSE_LOGS continue;
lisp_t *expr = VEC_GET(&ast, i, lisp_t *); printf("[%lu]@[%lu] => `" PR_SV "`\n", j, i, SV_FMT(token));
printf("\t[%lu]: ", i); ++j;
lisp_print(stdout, expr);
printf("\n");
#endif
}
} }
end: stream_stop(&stream);
sys_free(&sys); fclose(fp);
vec_free(&ast); sym_table_cleanup(&table);
stream_free(&stream);
if (pipe)
fclose(pipe);
return ret;
}
int init_stream_on_args(int argc, char *argv[], FILE **pipe, stream_t *stream)
{
if (argc == 1)
{
usage(stderr);
return 1;
}
else if (argc != 2)
{
TODO("alisp doesn't support multiple files currently.");
}
if (strncmp(argv[1], "--", 2) == 0)
{
stream_err_t err = stream_init_pipe(stream, "stdin", stdin);
if (err)
{
fprintf(stderr, "ERROR: %s from `%s`\n", stream_err_to_cstr(err),
argv[1]);
return 1;
}
}
else if (strncmp(argv[1], "--help", 6) == 0)
{
usage(stdout);
return 0; return 0;
} }
else
{
*pipe = fopen(argv[1], "rb");
stream_err_t err = stream_init_file(stream, argv[1], *pipe);
if (err)
{
fprintf(stderr, "ERROR: %s from `%s`\n", stream_err_to_cstr(err),
argv[1]);
return 1;
}
}
return 0;
}
void usage(FILE *fp)
{
fprintf(fp, "Usage: alisp [OPTIONS...] FILE\n"
"Options:\n"
"\t--help Print this usage and exit.\n"
"File:\n"
"\t<filename> Read and interpret this file from filesystem.\n"
"\t-- Read and interpret from stdin using an EOF.\n");
}
/* Copyright (C) 2025, 2026 Aryadev Chavali /* Copyright (C) 2025, 2026 Aryadev Chavali

View File

@@ -1,295 +0,0 @@
/* reader.c: Stream reader implementation
* Created: 2026-02-04
* Author: Aryadev Chavali
* License: See end of file
* Commentary:
*/
#include <ctype.h>
#include <string.h>
#include <alisp/reader.h>
const char *read_err_to_cstr(read_err_t err)
{
switch (err)
{
case READ_ERR_OK:
return "OK";
case READ_ERR_EOF:
return "EOF";
case READ_ERR_UNKNOWN_CHAR:
return "UNKNOWN_CHAR";
case READ_ERR_EXPECTED_CLOSED_BRACE:
return "EXPECTED_CLOSED_BRACE";
case READ_ERR_EXPECTED_CLOSED_SQUARE_BRACKET:
return "EXPECTED_CLOSED_SQUARE_BRACKET";
case READ_ERR_EXPECTED_CLOSING_SPEECHMARKS:
return "EXPECTED_CLOSING_SPEECHMARKS";
case READ_ERR_UNEXPECTED_CLOSED_BRACE:
return "UNEXPECTED_CLOSED_BRACE";
case READ_ERR_UNEXPECTED_CLOSED_SQUARE_BRACKET:
return "UNEXPECTED_CLOSED_SQUARE_BRACKET";
default:
FAIL("Unreachable");
}
}
// Accepted characters for symbols.
static const char *SYMBOL_CHARS =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!$%&*+,-./"
":<=>?@\\^_{|}~0123456789";
// Little predicate using SYMBOL_CHARS
bool is_sym(char c)
{
return strchr(SYMBOL_CHARS, c) != NULL;
}
void skip_comments_and_whitespace(stream_t *stream)
{
for (char c = stream_peek(stream); c != '\0' && (isspace(c) || c == ';');
c = stream_peek(stream))
{
stream_while(stream, " \t\n\0");
if (stream_peek(stream) == ';')
{
// Skip till newline
stream_till(stream, "\n");
}
}
}
read_err_t read_sym(sys_t *sys, stream_t *stream, lisp_t **ret)
{
sv_t sym_sv = stream_while(stream, SYMBOL_CHARS);
*ret = intern(sys, sym_sv);
return READ_ERR_OK;
}
read_err_t read_int(sys_t *sys, stream_t *stream, lisp_t **ret)
{
sv_t digits_sv = stream_while(stream, "0123456789");
if (is_sym(stream_peek(stream)))
{
// This is actually a symbol
stream_seek_backward(stream, digits_sv.size);
return read_sym(sys, stream, ret);
}
if (digits_sv.size >= 18)
{
TODO("alisp doesn't support big integers (bigger than 56 bits) yet");
}
i64 n = 0;
for (u64 i = 0; i < digits_sv.size; ++i)
{
char c = digits_sv.data[i];
u8 digit = c - '0';
// NOTE: 10i + digit > INT_MAX
// => 10i > INT_MAX - digit
// => i > (INT_MAX - digit) / 10
if (n > (INT_MAX - digit) / 10)
{
TODO("alisp doesn't support big integers (bigger than 56 bits) yet");
}
n *= 10;
n += digit;
}
*ret = make_int(n);
return READ_ERR_OK;
}
read_err_t read_negative(sys_t *sys, stream_t *stream, lisp_t **ret)
{
char c = stream_next(stream);
if (isdigit(c))
{
read_err_t err = read_int(sys, stream, ret);
if (err)
return err;
*ret = make_int(as_smi(*ret) * -1);
return READ_ERR_OK;
}
else if (is_sym(c) || isspace(c))
{
stream_seek_backward(stream, 1);
return read_sym(sys, stream, ret);
}
else
{
return READ_ERR_UNKNOWN_CHAR;
}
}
read_err_t read_list(sys_t *sys, stream_t *stream, lisp_t **ret)
{
u64 old_pos = stream->position;
// skip past the open parentheses '('
(void)stream_next(stream);
lisp_t *top = NIL;
lisp_t *cur = NIL;
while (!stream_eoc(stream) && stream_peek(stream) != ')')
{
lisp_t *item = NIL;
read_err_t err = read(sys, stream, &item);
if (err == READ_ERR_EOF)
{
goto no_close_brace;
}
else if (err)
{
return err;
}
else if (!top)
{
top = cons(sys, item, NIL);
cur = top;
}
else
{
as_cons(cur)->cdr = cons(sys, item, NIL);
cur = cdr(cur);
}
}
if (stream_peek(stream) != ')')
{
goto no_close_brace;
}
stream_next(stream);
*ret = top;
return READ_ERR_OK;
no_close_brace:
stream->position = old_pos;
return READ_ERR_EXPECTED_CLOSED_BRACE;
}
read_err_t read_vec(sys_t *sys, stream_t *stream, lisp_t **ret)
{
u64 old_pos = stream->position;
(void)stream_next(stream);
lisp_t *container = make_vec(sys, 0);
while (!stream_eoc(stream) && stream_peek(stream) != ']')
{
lisp_t *item = NIL;
read_err_t err = read(sys, stream, &item);
if (err == READ_ERR_EOF)
{
goto no_close_square_bracket;
}
else if (err)
{
return err;
}
else
{
vec_append(as_vec(container), &item, sizeof(item));
}
}
if (stream_peek(stream) != ']')
goto no_close_square_bracket;
stream_next(stream);
*ret = container;
return READ_ERR_OK;
no_close_square_bracket:
stream->position = old_pos;
return READ_ERR_EXPECTED_CLOSED_SQUARE_BRACKET;
}
read_err_t read_str(sys_t *sys, stream_t *stream, lisp_t **ret)
{
u64 old_pos = stream->position;
(void)stream_next(stream);
sv_t contents = stream_till(stream, "\"");
if (stream_eoc(stream) || stream_peek(stream) != '\"')
{
stream->position = old_pos;
return READ_ERR_EXPECTED_CLOSING_SPEECHMARKS;
}
stream_next(stream);
lisp_t *lisp = make_str(sys, contents.size);
vec_append(&as_str(lisp)->data, contents.data, contents.size);
*ret = lisp;
return READ_ERR_OK;
}
read_err_t read_quote(sys_t *sys, stream_t *stream, lisp_t **ret)
{
lisp_t *to_quote = NIL;
stream_next(stream);
read_err_t err = read(sys, stream, &to_quote);
if (err)
return err;
lisp_t *items[] = {intern(sys, SV_AUTO("quote")), to_quote};
*ret = make_list(sys, items, ARRSIZE(items));
return READ_ERR_OK;
}
read_err_t read_all(sys_t *sys, stream_t *stream, vec_t *out)
{
while (!stream_eoc(stream))
{
lisp_t *item = NIL;
read_err_t err = read(sys, stream, &item);
if (err)
return err;
else
vec_append(out, &item, sizeof(item));
skip_comments_and_whitespace(stream);
}
return READ_ERR_OK;
}
read_err_t read(sys_t *sys, stream_t *stream, lisp_t **ret)
{
skip_comments_and_whitespace(stream);
if (stream_eoc(stream))
return READ_ERR_EOF;
char c = stream_peek(stream);
if (isdigit(c))
return read_int(sys, stream, ret);
else if (c == '-')
return read_negative(sys, stream, ret);
else if (is_sym(c))
return read_sym(sys, stream, ret);
else if (c == '\'')
return read_quote(sys, stream, ret);
else if (c == '(')
return read_list(sys, stream, ret);
else if (c == ')')
return READ_ERR_UNEXPECTED_CLOSED_BRACE;
else if (c == '[')
return read_vec(sys, stream, ret);
else if (c == ']')
return READ_ERR_UNEXPECTED_CLOSED_SQUARE_BRACKET;
else if (c == '\"')
return read_str(sys, stream, ret);
return READ_ERR_UNKNOWN_CHAR;
}
/* Copyright (C) 2026 Aryadev Chavali
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License Version 2 for
* details.
* You may distribute and modify this code under the terms of the GNU General
* Public License Version 2, which you should have received a copy of along with
* this program. If not, please go to <https://www.gnu.org/licenses/>.
*/

View File

@@ -5,36 +5,12 @@
* Commentary: * Commentary:
*/ */
#include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <alisp/base.h>
#include <alisp/stream.h> #include <alisp/stream.h>
#include <alisp/sv.h>
#include <alisp/vec.h>
const char *stream_err_to_cstr(stream_err_t err) stream_err_t stream_init_string(stream_t *stream, char *name, sv_t contents)
{
switch (err)
{
case STREAM_ERR_INVALID_PTR:
return "INVALID PTR";
case STREAM_ERR_FILE_NONEXISTENT:
return "FILE NONEXISTENT";
case STREAM_ERR_FILE_READ:
return "FILE READ";
case STREAM_ERR_PIPE_NONEXISTENT:
return "PIPE NONEXISTENT";
case STREAM_ERR_OK:
return "OK";
default:
FAIL("Unreachable");
}
}
stream_err_t stream_init_string(stream_t *stream, const char *name,
sv_t contents)
{ {
if (!stream) if (!stream)
return STREAM_ERR_INVALID_PTR; return STREAM_ERR_INVALID_PTR;
@@ -48,13 +24,10 @@ stream_err_t stream_init_string(stream_t *stream, const char *name,
return STREAM_ERR_OK; return STREAM_ERR_OK;
} }
stream_err_t stream_init_pipe(stream_t *stream, const char *name, FILE *pipe) stream_err_t stream_init_pipe(stream_t *stream, char *name, FILE *pipe)
{ {
if (!stream) if (!stream || !pipe)
return STREAM_ERR_INVALID_PTR; return STREAM_ERR_INVALID_PTR;
else if (!pipe)
return STREAM_ERR_PIPE_NONEXISTENT;
name = name ? name : "<stream>"; name = name ? name : "<stream>";
memset(stream, 0, sizeof(*stream)); memset(stream, 0, sizeof(*stream));
@@ -62,56 +35,39 @@ stream_err_t stream_init_pipe(stream_t *stream, const char *name, FILE *pipe)
stream->name = name; stream->name = name;
stream->pipe.file = pipe; stream->pipe.file = pipe;
vec_init(&stream->pipe.cache, STREAM_DEFAULT_CHUNK);
return STREAM_ERR_OK; return STREAM_ERR_OK;
} }
stream_err_t stream_init_file(stream_t *stream, const char *name, FILE *pipe) stream_err_t stream_init_file(stream_t *stream, char *name, FILE *pipe)
{ {
if (!stream) if (!stream || !pipe)
return STREAM_ERR_INVALID_PTR; return STREAM_ERR_INVALID_PTR;
else if (!pipe)
return STREAM_ERR_FILE_NONEXISTENT;
name = name ? name : "<stream>"; name = name ? name : "<stream>";
memset(stream, 0, sizeof(*stream)); memset(stream, 0, sizeof(*stream));
stream->type = STREAM_TYPE_FILE; stream->type = STREAM_TYPE_FILE;
stream->name = name; stream->name = name;
stream->pipe.file = NULL; stream->pipe.file = pipe;
// NOTE: We're reading all the data from the file descriptor now. vec_init(&stream->pipe.cache, STREAM_DEFAULT_CHUNK);
fseek(pipe, 0, SEEK_END);
long size = ftell(pipe);
fseek(pipe, 0, SEEK_SET);
vec_ensure_free(&stream->pipe.cache, size);
int read = fread(vec_data(&stream->pipe.cache), 1, size, pipe);
// These must be equivalent for this function.
assert(read == size);
stream->pipe.cache.size += size;
return STREAM_ERR_OK; return STREAM_ERR_OK;
} }
void stream_reset(stream_t *stream) void stream_stop(stream_t *stream)
{
if (!stream)
return;
stream->position = 0;
}
void stream_free(stream_t *stream)
{ {
if (!stream) if (!stream)
return; return;
switch (stream->type) switch (stream->type)
{ {
case STREAM_TYPE_STRING: case STREAM_TYPE_STRING:
free((char *)stream->string.data); free(stream->string.data);
break; break;
case STREAM_TYPE_FILE:
case STREAM_TYPE_PIPE: case STREAM_TYPE_PIPE:
// Must cleanup caching vector case STREAM_TYPE_FILE:
// Must cleanup vector
vec_free(&stream->pipe.cache); vec_free(&stream->pipe.cache);
break; break;
} }
@@ -134,15 +90,31 @@ u64 stream_size(stream_t *stream)
} }
} }
bool stream_eos(stream_t *stream)
{
assert(stream);
switch (stream->type)
{
case STREAM_TYPE_STRING:
return stream->position >= stream->string.size;
case STREAM_TYPE_PIPE:
case STREAM_TYPE_FILE:
return feof(stream->pipe.file);
default:
FAIL("Unreachable");
return 0;
}
}
bool stream_eoc(stream_t *stream) bool stream_eoc(stream_t *stream)
{ {
assert(stream); assert(stream);
switch (stream->type) switch (stream->type)
{ {
case STREAM_TYPE_FILE:
case STREAM_TYPE_STRING: case STREAM_TYPE_STRING:
return stream->position >= stream_size(stream); return stream->position >= stream->string.size;
case STREAM_TYPE_PIPE: case STREAM_TYPE_PIPE:
case STREAM_TYPE_FILE:
return feof(stream->pipe.file) && return feof(stream->pipe.file) &&
stream->position >= stream->pipe.cache.size; stream->position >= stream->pipe.cache.size;
default: default:
@@ -154,20 +126,23 @@ bool stream_eoc(stream_t *stream)
bool stream_chunk(stream_t *stream) bool stream_chunk(stream_t *stream)
{ {
assert(stream); assert(stream);
u64 to_read = STREAM_DEFAULT_CHUNK;
switch (stream->type) switch (stream->type)
{ {
case STREAM_TYPE_FILE:
case STREAM_TYPE_STRING: case STREAM_TYPE_STRING:
// nothing to chunk, hence false // vacuously true
return false; return true;
case STREAM_TYPE_PIPE: case STREAM_TYPE_PIPE:
to_read = 1;
// fallthrough
case STREAM_TYPE_FILE:
{ {
if (feof(stream->pipe.file)) if (feof(stream->pipe.file))
// We can't read anymore. End of the line // We can't read anymore. End of the line
return false; return false;
vec_ensure_free(&stream->pipe.cache, STREAM_DEFAULT_CHUNK); vec_ensure_free(&stream->pipe.cache, to_read);
int read = fread(vec_data(&stream->pipe.cache) + stream->pipe.cache.size, 1, int read = fread(vec_data(&stream->pipe.cache) + stream->pipe.cache.size, 1,
STREAM_DEFAULT_CHUNK, stream->pipe.file); to_read, stream->pipe.file);
// If we read something it's a good thing // If we read something it's a good thing
if (read > 0) if (read > 0)
@@ -176,10 +151,8 @@ bool stream_chunk(stream_t *stream)
return true; return true;
} }
else else
{
return false; return false;
} }
}
default: default:
FAIL("Unreachable"); FAIL("Unreachable");
return 0; return 0;
@@ -188,27 +161,32 @@ bool stream_chunk(stream_t *stream)
char stream_next(stream_t *stream) char stream_next(stream_t *stream)
{ {
if (stream_peek(stream) != '\0') char c = stream_peek(stream);
if (c != '\0')
++stream->position; ++stream->position;
return stream_peek(stream); return c;
} }
char stream_peek(stream_t *stream) char stream_peek(stream_t *stream)
{ {
// End of the line? We're done. // If we've reached end of stream, and end of content, there's really nothing
// to check here.
if (stream_eoc(stream)) if (stream_eoc(stream))
return '\0'; return '\0';
switch (stream->type) switch (stream->type)
{ {
case STREAM_TYPE_FILE:
case STREAM_TYPE_STRING: case STREAM_TYPE_STRING:
return stream_sv(stream).data[0]; return stream->string.data[stream->position];
case STREAM_TYPE_PIPE: case STREAM_TYPE_PIPE:
case STREAM_TYPE_FILE:
{ {
// Cached already? We are done. // Cached already? We are done.
if (stream->position < stream->pipe.cache.size) if (stream->position < stream->pipe.cache.size)
return stream_sv(stream).data[0]; {
const char *const str = (char *)vec_data(&stream->pipe.cache);
return str[stream->position];
}
// Try to read chunks in till we've reached it or we're at the end of the // Try to read chunks in till we've reached it or we're at the end of the
// file. // file.
@@ -220,7 +198,7 @@ char stream_peek(stream_t *stream)
// Same principle as the stream_eos(stream) check. // Same principle as the stream_eos(stream) check.
if (stream->position >= stream->pipe.cache.size) if (stream->position >= stream->pipe.cache.size)
return '\0'; return '\0';
return stream_sv(stream).data[0]; return ((char *)vec_data(&stream->pipe.cache))[stream->position];
} }
default: default:
FAIL("Unreachable"); FAIL("Unreachable");
@@ -228,45 +206,43 @@ char stream_peek(stream_t *stream)
} }
} }
u64 stream_seek(stream_t *stream, i64 offset) bool stream_seek(stream_t *stream, i64 offset)
{ {
if (offset < 0) if (offset < 0)
return stream_seek_backward(stream, offset * -1); return stream_seek_backward(stream, offset * -1);
else if (offset > 0) else if (offset > 0)
return stream_seek_forward(stream, offset); return stream_seek_forward(stream, offset);
else else
return 0; // vacuously successful
return true;
} }
u64 stream_seek_forward(stream_t *stream, u64 offset) bool stream_seek_forward(stream_t *stream, u64 offset)
{ {
if (stream_eoc(stream)) if (stream_eoc(stream))
return 0; return false;
else if (stream->position + offset < stream_size(stream))
{
stream->position += offset;
return offset;
}
// NOTE: The only case not caught by the above branches is exact-to-end
// movement (i.e. offset puts us exactly at the end of the stream) or movement
// beyond what we've cached.
switch (stream->type) switch (stream->type)
{ {
case STREAM_TYPE_FILE:
case STREAM_TYPE_STRING: case STREAM_TYPE_STRING:
{ {
// Clamp in the case of FILE and STRING movement since they're already if (stream->position + offset >= stream->string.size)
// fully cached. return false;
if (stream->position + offset >= stream_size(stream))
offset = stream_size(stream) - stream->position;
stream->position += offset; stream->position += offset;
return offset; return true;
} }
case STREAM_TYPE_PIPE: case STREAM_TYPE_PIPE:
case STREAM_TYPE_FILE:
{ {
// Pipes may have data remaining that hasn't been cached - we need to chunk // Similar principle as stream_peek really...
// before we can be sure to stop.
// Cached already? We are done.
if (stream->position + offset < stream->pipe.cache.size)
{
stream->position += offset;
return true;
}
// Try to read chunks in till we've reached it or we're at the end of the // Try to read chunks in till we've reached it or we're at the end of the
// file. // file.
@@ -275,57 +251,25 @@ u64 stream_seek_forward(stream_t *stream, u64 offset)
read_chunk = stream_chunk(stream)) read_chunk = stream_chunk(stream))
continue; continue;
// NOTE: We've read everything from the pipe, but the offset is greater. We // Same principle as the stream_eoc(stream) check.
// must clamp here. if (stream->position + offset > stream->pipe.cache.size)
if (stream->position + offset > stream_size(stream)) return false;
offset = stream_size(stream) - stream->position;
stream->position += offset; stream->position += offset;
return offset; return true;
} }
default: default:
FAIL("Unreachable"); FAIL("Unreachable");
}
return 0; return 0;
} }
}
u64 stream_seek_backward(stream_t *stream, u64 offset) bool stream_seek_backward(stream_t *stream, u64 offset)
{ {
if (!stream) assert(stream);
return 0;
if (stream->position < offset) if (stream->position < offset)
offset = stream->position; return false;
stream->position -= offset; stream->position -= offset;
return offset; return true;
}
sv_t stream_sv(stream_t *stream)
{
sv_t sv = stream_sv_abs(stream);
sv = sv_chop_left(sv, stream->position);
return sv;
}
sv_t stream_sv_abs(stream_t *stream)
{
if (!stream)
return SV(NULL, 0);
sv_t sv = {0};
switch (stream->type)
{
case STREAM_TYPE_STRING:
sv = stream->string;
break;
case STREAM_TYPE_FILE:
case STREAM_TYPE_PIPE:
sv = SV((char *)vec_data(&stream->pipe.cache), stream_size(stream));
break;
default:
FAIL("Unreachable");
return SV(NULL, 0);
}
return sv;
} }
sv_t stream_substr(stream_t *stream, u64 size) sv_t stream_substr(stream_t *stream, u64 size)
@@ -335,147 +279,86 @@ sv_t stream_substr(stream_t *stream, u64 size)
// See if I can go forward enough to make this substring // See if I can go forward enough to make this substring
u64 current_position = stream->position; u64 current_position = stream->position;
u64 successful = stream_seek_forward(stream, size); bool successful = stream_seek_forward(stream, size);
// Reset the position in either situation // Reset the position in either situation
stream->position = current_position; stream->position = current_position;
if (successful != size) if (!successful)
return SV(NULL, 0); return SV(NULL, 0);
sv_t sv = stream_sv(stream); char *ptr = NULL;
sv = sv_truncate(sv, size); switch (stream->type)
return sv; {
case STREAM_TYPE_STRING:
ptr = stream->string.data;
break;
case STREAM_TYPE_PIPE:
case STREAM_TYPE_FILE:
ptr = (char *)vec_data(&stream->pipe.cache);
break;
default:
FAIL("Unreachable");
return SV(NULL, 0);
}
return SV(ptr + stream->position, size);
} }
sv_t stream_substr_abs(stream_t *stream, u64 index, u64 size) sv_t stream_substr_abs(stream_t *stream, u64 index, u64 size)
{ {
switch (stream->type) switch (stream->type)
{ {
case STREAM_TYPE_STRING:
if (index + size <= stream_size(stream))
return SV(stream->string.data + index, size);
return SV(NULL, 0);
case STREAM_TYPE_PIPE: case STREAM_TYPE_PIPE:
case STREAM_TYPE_FILE:
{ {
if (index + size > stream_size(stream)) if (index + size <= stream_size(stream))
{ return SV((char *)vec_data(&stream->pipe.cache) + index, size);
// => try reading chunks till either we drop or we have enough space // (index + size > stream_size(stream)) => try reading chunks
for (bool read_chunk = stream_chunk(stream); for (bool read_chunk = stream_chunk(stream);
read_chunk && index + size >= stream->pipe.cache.size; read_chunk && index + size >= stream->pipe.cache.size;
read_chunk = stream_chunk(stream)) read_chunk = stream_chunk(stream))
continue; continue;
}
break;
}
case STREAM_TYPE_STRING:
case STREAM_TYPE_FILE:
break;
default:
FAIL("Unreachable");
}
sv_t sv = stream_sv_abs(stream); if (index + size > stream_size(stream))
sv = sv_chop_left(sv, index); return SV(NULL, 0);
sv = sv_truncate(sv, size); return SV((char *)vec_data(&stream->pipe.cache) + index, size);
return sv; }
default:
assert("Unreachable");
return SV(NULL, 0);
}
} }
sv_t stream_till(stream_t *stream, const char *str) sv_t stream_till(stream_t *stream, const char *str)
{ {
if (stream_eoc(stream)) if (stream_eoc(stream))
return SV(NULL, 0); return SV(NULL, 0);
u64 current_position = stream->position;
sv_t cur_sv = stream_sv(stream); for (char c = stream_peek(stream); c != '\0' && strchr(str, c) == NULL;
sv_t sv = sv_till(cur_sv, str); c = stream_next(stream))
stream_seek_forward(stream, sv.size); continue;
switch (stream->type) u64 size = stream->position - current_position;
{ if (size == 0)
case STREAM_TYPE_FILE: return SV(NULL, 0);
case STREAM_TYPE_STRING: return stream_substr_abs(stream, current_position, size - 1);
return sv;
case STREAM_TYPE_PIPE:
{
if (cur_sv.size > sv.size)
return sv;
// Build a substring by hand while chunking data.
u64 index, size;
for (index = stream->position - sv.size, size = sv.size;
cur_sv.size == sv.size; size += sv.size)
{
cur_sv = stream_sv(stream);
sv = sv_till(cur_sv, str);
stream_seek_forward(stream, sv.size);
if (sv.size == 0)
// Must stop if this has happened; nothing else to pick up.
break;
}
return stream_substr_abs(stream, index, size);
}
default:
FAIL("Unreachable");
}
} }
sv_t stream_while(stream_t *stream, const char *str) sv_t stream_while(stream_t *stream, const char *str)
{ {
if (stream_eoc(stream)) if (stream_eoc(stream))
return SV(NULL, 0); return SV(NULL, 0);
u64 current_position = stream->position;
sv_t cur_sv = stream_sv(stream); for (char c = stream_peek(stream); c != '\0' && strchr(str, c);
sv_t sv = sv_while(cur_sv, str); c = stream_next(stream))
stream_seek_forward(stream, sv.size); continue;
switch (stream->type) u64 size = stream->position - current_position;
{ if (size == 0)
case STREAM_TYPE_FILE: return SV(NULL, 0);
case STREAM_TYPE_STRING: return stream_substr_abs(stream, current_position, size - 1);
return sv;
case STREAM_TYPE_PIPE:
{
if (cur_sv.size > sv.size)
return sv;
// Build a substring by hand while chunking data.
u64 index, size;
for (index = stream->position - sv.size, size = sv.size;
cur_sv.size == sv.size; size += sv.size)
{
cur_sv = stream_sv(stream);
sv = sv_while(cur_sv, str);
stream_seek_forward(stream, sv.size);
if (sv.size == 0)
// Must stop if this has happened; nothing else to pick up.
break;
}
return stream_substr_abs(stream, index, size);
}
default:
FAIL("Unreachable");
}
}
void stream_line_col(stream_t *stream, u64 *line, u64 *col)
{
if (!stream || !line || !col)
return;
// Generate a string view from the stream of exactly the content /upto/
// stream.postion.
sv_t sv = stream_sv_abs(stream);
sv = sv_truncate(sv, stream->position + 1);
*line = 1;
*col = 0;
// TODO: Could this be faster? Does it matter?
for (u64 i = 0; i < sv.size; ++i)
{
char c = sv.data[i];
if (c == '\n')
{
*line += 1;
*col = 0;
}
else
{
*col += 1;
}
}
} }
/* Copyright (C) 2025, 2026 Aryadev Chavali /* Copyright (C) 2025, 2026 Aryadev Chavali

View File

@@ -1,44 +0,0 @@
/* string.c: String library implementation
* Created: 2026-03-05
* Author: Aryadev Chavali
* License: See end of file
* Commentary:
*/
#include <string.h>
#include <alisp/string.h>
str_t string_make(sv_t sv)
{
str_t string = {0};
if (sv.size)
{
vec_init(&string.data, sv.size);
if (sv.data)
{
memcpy(vec_data(&string.data), sv.data, sv.size);
}
}
return string;
}
sv_t string_sv(str_t *str)
{
if (!str)
return SV(NULL, 0);
return SV((char *)vec_data(&str->data), str->data.size);
}
/* Copyright (C) 2026 Aryadev Chavali
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License Version 2 for
* details.
* You may distribute and modify this code under the terms of the GNU General
* Public License Version 2, which you should have received a copy of along with
* this program. If not, please go to <https://www.gnu.org/licenses/>.
*/

View File

@@ -12,69 +12,12 @@
sv_t sv_copy(sv_t old) sv_t sv_copy(sv_t old)
{ {
if (old.size == 0)
return SV(old.data, 0);
else if (old.data == NULL)
return SV(NULL, old.size);
char *newstr = calloc(1, (old.size + 1) * sizeof(*newstr)); char *newstr = calloc(1, (old.size + 1) * sizeof(*newstr));
memcpy(newstr, old.data, old.size); memcpy(newstr, old.data, old.size);
newstr[old.size] = '\0'; newstr[old.size] = '\0';
return SV(newstr, old.size); return SV(newstr, old.size);
} }
sv_t sv_chop_left(sv_t sv, u64 size)
{
if (sv.size <= size)
return SV(NULL, 0);
return SV(sv.data + size, sv.size - size);
}
sv_t sv_chop_right(sv_t sv, u64 size)
{
if (sv.size <= size)
return SV(NULL, 0);
return SV(sv.data, sv.size - size);
}
sv_t sv_truncate(sv_t sv, u64 newsize)
{
if (newsize > sv.size)
return SV(NULL, 0);
return SV(sv.data, newsize);
}
sv_t sv_substr(sv_t sv, u64 position, u64 size)
{
sv_t result = sv_truncate(sv_chop_left(sv, position), size);
return result;
}
sv_t sv_till(sv_t sv, const char *reject)
{
if (sv.size == 0 || !sv.data)
return SV(NULL, 0);
u64 offset;
for (offset = 0; offset < sv.size && strchr(reject, sv.data[offset]) == NULL;
++offset)
continue;
return sv_truncate(sv, offset);
}
sv_t sv_while(sv_t sv, const char *accept)
{
if (sv.size == 0 || !sv.data)
return SV(NULL, 0);
u64 offset;
for (offset = 0; offset < sv.size && strchr(accept, sv.data[offset]) != NULL;
++offset)
continue;
return sv_truncate(sv, offset);
}
/* Copyright (C) 2025, 2026 Aryadev Chavali /* Copyright (C) 2025, 2026 Aryadev Chavali
* This program is distributed in the hope that it will be useful, but WITHOUT * This program is distributed in the hope that it will be useful, but WITHOUT

View File

@@ -29,7 +29,7 @@ void sym_table_init(sym_table_t *table)
vec_init(&table->entries, table->capacity * sizeof(sv_t)); vec_init(&table->entries, table->capacity * sizeof(sv_t));
} }
const char *sym_table_find(sym_table_t *table, sv_t sv) char *sym_table_find(sym_table_t *table, sv_t sv)
{ {
// Initialise the table if it's not done already // Initialise the table if it's not done already
if (table->entries.capacity == 0) if (table->entries.capacity == 0)
@@ -54,7 +54,7 @@ const char *sym_table_find(sym_table_t *table, sv_t sv)
return ENTRY_GET(table, index).data; return ENTRY_GET(table, index).data;
} }
void sym_table_free(sym_table_t *table) void sym_table_cleanup(sym_table_t *table)
{ {
// Iterate through the strings and free each of them. // Iterate through the strings and free each of them.
sv_t current = {0}; sv_t current = {0};
@@ -62,29 +62,13 @@ void sym_table_free(sym_table_t *table)
{ {
current = ENTRY_GET(table, i); current = ENTRY_GET(table, i);
if (current.data) if (current.data)
{ free(current.data);
// NOTE: We clone all data here, so it's okay to free by hand.
free((void *)current.data);
}
} }
// Free the underlying container // Free the underlying container
vec_free(&table->entries); vec_free(&table->entries);
memset(table, 0, sizeof(*table)); memset(table, 0, sizeof(*table));
} }
u64 sym_table_cost(sym_table_t *table)
{
if (!table || !table->count)
return 0;
else
{
u64 total_size = 0;
for (u64 i = 0; i < table->capacity; ++i)
total_size += ENTRY_GET(table, i).size;
return total_size;
}
}
/* Copyright (C) 2025, 2026 Aryadev Chavali /* Copyright (C) 2025, 2026 Aryadev Chavali
* This program is distributed in the hope that it will be useful, but WITHOUT * This program is distributed in the hope that it will be useful, but WITHOUT

180
src/sys.c
View File

@@ -1,180 +0,0 @@
/* sys.c: System implementation
* Created: 2026-02-12
* Author: Aryadev Chavali
* License: See end of file
* Commentary:
*/
#include <stdlib.h>
#include <string.h>
#include <alisp/sys.h>
void sys_init(sys_t *sys)
{
memset(sys, 0, sizeof(*sys));
}
lisp_t *sys_alloc(sys_t *sys, tag_t type)
{
static_assert(NUM_TAGS == 6);
switch (type)
{
case TAG_CONS:
case TAG_VEC:
case TAG_STR:
return alloc_make(&sys->memory, type);
// Shouldn't be allocated
case TAG_NIL:
case TAG_SMI:
case TAG_SYM:
default:
FAIL("Unreachable");
}
return NIL;
}
void sys_delete(sys_t *sys, lisp_t *lisp)
{
alloc_delete(&sys->memory, lisp);
}
u64 sys_cost(sys_t *sys)
{
return alloc_cost(&sys->memory) + sym_table_cost(&sys->symtable);
}
void sys_free(sys_t *sys)
{
sym_table_free(&sys->symtable);
alloc_free(&sys->memory);
memset(sys, 0, sizeof(*sys));
}
lisp_t *make_int(i64 i)
{
return tag_smi(i);
}
lisp_t *cons(sys_t *sys, lisp_t *car, lisp_t *cdr)
{
lisp_t *cons = sys_alloc(sys, TAG_CONS);
CAR(cons) = car;
CDR(cons) = cdr;
return cons;
}
lisp_t *make_list(sys_t *sys, lisp_t **lisps, u64 size)
{
lisp_t *root = NIL;
for (u64 i = size; i > 0; --i)
{
lisp_t *node = lisps[i - 1];
root = cons(sys, node, root);
}
return root;
}
lisp_t *make_vec(sys_t *sys, u64 capacity)
{
lisp_t *vec = sys_alloc(sys, TAG_VEC);
vec_init(as_vec(vec), capacity);
return vec;
}
lisp_t *make_str(sys_t *sys, u64 capacity)
{
lisp_t *str = sys_alloc(sys, TAG_STR);
vec_init(&as_str(str)->data, capacity);
return str;
}
lisp_t *intern(sys_t *sys, sv_t sv)
{
const char *str = sym_table_find(&sys->symtable, sv);
return tag_sym(str);
}
lisp_t *car(lisp_t *lsp)
{
if (!IS_TAG(lsp, CONS))
return NIL;
else
return CAR(lsp);
}
lisp_t *cdr(lisp_t *lsp)
{
if (!IS_TAG(lsp, CONS))
return NIL;
else
return CDR(lsp);
}
void lisp_free(sys_t *sys, lisp_t *lisp)
{
static_assert(NUM_TAGS == 6);
switch (tag_get(lisp))
{
case TAG_STR:
case TAG_VEC:
case TAG_CONS:
// Delete the underlying data
alloc_delete(&sys->memory, lisp);
break;
case TAG_NIL:
case TAG_SMI:
case TAG_SYM:
// shouldn't be dealt with (either constant or dealt with elsewhere)
break;
}
}
void lisp_free_rec(sys_t *sys, lisp_t *item)
{
static_assert(NUM_TAGS == 6);
switch (tag_get(item))
{
case TAG_CONS:
{
lisp_free_rec(sys, car(item));
lisp_free_rec(sys, cdr(item));
lisp_free(sys, item);
break;
}
case TAG_VEC:
{
vec_t *vec = as_vec(item);
FOR_VEC(i, vec, lisp_t *)
{
lisp_t *allocated = VEC_GET(vec, i, lisp_t *);
lisp_free_rec(sys, allocated);
}
lisp_free(sys, item);
break;
}
case TAG_STR:
{
lisp_free(sys, item);
break;
}
case TAG_NIL:
case TAG_SMI:
case TAG_SYM:
// shouldn't be dealt with (either constant or dealt with elsewhere)
break;
}
}
/* Copyright (C) 2026 Aryadev Chavali
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License Version 2 for
* details.
* You may distribute and modify this code under the terms of the GNU General
* Public License Version 2, which you should have received a copy of along with
* this program. If not, please go to <https://www.gnu.org/licenses/>.
*/

82
src/tag.c Normal file
View File

@@ -0,0 +1,82 @@
/* tag.c: Pointer tagging
* Created: 2025-08-19
* Author: Aryadev Chavali
* License: See end of file
* Commentary:
*/
#include <assert.h>
#include <stdlib.h>
#include <alisp/tag.h>
lisp_t *tag_int(i64 i)
{
return TAG((u64)i, INT);
}
lisp_t *tag_sym(char *str)
{
return TAG((u64)str, SYM);
}
lisp_t *tag_vec(vec_t *vec)
{
return TAG((u64)vec, VEC);
}
lisp_t *tag_cons(cons_t *cons)
{
return TAG((u64)cons, CONS);
}
tag_t get_tag(lisp_t *lisp)
{
static_assert(NUM_TAGS == 5);
if (!lisp)
return TAG_NIL;
else if (IS_TAG(lisp, INT))
return TAG_INT;
return (u64)lisp & 0xFF;
}
i64 as_int(lisp_t *obj)
{
assert(IS_TAG(obj, INT));
u64 p_obj = (u64)obj;
return UNTAG(p_obj, INT) | // Delete the tag
(NTH_BYTE(p_obj, 7) & 0x80) << 56 // duplicate the MSB (preserve sign)
;
}
char *as_sym(lisp_t *obj)
{
assert(IS_TAG(obj, SYM));
return (char *)UNTAG(obj, SYM);
}
cons_t *as_cons(lisp_t *obj)
{
assert(IS_TAG(obj, CONS));
return (cons_t *)UNTAG(obj, CONS);
}
vec_t *as_vec(lisp_t *obj)
{
assert(IS_TAG(obj, VEC));
return (vec_t *)UNTAG(obj, VEC);
}
/* Copyright (C) 2025, 2026 Aryadev Chavali
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License Version 2 for
* details.
* You may distribute and modify this code under the terms of the GNU General
* Public License Version 2, which you should have received a copy of along with
* this program. If not, please go to <https://www.gnu.org/licenses/>.
*/

View File

@@ -38,14 +38,6 @@ void vec_free(vec_t *vec)
memset(vec, 0, sizeof(*vec)); memset(vec, 0, sizeof(*vec));
} }
void vec_reset(vec_t *vec)
{
if (!vec)
return;
memset(vec_data(vec), 0, vec->capacity);
vec->size = 0;
}
u8 *vec_data(vec_t *vec) u8 *vec_data(vec_t *vec)
{ {
return vec->not_inlined ? vec->ptr : vec->inlined; return vec->not_inlined ? vec->ptr : vec->inlined;
@@ -89,24 +81,10 @@ void vec_append(vec_t *vec, const void *const ptr, u64 size)
if (!vec) if (!vec)
return; return;
vec_ensure_free(vec, size); vec_ensure_free(vec, size);
if (ptr)
memcpy(vec_data(vec) + vec->size, ptr, size); memcpy(vec_data(vec) + vec->size, ptr, size);
vec->size += size; vec->size += size;
} }
bool vec_try_append(vec_t *vec, const void *const ptr, u64 size)
{
if (!vec || vec->capacity - vec->size < size)
return false;
if (ptr)
{
void *newptr = vec_data(vec) + vec->size;
memcpy(newptr, ptr, size);
}
vec->size += size;
return true;
}
void vec_clone(vec_t *dest, vec_t *src) void vec_clone(vec_t *dest, vec_t *src)
{ {
if (!src || !dest) if (!src || !dest)

View File

@@ -6,33 +6,28 @@
*/ */
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include <time.h>
#include "./data.h" #include "./data.h"
#include "./test.h" #include "./test.h"
#include "./test_lisp_api.c" #include "./test_lisp_api.c"
#include "./test_stream.c"
#include "./test_sv.c"
#include "./test_symtable.c"
#include "./test_vec.c" #include "./test_vec.c"
test_suite_t SUITES[] = { test_suite_t SUITES[] = {
SV_SUITE, VEC_SUITE, SYMTABLE_SUITE, STREAM_SUITE, LISP_API_SUITE, LISP_API_SUITE,
VEC_SUITE,
}; };
int main(void) int main(void)
{ {
// Seed the pseudorandom gen for subsequent tests.
srand(time(NULL));
for (u64 i = 0; i < ARRSIZE(SUITES); ++i) for (u64 i = 0; i < ARRSIZE(SUITES); ++i)
{ {
test_suite_t suite = SUITES[i]; test_suite_t suite = SUITES[i];
printf("Suite [%s]\n", suite.name); printf("Suite [%s]\n", suite.name);
for (u64 j = 0; j < suite.size; ++j) for (u64 j = 0; j < suite.size; ++j)
{ {
printf("[%s]: Running...\n", suite.tests[j].name);
suite.tests[j].fn(); suite.tests[j].fn();
} }
} }

View File

@@ -10,50 +10,27 @@
#include <alisp/alisp.h> #include <alisp/alisp.h>
#ifndef TEST_VERBOSE #define TEST_PASSED() printf("[%s]: Passed\n", __func__)
#define TEST_VERBOSE 0
#endif
#define TEST_END() printf("\t[%s]: Passed\n", __func__)
#define TEST_INFO(...) \
do \
{ \
printf("\tINFO: "); \
printf(__VA_ARGS__); \
} while (0);
#if TEST_VERBOSE
#define TEST_START() printf("\t[%s]: Running...\n", __func__)
#define TEST(COND, ...) \ #define TEST(COND, ...) \
do \ do \
{ \ { \
bool cond = (COND); \ bool cond = (COND); \
if (!cond) \ if (!cond) \
{ \ { \
printf("\t\tFAIL: "); \ printf("\tFAIL: "); \
} \ } \
else \ else \
{ \ { \
printf("\t\tPASS: "); \ printf("\tPASS: "); \
} \ } \
printf("%s => ", #COND); \
printf(__VA_ARGS__); \ printf(__VA_ARGS__); \
printf("\n\t\t [%s]\n", #COND); \ printf("\n"); \
if (!cond) \ if (!cond) \
{ \ { \
assert(0); \ assert(0); \
} \ } \
} while (0) } while (0)
#else
#define TEST_START()
#define TEST(COND, ...) \
do \
{ \
if (!(COND)) \
{ \
assert(0); \
} \
} while (0)
#endif
typedef struct TestFn typedef struct TestFn
{ {
@@ -70,13 +47,8 @@ typedef struct
const u64 size; const u64 size;
} test_suite_t; } test_suite_t;
#define MAKE_TEST_SUITE(NAME, DESC, ...) \ #define MAKE_TEST_SUITE(NAME) \
const test_fn NAME##_TESTS[] = {__VA_ARGS__}; \ {.name = #NAME, .tests = NAME, .size = ARRSIZE(NAME)}
const test_suite_t NAME = { \
.name = DESC, \
.tests = NAME##_TESTS, \
.size = ARRSIZE(NAME##_TESTS), \
}
#endif #endif

View File

@@ -10,11 +10,8 @@
#include <alisp/lisp.h> #include <alisp/lisp.h>
void smi_test(void) void int_test(void)
{ {
TEST_START();
// Standard old testing, checking both sides of the number line and our set
// bounds.
i64 ints[] = { i64 ints[] = {
1, -1, (1 << 10) - 1, (-1) * ((1 << 10) - 1), INT_MIN, INT_MAX, 1, -1, (1 << 10) - 1, (-1) * ((1 << 10) - 1), INT_MIN, INT_MAX,
}; };
@@ -23,46 +20,36 @@ void smi_test(void)
{ {
i64 in = ints[i]; i64 in = ints[i];
lisp_t *lisp = make_int(in); lisp_t *lisp = make_int(in);
i64 out = as_smi(lisp); i64 out = as_int(lisp);
TEST(in == out, "%ld == %ld", in, out); TEST(in == out, "%ld == %ld", in, out);
} }
TEST_END(); TEST_PASSED();
} }
void smi_oob_test(void) void symtable_test(void)
{ {
TEST_START(); sym_table_t table = {0};
// These are integers that are completely out of the bounds of our standard sym_table_init(&table);
// tagging system due to their size. We need to use big integers for this. for (u64 i = 0; i < ARRSIZE(words); ++i)
i64 ints[] = { sym_table_find(&table, SV((char *)words[i], strlen(words[i])));
INT_MIN - 1,
INT_MAX + 1,
INT64_MIN,
INT64_MAX,
};
for (u64 i = 0; i < ARRSIZE(ints); ++i) TEST(table.count == ARRSIZE(unique_words), "%lu == %lu", table.count,
{ ARRSIZE(unique_words));
i64 in = ints[i];
lisp_t *lisp = make_int(in);
i64 out = as_smi(lisp);
TEST(in != out, "%ld != %ld", in, out); TEST(table.count < ARRSIZE(unique_words), "%lu < %lu", table.count,
ARRSIZE(unique_words));
TEST_PASSED();
sym_table_cleanup(&table);
} }
TEST_END(); void sym_test(void)
}
void sym_fresh_test(void)
{ {
TEST_START();
sys_t system = {0}; sys_t system = {0};
sys_init(&system); sys_init(&system);
// We expect every interned symbol to get a fresh allocation, but still be a
// valid representation of the original symbol.
for (u64 i = 0; i < ARRSIZE(words); ++i) for (u64 i = 0; i < ARRSIZE(words); ++i)
{ {
const char *in = words[i]; const char *in = words[i];
@@ -70,48 +57,14 @@ void sym_fresh_test(void)
char *out = as_sym(lisp); char *out = as_sym(lisp);
TEST(in != out, "%p != %p", in, out); TEST(in != out, "%p != %p", in, out);
TEST(strlen(in) == strlen(out), "%zu == %zu", strlen(in), strlen(out)); TEST(strlen(in) == strlen(out), "%zu == %zu", strlen(in), strlen(out));
TEST(strncmp(in, out, strlen(in)) == 0, "`%s` == `%s`", in, out); TEST(strncmp(in, out, strlen(in)) == 0, "%d", strncmp(in, out, strlen(in)));
} }
TEST_PASSED();
sys_free(&system); sys_cleanup(&system);
TEST_END();
}
void sym_unique_test(void)
{
TEST_START();
sys_t system = {0};
sys_init(&system);
sv_t symbols[] = {
SV_AUTO("hello"),
SV_AUTO("goodbye"),
SV_AUTO("display"),
SV_AUTO("@xs'a_sh;d::a-h]"),
};
lisp_t *ptrs[ARRSIZE(symbols)];
for (u64 i = 0; i < ARRSIZE(symbols); ++i)
{
ptrs[i] = intern(&system, symbols[i]);
TEST(ptrs[i] != 0, "%p (derived from `" PR_SV "`) is not NIL",
(void *)ptrs[i], SV_FMT(symbols[i]));
}
for (u64 i = 0; i < ARRSIZE(symbols); ++i)
{
lisp_t *newptr = intern(&system, symbols[i]);
TEST(newptr == ptrs[i], "interning again (%p) gives us the same (%p)",
(void *)newptr, (void *)ptrs[i]);
}
sys_free(&system);
TEST_END();
} }
void cons_test(void) void cons_test(void)
{ {
TEST_START();
sys_t system = {0}; sys_t system = {0};
sys_init(&system); sys_init(&system);
@@ -124,13 +77,7 @@ void cons_test(void)
lisp = cons(&system, lword, lisp); lisp = cons(&system, lword, lisp);
} }
/* // Make sure we've essentially reversed the `words` array
As we've cons'd each word, we'd expect the order to be reversed. This test
will allow us to verify:
1) words have actually been added to the linked list.
2) words are in the order we expect.
in one go.
*/
u64 i = ARRSIZE(words); u64 i = ARRSIZE(words);
for (lisp_t *iter = lisp; iter; iter = cdr(iter), --i) for (lisp_t *iter = lisp; iter; iter = cdr(iter), --i)
{ {
@@ -142,59 +89,22 @@ void cons_test(void)
TEST(strncmp(expected, got, size) == 0, "%s == %s", expected, got); TEST(strncmp(expected, got, size) == 0, "%s == %s", expected, got);
} }
sys_free(&system); TEST_PASSED();
TEST_END();
sys_cleanup(&system);
} }
void sys_test(void) const test_fn TESTS_LISP_API[] = {
{ MAKE_TEST_FN(int_test),
TEST_START(); MAKE_TEST_FN(sym_test),
sys_t sys = {0}; MAKE_TEST_FN(cons_test),
sys_init(&sys); };
u64 old_memory_size = sys_cost(&sys);
// Creating integers doesn't affect memory size const test_suite_t LISP_API_SUITE = {
(void)make_int(2000); .name = "Lisp API Tests",
TEST(sys_cost(&sys) == old_memory_size, .tests = TESTS_LISP_API,
"Making integers doesn't affect system memory size"); .size = ARRSIZE(TESTS_LISP_API),
};
// Creating symbols does affect memory size and memory table
(void)intern(&sys, SV_AUTO("hello world!"));
TEST(sys_cost(&sys) > old_memory_size,
"Interning doesn't affect system memory size");
TEST(sys.symtable.count > 0, "Interning affects symbol table");
old_memory_size = sys_cost(&sys);
// Creating conses do affect memory size
(void)cons(&sys, make_int(1), make_int(2));
TEST(sys_cost(&sys) > 0, "Creating conses affects memory size");
old_memory_size = sys_cost(&sys);
(void)cons(&sys, intern(&sys, SV_AUTO("test")), NIL);
TEST(sys_cost(&sys) > old_memory_size,
"Creating conses back to back affects memory size");
old_memory_size = sys_cost(&sys);
// Creating vectors does affect memory size
(void)make_vec(&sys, 8);
TEST(sys_cost(&sys) > old_memory_size,
"Creating vectors (size 8) affects memory size");
old_memory_size = sys_cost(&sys);
(void)make_vec(&sys, 1000);
TEST(sys_cost(&sys) > old_memory_size,
"Creating vectors (size 1000) affects memory size");
old_memory_size = sys_cost(&sys);
sys_free(&sys);
TEST_END();
}
MAKE_TEST_SUITE(LISP_API_SUITE, "LISP API Tests",
MAKE_TEST_FN(smi_test), MAKE_TEST_FN(smi_oob_test),
MAKE_TEST_FN(sym_fresh_test), MAKE_TEST_FN(sym_unique_test),
MAKE_TEST_FN(cons_test), MAKE_TEST_FN(sys_test), );
/* Copyright (C) 2026 Aryadev Chavali /* Copyright (C) 2026 Aryadev Chavali

View File

@@ -1,424 +0,0 @@
/* test_stream.c: Stream tests
* Created: 2026-02-05
* Author: Aryadev Chavali
* License: See end of file
* Commentary:
*/
#include <malloc.h>
#include <stdio.h>
#include "./data.h"
#include "./test.h"
#include <alisp/stream.h>
#include <string.h>
char valid_filename[50];
FILE *valid_fp = NULL;
FILE *invalid_fp = NULL;
void stream_test_prologue(void)
{
const char filename_prefix[] = "build/stream_test_";
valid_filename[ARRSIZE(valid_filename) - 1] = '\0';
memcpy(valid_filename, filename_prefix, ARRSIZE(filename_prefix) - 1);
for (u64 i = ARRSIZE(filename_prefix) - 1; i < ARRSIZE(valid_filename) - 1;
++i)
{
u8 num = (rand() % 36);
if (num < 26)
{
valid_filename[i] = num + 'a';
}
else
{
valid_filename[i] = num + '0';
}
}
TEST_INFO("Creating file named `%.*s`\n", (int)ARRSIZE(valid_filename),
valid_filename);
valid_fp = fopen(valid_filename, "wb");
// This should do a few things for us
// 1) Create a file, or clear the contents of it if it exists already.
// 2) Write some content to it.
assert(valid_fp);
fwrite(words_text, ARRSIZE(words_text) - 1, 1, valid_fp);
fclose(valid_fp);
valid_fp = fopen(valid_filename, "rb");
assert(valid_fp);
invalid_fp = NULL;
}
void stream_test_epilogue(void)
{
TEST_INFO("Deleting file `%s`\n", valid_filename);
assert(valid_fp);
fclose(valid_fp);
remove(valid_filename);
}
void stream_test_string(void)
{
TEST_START();
sv_t test_strings[] = {
SV_AUTO("hello, world!"),
SV_AUTO("another string"),
sv_truncate(SV_AUTO(text), ARRSIZE(text) / 2),
};
for (u64 i = 0; i < ARRSIZE(test_strings); ++i)
{
sv_t copy = sv_copy(test_strings[i]);
stream_t stream = {0};
stream_err_t err = stream_init_string(&stream, NULL, test_strings[i]);
TEST(err == STREAM_ERR_OK, "Stream initialising did not fail: %s",
stream_err_to_cstr(err));
TEST(stream_size(&stream) == test_strings[i].size,
"Stream size is always string size (%lu == %lu)", stream_size(&stream),
test_strings[i].size);
TEST(!stream_eoc(&stream), "Not end of content already");
stream_free(&stream);
TEST(strncmp(copy.data, test_strings[i].data, copy.size) == 0,
"Freeing a stream does not free the underlying memory it was derived "
"from");
// NOTE: Okay to free since we own copy.
free((void *)copy.data);
}
stream_t stream = {0};
stream_err_t err = stream_init_string(&stream, NULL, SV(NULL, 0));
TEST(err == STREAM_ERR_OK, "NULL stream initialising did not fail: %s",
stream_err_to_cstr(err));
TEST(stream_size(&stream) == 0, "NULL stream size is 0");
TEST(stream_eoc(&stream), "NULL stream is always at end of content");
stream_free(&stream);
TEST_END();
}
void stream_test_file(void)
{
TEST_START();
// Test that initialising works correctly
{
stream_t stream = {0};
{
stream_err_t err = stream_init_file(&stream, valid_filename, valid_fp);
TEST(err == STREAM_ERR_OK, "Expected initialisating to be okay: %s",
stream_err_to_cstr(err));
}
TEST(!stream_eoc(&stream), "Stream should not be at the EoC from init.");
stream_free(&stream);
}
// try to initialise the stream again but against a nonexistent file - we're
// expecting an error.
{
stream_t stream = {0};
{
stream_err_t err = stream_init_file(&stream, NULL, invalid_fp);
TEST(err != STREAM_ERR_OK, "Expected initialisating to not be okay: %s",
stream_err_to_cstr(err));
}
}
TEST_END();
}
void stream_test_peek_next(void)
{
TEST_START();
// Valid streams
{
stream_t stream = {0};
stream_init_file(&stream, valid_filename, valid_fp);
u64 old_position = stream.position;
char c1 = stream_peek(&stream);
TEST(c1 != '\0', "Peek should provide a normal character (%c)", c1);
TEST(stream.position == old_position,
"Peek should not shift the position (%lu -> %lu)", old_position,
stream.position);
char c2 = stream_next(&stream);
TEST(c2 != '\0', "Next should provide a normal character (%c)", c2);
TEST(stream.position > old_position,
"Next should shift the position (%lu -> %lu)", old_position,
stream.position);
TEST(c2 != c1,
"Next should yield a different character (%c) to the previous peek "
"(%c)",
c2, c1);
char c3 = stream_peek(&stream);
TEST(c3 == c2,
"Peeking should yield the same character (%c) as the previous next "
"(%c)",
c3, c2);
stream_free(&stream);
}
// Invalid streams
{
stream_t stream = {0};
stream_init_file(&stream, NULL, invalid_fp);
char c = stream_peek(&stream);
TEST(c == '\0', "Invalid streams should have an invalid peek (%c)", c);
u64 old_position = stream.position;
c = stream_next(&stream);
TEST(c == '\0', "Invalid streams should have an invalid next (%c)", c);
TEST(old_position == stream.position,
"Next on an invalid stream should not affect position (%lu -> %lu)",
old_position, stream.position);
stream_free(&stream);
}
TEST_END();
}
void stream_test_seek(void)
{
TEST_START();
// Seeking on invalid streams
{
stream_t stream = {0};
stream_init_file(&stream, NULL, invalid_fp);
u64 old_position = stream.position;
TEST(!stream_seek_forward(&stream, 1),
"Shouldn't be possible to seek forward on an invalid stream.");
TEST(old_position == stream.position,
"Position shouldn't be affected when seeking forward on an invalid "
"stream"
"(%lu -> %lu)",
old_position, stream.position);
TEST(!stream_seek_backward(&stream, 1),
"Shouldn't be possible to seek backward on an invalid stream.");
TEST(old_position == stream.position,
"Position shouldn't be affected when seeking backward on an invalid "
"stream (%lu -> %lu)",
old_position, stream.position);
stream_free(&stream);
}
// Valid streams
{
stream_t stream = {0};
stream_init_file(&stream, valid_filename, valid_fp);
u64 old_position = stream.position;
TEST(stream_seek_forward(&stream, 1),
"Okay to seek forward on a valid stream.");
TEST(old_position < stream.position,
"Position should be greater than before when seeking forward on a "
"valid stream (%lu -> %lu)",
old_position, stream.position);
TEST(stream_seek_backward(&stream, 1),
"Okay to seek backward on a valid stream.");
TEST(old_position == stream.position,
"stream_seek_forward and stream_seek_backward are inverse operations");
u64 forward_offset = stream_seek_forward(&stream, ARRSIZE(words_text) * 2);
TEST(forward_offset < ARRSIZE(words_text) * 2,
"Forward seeking by offsets greater than file size clamps (%lu "
"clamps to %lu)",
ARRSIZE(words_text) * 2, forward_offset);
u64 file_size = stream_size(&stream);
u64 backward_offset = stream_seek_backward(&stream, file_size + 1);
TEST(backward_offset == file_size,
"Backward seeking by offsets greater than file size clamps (%lu "
"clamps to %lu)",
file_size + 1, backward_offset);
TEST(stream.position == 0,
"Clamped forward and clamped backward seeking "
"leads to start of stream (position=%lu)",
stream.position);
i64 r_forward_offset = (rand() % (file_size - 1)) + 1;
i64 r_backward_offset = (rand() % (file_size - 1)) + 1;
while (r_backward_offset >= r_forward_offset)
r_backward_offset = (rand() % (file_size - 1)) + 1;
TEST(stream_seek(&stream, r_forward_offset) == (u64)r_forward_offset,
"Seeking by a random positive offset (%lu) is valid",
r_forward_offset);
TEST(stream_seek(&stream, -r_backward_offset) == (u64)r_backward_offset,
"Seeking backward by a random negative offset (%lu) is valid",
r_backward_offset);
TEST(
(i64)stream.position == r_forward_offset - r_backward_offset,
"Stream position (%lu) is exactly shifted by seeking offsets described "
"above.",
stream.position);
stream_free(&stream);
}
TEST_END();
}
void stream_test_substr(void)
{
TEST_START();
u64 size = rand() % (ARRSIZE(words_text) - 1);
u64 position = ARRSIZE(words_text) - size - 1;
// Taking substrings of invalid streams
{
stream_t stream = {0};
stream_init_file(&stream, NULL, invalid_fp);
// Relative
{
sv_t result = stream_substr(&stream, size);
TEST(result.data == NULL && result.size == 0,
"Relative substring with size %lu on invalid stream should be NULL",
size);
}
// Absolute
{
sv_t result = stream_substr_abs(&stream, position, size);
TEST(result.data == NULL && result.size == 0,
"Absolute substring @%lu with size %lu on invalid stream should be "
"NULL",
position, size);
}
stream_free(&stream);
}
// Taking substrings of valid streams
{
stream_t stream = {0};
stream_init_file(&stream, valid_filename, valid_fp);
// Absolute
{
sv_t result = stream_substr_abs(&stream, position, size);
TEST(result.data && result.size,
"Absolute substring @%lu with size %lu on valid stream should be "
"nonzero",
position, size);
TEST(result.size == size, "Substring has right size (%lu)", result.size);
sv_t expected = sv_substr(SV_AUTO(words_text), position, size);
TEST(strncmp(result.data, expected.data, result.size) == 0,
"Expect the substring to be the same as the data we put in");
}
// Relative
{
sv_t result = stream_substr(&stream, size);
TEST(result.data && result.size,
"Relative substring with size %lu should be nonzero", size);
TEST(result.size == size, "Substring has right size (%lu)", result.size);
sv_t expected = sv_truncate(SV_AUTO(words_text), size);
TEST(strncmp(result.data, expected.data, result.size) == 0,
"Expect the substring to be the same as the data we put in");
}
// Relative substring after seeking
{
// Shift forward to a random position
assert(stream_seek_forward(&stream, position)); // not a test
sv_t result = stream_substr(&stream, size);
TEST(result.data && result.size,
"Relative substring with size %lu after seeking %lu bytes should be "
"nonzero",
size, position);
TEST(result.size == size, "Substring has right size (%lu)", result.size);
sv_t expected = sv_substr(SV_AUTO(words_text), position, size);
TEST(strncmp(result.data, expected.data, result.size) == 0,
"Expect the substring to be the same as the data we put in");
// Shift back to the original position.
assert(stream_seek_backward(&stream, position)); // not a test
}
// Bad substrings
{
{
sv_t result = stream_substr_abs(&stream, stream_size(&stream), 100);
TEST(!result.data && !result.size,
"Absolute substring at %lu of 100 bytes is invalid",
stream_size(&stream));
}
assert(stream_seek_forward(&stream, stream_size(&stream))); // not a test
{
sv_t result = stream_substr(&stream, 100);
TEST(!result.data && !result.size,
"Relative substring with size 100 after seeking %lu bytes is "
"invalid",
stream.position);
}
}
stream_free(&stream);
}
TEST_END();
}
void stream_test_till(void)
{
TEST_START();
TODO("Not implemented");
}
void stream_test_while(void)
{
TEST_START();
TODO("Not implemented");
}
void stream_test_line_col(void)
{
TEST_START();
TODO("Not implemented");
}
MAKE_TEST_SUITE(STREAM_SUITE, "Stream Tests",
MAKE_TEST_FN(stream_test_prologue),
MAKE_TEST_FN(stream_test_string),
MAKE_TEST_FN(stream_test_file),
MAKE_TEST_FN(stream_test_peek_next),
MAKE_TEST_FN(stream_test_seek),
MAKE_TEST_FN(stream_test_substr),
MAKE_TEST_FN(stream_test_epilogue), );
/* Copyright (C) 2026 Aryadev Chavali
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License Version 2 for
* details.
* You may distribute and modify this code under the terms of the GNU General
* Public License Version 2, which you should have received a copy of along with
* this program. If not, please go to <https://www.gnu.org/licenses/>.
*/

View File

@@ -1,45 +0,0 @@
/* test_sv.c: String View tests
* Created: 2026-02-05
* Author: Aryadev Chavali
* License: See end of file
* Commentary:
*/
#include <assert.h>
#include <malloc.h>
#include "./data.h"
#include "./test.h"
void sv_copy_test(void)
{
TEST_START();
static_assert(ARRSIZE(unique_words) > 3, "Expected at least 3 unique words");
for (u64 i = 0; i < 3; ++i)
{
sv_t word = SV((char *)unique_words[i], strlen(unique_words[i]));
sv_t copy = sv_copy(word);
TEST(word.data != copy.data, "%p != %p", word.data, copy.data);
TEST(word.size == copy.size, "%lu == %lu", word.size, copy.size);
TEST(strncmp(word.data, copy.data, copy.size) == 0, "`%s` == `%s`",
word.data, copy.data);
// NOTE: Okay to free since we own copy.
free((void *)copy.data);
}
}
MAKE_TEST_SUITE(SV_SUITE, "String View Tests", MAKE_TEST_FN(sv_copy_test), );
/* Copyright (C) 2026 Aryadev Chavali
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License Version 2 for
* details.
* You may distribute and modify this code under the terms of the GNU General
* Public License Version 2, which you should have received a copy of along with
* this program. If not, please go to <https://www.gnu.org/licenses/>.
*/

View File

@@ -1,41 +0,0 @@
/* test_symtable.c: Symbol table tests
* Created: 2026-02-05
* Author: Aryadev Chavali
* License: See end of file
* Commentary:
*/
#include "./data.h"
#include "./test.h"
void symtable_test(void)
{
TEST_START();
sym_table_t table = {0};
sym_table_init(&table);
for (u64 i = 0; i < ARRSIZE(words); ++i)
sym_table_find(&table, SV((char *)words[i], strlen(words[i])));
TEST(table.count == ARRSIZE(unique_words), "%lu == %lu", table.count,
ARRSIZE(unique_words));
sym_table_free(&table);
TEST_END();
}
MAKE_TEST_SUITE(SYMTABLE_SUITE, "Symbol Table Tests",
MAKE_TEST_FN(symtable_test), );
/* Copyright (C) 2026 Aryadev Chavali
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License Version 2 for
* details.
* You may distribute and modify this code under the terms of the GNU General
* Public License Version 2, which you should have received a copy of along with
* this program. If not, please go to <https://www.gnu.org/licenses/>.
*/

View File

@@ -8,9 +8,8 @@
#include "./data.h" #include "./data.h"
#include "./test.h" #include "./test.h"
void vec_test_concat(void) void vec_test1(void)
{ {
TEST_START();
sys_t system = {0}; sys_t system = {0};
sys_init(&system); sys_init(&system);
@@ -29,17 +28,15 @@ void vec_test_concat(void)
TEST(vec->size == ARRSIZE(words_text), "%lu == %lu", vec->size, TEST(vec->size == ARRSIZE(words_text), "%lu == %lu", vec->size,
ARRSIZE(words_text)); ARRSIZE(words_text));
TEST(strncmp((char *)vec_data(vec), words_text, vec->size) == 0, TEST(strncmp((char *)vec_data(vec), words_text, vec->size) == 0, "%d",
"%p@%lu == %p@%lu", (char *)vec_data(vec), vec->size, words_text, strncmp((char *)vec_data(vec), words_text, vec->size));
strlen(words_text));
sys_free(&system); TEST_PASSED();
TEST_END(); sys_cleanup(&system);
} }
void vec_test_gen_substr(void) void vec_test2(void)
{ {
TEST_START();
sys_t system = {0}; sys_t system = {0};
sys_init(&system); sys_init(&system);
// Generating substrings // Generating substrings
@@ -50,31 +47,37 @@ void vec_test_gen_substr(void)
{0, 16}, {0, 16},
{0, 32}, {0, 32},
{32, 64}, {32, 64},
{0, ARRSIZE(text) - 1}, {0, ARRSIZE(text)},
}; };
for (u64 i = 0; i < ARRSIZE(tests); ++i) for (u64 i = 0; i < ARRSIZE(tests); ++i)
{ {
struct Test test = tests[i]; struct Test test = tests[i];
const sv_t substr = sv_substr(SV_AUTO(text), test.start, test.size); const sv_t substr = SV((char *)text + test.start, test.size);
const u64 size = test.size / 2; const u64 size = test.size / 2;
lisp_t *lvec = make_vec(&system, size); lisp_t *lvec = make_vec(&system, size);
vec_append(as_vec(lvec), text + test.start, test.size); vec_append(as_vec(lvec), text + test.start, test.size);
TEST(as_vec(lvec)->size > size, "%lu > %lu", as_vec(lvec)->size, size); TEST(as_vec(lvec)->size > size, "%lu > %lu", as_vec(lvec)->size, size);
TEST(strncmp((char *)vec_data(as_vec(lvec)), substr.data, substr.size) == 0, TEST(strncmp((char *)vec_data(as_vec(lvec)), substr.data, substr.size) == 0,
"%p@%lu == %p@%lu", (char *)vec_data(as_vec(lvec)), as_vec(lvec)->size, "%d",
substr.data, substr.size); strncmp((char *)vec_data(as_vec(lvec)), substr.data, substr.size));
} }
sys_free(&system); TEST_PASSED();
TEST_END(); sys_cleanup(&system);
} }
MAKE_TEST_SUITE(VEC_SUITE, "Vector Tests", const test_fn TESTS_VEC[] = {
MAKE_TEST_FN(vec_test1),
MAKE_TEST_FN(vec_test2),
};
MAKE_TEST_FN(vec_test_concat), const test_suite_t VEC_SUITE = {
MAKE_TEST_FN(vec_test_gen_substr), ); .name = "Vector Tests",
.tests = TESTS_VEC,
.size = ARRSIZE(TESTS_VEC),
};
/* Copyright (C) 2026 Aryadev Chavali /* Copyright (C) 2026 Aryadev Chavali