<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Pranav Desai</title>
    <description>The latest articles on DEV Community by Pranav Desai (@prnv_dsi).</description>
    <link>https://dev.to/prnv_dsi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3955719%2Fdb8bd48c-a62f-4f29-8e4d-b15818393703.png</url>
      <title>DEV Community: Pranav Desai</title>
      <link>https://dev.to/prnv_dsi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/prnv_dsi"/>
    <language>en</language>
    <item>
      <title>Needed 1+1, Built a Functional Programming Language</title>
      <dc:creator>Pranav Desai</dc:creator>
      <pubDate>Thu, 17 Sep 2026 16:49:53 +0000</pubDate>
      <link>https://dev.to/prnv_dsi/needed-11-built-a-functional-programming-language-2mdi</link>
      <guid>https://dev.to/prnv_dsi/needed-11-built-a-functional-programming-language-2mdi</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/PranavDesai-Git/graphLang" rel="noopener noreferrer"&gt;graphLang&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I was given a data structures problem of converting an arithmetic expression into a binary tree.&lt;br&gt;
Naturally, I decided to build an evaluator.&lt;/p&gt;

&lt;p&gt;A few days later I implemented closures, a garbage collector, a custom memory allocator, a REPL, an FFI, and a whole bunch of other stuff in C.&lt;/p&gt;

&lt;h3&gt;
  
  
  THE DATA STRUCTURES ASSIGNMENT
&lt;/h3&gt;

&lt;p&gt;The problem was: &lt;br&gt;
Evaluate 1 + 1 + 1 to 3 using a binary tree.&lt;/p&gt;

&lt;p&gt;How do we get there?&lt;/p&gt;

&lt;p&gt;Well, we first form our tree for 1+1+1&lt;/p&gt;

&lt;pre&gt;     (+)
     / \
   (+) (1)
   / \
 (1) (1)
&lt;/pre&gt;

&lt;p&gt;The operator becomes the root, with its two operands as children.&lt;/p&gt;

&lt;p&gt;Now let's evaluate this tree.&lt;/p&gt;

&lt;p&gt;First, we evaluate the root's left operand. It's another + expression, so we have to collapse it down to a value before the outer + can execute.&lt;/p&gt;

&lt;pre&gt;    (+)
    / \
  (2) (1)
&lt;/pre&gt;

&lt;p&gt;Then we evaluate again.&lt;/p&gt;

&lt;pre&gt;(3) &amp;lt;--- that's our result
&lt;/pre&gt;

&lt;p&gt;We just performed the equivalent of&lt;/p&gt;

&lt;pre&gt;(+ (+ 1 1) 1)
    |
    v
(+  2  1)
    |
    v
   (3)
&lt;/pre&gt;

&lt;p&gt;But notice what the evaluator had to know to do this: what + means.&lt;/p&gt;

&lt;p&gt;One way to represent this is to make every operation a different case in our expression type:&lt;/p&gt;

&lt;pre&gt;Expr ::= Add Expr Expr
       | Sub Expr Expr
       | Mul Expr Expr
       | Div Expr Expr
       | Val
&lt;/pre&gt;

&lt;p&gt;But what do these different cases actually represent?&lt;/p&gt;

&lt;p&gt;And does the evaluator really need to know the difference between Add and Sub?&lt;/p&gt;

&lt;p&gt;Then I started implementing our sum types. And when I looked at the structure:&lt;/p&gt;

&lt;pre&gt;Add: Expr x Expr  → Expr
Sub: Expr x Expr  → Expr
Mul: Expr x Expr  → Expr
Div: Expr x Expr  → Expr
&lt;/pre&gt;

&lt;p&gt;They all take two expressions and produce one expression.&lt;/p&gt;

&lt;p&gt;So why should the evaluator care whether the operation is Add, Sub, Mul, or Div?&lt;/p&gt;

&lt;p&gt;Seems like it doesn't.&lt;/p&gt;

&lt;p&gt;So now we can just represent our expression as:&lt;/p&gt;

&lt;pre&gt;Expr ::= Func Expr Expr
       | Val
&lt;/pre&gt;

&lt;p&gt;The evaluator doesn't need to know what a function does. It only needs to know how to apply one.&lt;/p&gt;

&lt;h3&gt;
  
  
  We can add vars to this. It wouldn't be a big change
&lt;/h3&gt;

&lt;p&gt;Should be a tiny addition, no problem whatsoever. I mean variables are just a hash table lookup that gives you an Expr.&lt;br&gt;
Oh wait. C doesn't have built-in hash tables.&lt;/p&gt;

&lt;p&gt;hmmm.（´-`）.｡oO( ... )&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Let's just implement a hashtable. It's a small change! m9(・∀・)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Soo...how does that work? I never implemented it before.&lt;br&gt;
I look it up on Google like a caveman and find this amazing text.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://benhoyt.com/writings/hash-table-in-c/" rel="noopener noreferrer"&gt;How to implement a hash table (in C)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So now we just got a little change in the Expr:&lt;/p&gt;

&lt;pre&gt;Expr ::= Func Expr Expr
       | Val 
       | Var 
&lt;/pre&gt;

&lt;p&gt;Would you look at that! We have variables now that can be passed to functions once evaluated. Just like (+ 1 1)&lt;/p&gt;




&lt;h3&gt;
  
  
  Actually implementing it in C
&lt;/h3&gt;

&lt;p&gt;Alright then, time to code in C with this plan. Seems simple enough. Just a tagged union.&lt;/p&gt;

&lt;pre&gt;typedef enum {
    LITERAL,
    VAR,
    FUNC,
} NodeType;

struct Node {
    struct Node *left;
    struct Node *right;

    union {
        int literal;
        char *var;
        char *func;
    } data;

    NodeType type;
};
&lt;/pre&gt;

&lt;p&gt;Right now the mem size of each ( assuming 64 bit system) is:&lt;/p&gt;

&lt;pre&gt;+------------------------+----------+
| Field                  | Size     |
+------------------------+----------+
| Left Pointer           |  8 bytes |
| Right Pointer          |  8 bytes |
| Data                   |  8 bytes |
| Type                   |  4 bytes |
| Padding                |  4 bytes |
+------------------------+----------+
| Total                  | 32 bytes |
+------------------------+----------+
&lt;/pre&gt;

&lt;p&gt;32 Bytes might not seem like a lot, but we gotta think about how this is being used. For evaluating 1+1, we would need 3 nodes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 for the operator&lt;/li&gt;
&lt;li&gt;2 for the operands&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That would be 32 x 3= &lt;strong&gt;96 bytes&lt;/strong&gt; to evaluate 1+1.&lt;/p&gt;

&lt;p&gt;But here's the thing. On my system, when you malloc() a node, it adds metadata, which takes up &lt;strong&gt;16 bytes&lt;/strong&gt; of memory!&lt;br&gt;
Bringing our total per node to 32 (node size) + 16 (malloc header) = &lt;strong&gt;48 bytes!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So for our 3 nodes to evaluate a (+) we would need 144 bytes!&lt;/p&gt;

&lt;p&gt;And notice we're going to be doing a lot of little individual allocations.&lt;br&gt;
We need a better way to allocate these nodes.&lt;/p&gt;

&lt;p&gt;We clearly need a custom allocator.&lt;/p&gt;

&lt;p&gt;So, I look up what allocator we can use, again like a caveman, and I decide I will be writing an Arena Allocator.&lt;/p&gt;




&lt;h3&gt;
  
  
  Arena Allocator
&lt;/h3&gt;

&lt;p&gt;The idea of an arena allocator is pretty simple. All you do is take a big chunk of memory at the start, allocate stuff yourself, and then at the end just free the entire block.&lt;/p&gt;

&lt;p&gt;So my arena allocator would just be:&lt;/p&gt;

&lt;pre&gt;    #define SIZE 1024
    Node arena[SIZE]
&lt;/pre&gt;

&lt;p&gt;And when we allocate a node, we can just keep track of the top using.&lt;/p&gt;

&lt;pre&gt;    int top = 0;
&lt;/pre&gt;

&lt;p&gt;When we want to allocate a node, we just return.&lt;/p&gt;

&lt;pre&gt;    &amp;amp;arena[top++];
&lt;/pre&gt;

&lt;p&gt;I wrote the allocator and defined a C function to allocate nodes:&lt;/p&gt;

&lt;pre&gt;Node *allocNode();
&lt;/pre&gt;

&lt;p&gt;It was great! Now moving on to actually calling the functions.&lt;/p&gt;

&lt;p&gt;Then I realized&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We can store vars and funcs in the same environment! That means functions can just be values too (°◇°)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Remember the hashtable we created earlier? It's time to upgrade it.&lt;/p&gt;




&lt;h3&gt;
  
  
  Making the Env Table
&lt;/h3&gt;

&lt;p&gt;In the Env table we are storing two things. Vars and&lt;br&gt;
functions.&lt;/p&gt;

&lt;p&gt;But what are functions?&lt;br&gt;
As far as our evaluator is concerned, it is a thing&lt;br&gt;
that consumes arguments on one side and spits out a&lt;br&gt;
result on the other.&lt;/p&gt;

&lt;pre&gt;    (func node)
    /         \
 (arg 1)    (arg 2)
&lt;/pre&gt;

&lt;p&gt;The initial idea was that they would just be pointers to C&lt;br&gt;
funcs. Seems simple enough.&lt;/p&gt;

&lt;p&gt;But there is a huge problem with this: how do&lt;br&gt;
users write their own functions?&lt;/p&gt;

&lt;p&gt;But there's another problem.&lt;/p&gt;

&lt;p&gt;When a user types code into our language, it can't magically become a native C function pointer. It can only build an AST (a tree of nodes).&lt;/p&gt;

&lt;p&gt;If functions are just C pointers, they immediately become opaque values.&lt;br&gt;
What happens when a function returns another function? We need to be able to put that function back into the graph and evaluate it later. But a C function pointer isn't something our evaluator can walk through.&lt;/p&gt;

&lt;p&gt;Thus we need a type of node that tells the evaluator&lt;br&gt;
"Hey, I am a function, but my code isn't a C&lt;br&gt;
pointer; it is this tree right here." &lt;br&gt;
It needs to store the body of the function as a tree, so the evaluator can evaluate it over multiple steps. And for our purposes, that is our closure representation.&lt;/p&gt;

&lt;p&gt;Instead of a black-box C function, a closure is an&lt;br&gt;
actual node in our graph. It holds the parameter on&lt;br&gt;
one side, and the tree of operations (the body) on&lt;br&gt;
the other.&lt;/p&gt;

&lt;pre&gt;         (closure)
         /        \
 (parameter)      (body)
                 /      \
              (math)  (literal)
&lt;/pre&gt;

&lt;p&gt;(technically closures also have an environment, but we haven't gotten to local vars yet)&lt;/p&gt;

&lt;p&gt;By making the function an actual node, we can pass it&lt;br&gt;
around, return it from other functions, and evaluate&lt;br&gt;
it step-by-step whenever we want!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Now we have functions users can define themselves without ever touching the C code!!!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Alright then, let's actually implement this in C. So what do we need?&lt;/p&gt;

&lt;p&gt;If variables and functions are both values, the environment needs to map names to nodes.&lt;br&gt;
So now we can create our env entry as&lt;/p&gt;

&lt;pre&gt;typedef struct EnvEntry {
    char *key;
    Node *val;
    struct EnvEntry *next;
} EnvEntry;
&lt;/pre&gt;

&lt;p&gt;Now our hash table maps the names (key) to the Node * which is the value.&lt;/p&gt;

&lt;p&gt;But what &lt;em&gt;is&lt;/em&gt; val?&lt;/p&gt;

&lt;p&gt;Val is a Node! But our Node doesn't know about closures or native functions yet.&lt;br&gt;
So let's add those to our node definition from before.&lt;/p&gt;

&lt;pre&gt;struct Node {
    struct Node *left;
    struct Node *right;

    union {
        int literal;
        char *var;
        int index;
        char *call;
        struct Node *closure;
        struct Node *nativeFunc;
    } data;

    NodeType type;
};
&lt;/pre&gt;

&lt;p&gt;nativeFuncs are nodes representing our C funcs, while closures are user-defined funcs composed of nodes&lt;/p&gt;

&lt;p&gt;native function = opaque C implementation&lt;br&gt;
closure = language-level graph representation&lt;/p&gt;

&lt;p&gt;SO. We have assembled our pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A Memory Allocator &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;An Environment table that holds our vars, c funcs and user-defined funcs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;An evaluator that walks down the program and applies functions&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Let's execute our first program!!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Right now we don't have a lexer/parser yet, so we can just hand-construct our AST.&lt;/p&gt;

&lt;p&gt;What better program to test than the fibonacci sequence!&lt;/p&gt;

&lt;p&gt;So I wrote the program. Typed in fib(5).&lt;/p&gt;

&lt;p&gt;and...&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IT CRASHED&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  UPGRADING THE MEMORY ALLOCATOR
&lt;/h3&gt;

&lt;p&gt;Why? Because our fib(5) spawned &lt;strong&gt;13k nodes&lt;/strong&gt;. But our allocator size is only 1024 nodes total!&lt;br&gt;
You might think "Okay, it's obvious: reallocate the block and grow the size. Have it be a dynamic array"&lt;br&gt;
And that's exactly where the problem lies. The thing is, all of our nodes are pointing to each other inside this memory block.&lt;br&gt;
When we realloc it with an increased size, it might get moved to a new memory address.&lt;br&gt;
Completely breaking all of our pointers and causing a segfault! &lt;br&gt;
How do we tackle this problem? &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Instead of growing one allocator, we can just chain multiple blocks together!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We can build a linked list of our allocated blocks. When we run out of space in one block, we just allocate a new one and point to it! We don't have to move any data!&lt;/p&gt;

&lt;p&gt;This is called a chunk allocator! Each of our blocks is a chunk that stores the memory, and we chain them together like a linked list with a next pointer.&lt;/p&gt;

&lt;pre&gt;typedef struct Chunk {
    Node nodes[CHUNK_SIZE];
    struct Chunk *next;
} Chunk;
&lt;/pre&gt;

&lt;p&gt;We can keep track of the first chunk and the chunk we're currently allocating from.&lt;/p&gt;

&lt;p&gt;We have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A Chunk Memory Allocator (new) &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;An Environment table that holds both our vars, c funcs and user-defined funcs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;An Evaluator that just walks down the program recursively and reduces them using env lookup&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Let's execute our first program again&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and...&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It works!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We get the result 5! 3+2 is 5. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But something weird happened&lt;/strong&gt;. It was using 1.32 mb of memory.&lt;br&gt;
That's weird, because fib(5) isn't a complex operation.&lt;/p&gt;

&lt;p&gt;So I ran fib(10)&lt;br&gt;
It took 40 mb of RAM!!&lt;br&gt;
Okay, well that's weird.&lt;br&gt;
So I had to test it out. I ran a benchmark.&lt;/p&gt;

&lt;pre&gt;RAM (GB) vs fib(n)

12.29 GB ┤  
11.34 GB ┤                               ╭───
10.40 GB ┤                        ╭──────╯
 9.45 GB ┤                  ╭─────╯
 8.51 GB ┤              ╭───╯
 7.56 GB ┤             ╭╯
 6.62 GB ┤            ╭╯
 5.67 GB ┤           ╭╯
 4.73 GB ┤         ╭─╯
 3.78 GB ┤        ╭╯
 2.84 GB ┤       ╭╯
 1.89 GB ┤      ╭╯
 0.95 GB ┤     ╭╯
 0.00 GB ┼─────╯
         -----------------------------------
         5    10        20                  40
&lt;/pre&gt;

&lt;p&gt;Fib(40) literally took 12+ GIGABYTES before hitting an OOM and crashing.&lt;/p&gt;

&lt;p&gt;Why? Because it spawns approximately 1.3 Billion nodes.&lt;/p&gt;

&lt;p&gt;At 48 bytes per node, that's ~62.4 GB worth of node allocations.&lt;/p&gt;

&lt;p&gt;The problem is... &lt;/p&gt;

&lt;p&gt;We're allocating nodes but never freeing them once their use is over.&lt;/p&gt;

&lt;p&gt;To tackle this problem, I had to build a garbage collector.&lt;/p&gt;




&lt;h3&gt;
  
  
  BUILDING THE GARBAGE COLLECTOR
&lt;/h3&gt;

&lt;p&gt;What does it mean to collect garbage?&lt;/p&gt;

&lt;p&gt;Basically, we need to get rid of nodes that the program can no longer reach.&lt;/p&gt;

&lt;p&gt;When we evaluate 1+1+1 the evaluator does this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;ol&gt;
&lt;li&gt;Builds the ast
&lt;pre&gt;(+)
/ \
(1) (+)
/ \
(1) (1)
&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;&lt;ol&gt;
&lt;li&gt;Evaluates left. Left is already a literal. Moves on to right.&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;li&gt;&lt;ol&gt;
&lt;li&gt;Right is a function. So it gets evaluated first. And we mutate the tree.&lt;/li&gt;
&lt;/ol&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;pre&gt;      (+)
      / \
    (1) (2)
&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;But what happens to the two 1s?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They are left sitting in the allocator! They aren't freed until the end of the program!&lt;/p&gt;

&lt;p&gt;What we need our garbage collector to do is start from our roots and mark every node that can still be reached.&lt;/p&gt;

&lt;p&gt;Now if you notice, when we've reduced a node to a literal, its old children are no longer reachable through that node!&lt;/p&gt;

&lt;p&gt;What we could do is, once we mutate to a literal, just null out both children. Then the garbage collector can never reach those old nodes from this part of the graph.&lt;/p&gt;

&lt;p&gt;This is called the mark phase. The garbage collector starts from its roots and marks every node it can reach.&lt;/p&gt;

&lt;p&gt;Once marking is done, we have the garbage collector go through our chunks and just check if a node is not marked and add it to a linked list called the freeList.&lt;/p&gt;

&lt;p&gt;Now when we need a new node, we can reuse one of those nodes instead of allocating another one.&lt;/p&gt;

&lt;p&gt;If freeList is empty, only then do we allocate a new node from the current chunk.&lt;br&gt;
Otherwise, we just pop the head and reuse that memory.&lt;/p&gt;

&lt;p&gt;This lets us recycle our nodes effectively!!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Let's run the benchmark after applying the gc:&lt;/p&gt;

&lt;pre&gt;  RAM w/ GC (MB) vs fib(n)
      1.7200 MB ┼
      1.5886 MB ┤                               ╭───
      1.4571 MB ┤                          ╭────╯
      1.3257 MB ┤                    ╭─────╯
      1.1942 MB ┤               ╭────╯
      1.0628 MB ┤             ╭─╯
      0.9314 MB ┤            ╭╯
      0.7999 MB ┤          ╭─╯
      0.6685 MB ┤         ╭╯
      0.5371 MB ┤       ╭─╯
      0.4056 MB ┤      ╭╯
      0.2742 MB ┤     ╭╯
      0.1427 MB ┤ ╭───╯
      0.0113 MB ┼─╯
                 -----------------------------------
                 5    10        20                40
&lt;/pre&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;LOOK AT THAT!&lt;/strong&gt; Our ram usage went down from 12 GIGABYTES -&amp;gt; 1.7 MEGABYTES for fib(40)&lt;/p&gt;

&lt;p&gt;That's insane.&lt;/p&gt;

&lt;p&gt;But the thing is not yet solved. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;fib 40 took 6 MINUTES to evaluate&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;The mark-and-sweep garbage collector we just completed is a stop-the-world garbage collector.&lt;/p&gt;

&lt;p&gt;And the algorithm we're running is inherently exponential.&lt;/p&gt;

&lt;p&gt;So we've fixed our memory problem.&lt;/p&gt;

&lt;p&gt;But now we have a performance problem.&lt;/p&gt;

&lt;p&gt;We can tackle both of these issues.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We can implement a concurrent garbage collector&lt;/li&gt;
&lt;li&gt;And we can do something about how we're evaluating fib itself&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  What to expect in the next parts
&lt;/h3&gt;

&lt;p&gt;This has gone on long enough, so I decided to split it into parts.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how I tackled the speed issue with TCO and better evaluation&lt;/li&gt;
&lt;li&gt;implemented a lexer/parser&lt;/li&gt;
&lt;li&gt;implemented an FFI...&lt;/li&gt;
&lt;li&gt;implemented a REPL&lt;/li&gt;
&lt;li&gt;switched from pointer dereferencing...&lt;/li&gt;
&lt;li&gt;brought scuffed encapsulation into C&lt;/li&gt;
&lt;li&gt;implemented lambda functions&lt;/li&gt;
&lt;li&gt;added local vars&lt;/li&gt;
&lt;li&gt;set up stuff for a Cheney's copying collector&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What have we achieved so far?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Realized our expression type can be an Algebraic Data Type.&lt;/li&gt;
&lt;li&gt;Then realized the actual variants are the kinds of data: funcs, vars, literals&lt;/li&gt;
&lt;li&gt;Then realized vars and funcs aren't really two different things but both are just data&lt;/li&gt;
&lt;li&gt;Implemented a graph evaluator which mutates the current node after eval&lt;/li&gt;
&lt;li&gt;Implemented an Environment Table using a custom hashtable&lt;/li&gt;
&lt;li&gt;Implemented a custom chunk allocator to allocate our nodes&lt;/li&gt;
&lt;li&gt;Realized we were keeping around a ton of garbage and implemented a mark-and-sweep garbage collector&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Overall, I built a Graph Reduction engine&lt;/p&gt;

&lt;h3&gt;
  
  
  WAIT. BUT DOES IT EVALUATE 1+1
&lt;/h3&gt;

&lt;p&gt;Yeah... I mean, now it does.&lt;/p&gt;

&lt;p&gt;1+1 is in fact 2 according to graphLang! (´･ω･`)&lt;/p&gt;

&lt;p&gt;Anyhow, there's a bunch more stuff I didn't cover here. It's all in the repo.&lt;/p&gt;

</description>
      <category>computerscience</category>
      <category>github</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>I cured my adhd with ai.</title>
      <dc:creator>Pranav Desai</dc:creator>
      <pubDate>Thu, 28 May 2026 04:52:29 +0000</pubDate>
      <link>https://dev.to/prnv_dsi/i-cured-my-adhd-with-ai-1227</link>
      <guid>https://dev.to/prnv_dsi/i-cured-my-adhd-with-ai-1227</guid>
      <description>&lt;p&gt;(not literally, did fix the developer kind tho)&lt;/p&gt;

&lt;p&gt;the problem was i had too many projects. &lt;br&gt;
and i needed to manage my day properly and i didnt want to use &lt;br&gt;
obsidian or notion or any of the current tools because its just too much work&lt;br&gt;
i had to spend time outta my day to work into these systems and waste brain power on&lt;br&gt;
just planning. i didnt want to do that at all, also didnt work because forcing yourself to write down&lt;br&gt;
a notion board is genuinly the hardest thing possible. i mean you spend like 40 hours just crafting your board&lt;/p&gt;

&lt;p&gt;you would be larping productivity basically. &lt;br&gt;
(i used notion once for an internship about a week was just spent making the board&lt;br&gt;
we didnt even use it because the planning was being done in like google docs and sheets&lt;br&gt;
and someone had to move all the plans to notion just to look productive, its not worth it)&lt;br&gt;
productivity software should not be a bottleneck of productivity&lt;/p&gt;

&lt;p&gt;so i decided the best kind of productive software is the one that works with you not against you.&lt;br&gt;
i looked into what i was doing the time i was not programming. and i realized i was just spending my time &lt;br&gt;
talking to gemini/claude about my projects and during these sessions i would come up with new ideas and features&lt;br&gt;
once i got this i would go and update my planning docs.&lt;br&gt;
also these tools would lock you into their echosystems pretty hard.&lt;br&gt;
obsidian being a markdown file only tool is something that i took inspiration from.&lt;/p&gt;

&lt;p&gt;it was a very ineffecient system. and the problem was i spent half my time &lt;br&gt;
re explaining to the llms about the project even in the same chat.&lt;/p&gt;

&lt;p&gt;this also ruined my workflow. i basically live in the terminal. my entire workflow is around neovim and a bunch of terminal&lt;br&gt;
tools. the only reason i was leaving the terminal was to talk to llms which took me out of the workflow&lt;/p&gt;

&lt;p&gt;to combat this i tried using ollama and llama.cpp. self hosting was great, the lightweight models responded instantaneously&lt;br&gt;
but ollama cli and llama.cpp cli were lacking of a good interface and a workflow. &lt;br&gt;
so i looked into tui wrappers of these projects. when i used them i noticed none of them were really treating themselves like &lt;br&gt;
terminal applications properly. they were trying to be gui applications in a terminal which defeats the whole purpose.&lt;/p&gt;

&lt;p&gt;so now the solution was clear. i needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a terminal native tui interface for ollama&lt;/li&gt;
&lt;li&gt;something lightweight so it doesnt hogg up resources&lt;/li&gt;
&lt;li&gt;creates 0 friction&lt;/li&gt;
&lt;li&gt;reduces my thinking need.&lt;/li&gt;
&lt;li&gt;portable format&lt;/li&gt;
&lt;li&gt;works well with the shell terminal&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and so i came up with qlog, short for quick log.&lt;/p&gt;

&lt;p&gt;the first thing i wanted to nail in qlog was the input system.&lt;br&gt;
lotta tui tools create these input fields which are janky they are not feature rich because honestly, a single team working on&lt;br&gt;
a whole tui app cant put enough dedicated time into an input system alone as much as say neovim or emacs devs put into it. they will&lt;br&gt;
not have the huge echosystem that comes with these other tools.&lt;br&gt;
so, i reffered to the old texts. when in doubt reffer to philosophy.&lt;br&gt;
and, we got to the unix philosophy. do one thing and one thing well.&lt;br&gt;
why write a scuffed input implementation when you can hand it off to your $editor&lt;br&gt;
this let me write my prompts in the comfort of neovim with all my plugins and hand crafted config&lt;br&gt;
and honestly this is what i was expecting with the tui tools. they dont leverage the power of the terminal enough.&lt;/p&gt;

&lt;p&gt;the next thing i wanted was a good way to build a prompt.&lt;br&gt;
so i added two key things that makes qlog infinitely powerful.&lt;br&gt;
file injection and command piping.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[[filepath]] lets you inject any file you want into your prompt&lt;/li&gt;
&lt;li&gt;{{command}} lets you pass in any command's output directly into your prompt
these two features essentially give you the full power of your terminal in your tui ollama interface
why spend time copying std out when you can just exec command inside your prompt. why spend time
copying the contents of a file when you can inject it with filepath&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next thing I wanted was&lt;br&gt;
An agenda planner. once I had a conversation about a project, an llm would read the whole chat and write a good&lt;br&gt;
technical doc in markdown format so it was really portable(you could cat it or less and it would be nice)&lt;/p&gt;

&lt;p&gt;Once I had all my project technical docs and progresses I could talk to the llm and it would plan out my day and&lt;br&gt;
produce an Agenda doc. This agenda doc was also stored as a readme and you could access it with a cli command.&lt;br&gt;
You could just drop the cli command into your zshrc or bashrc and you would get reminded of your agenda.&lt;/p&gt;

&lt;p&gt;and honestly by this time qlog stopped feeling like an ai app and more of a shell utility which is exaclty what&lt;br&gt;
you would want a productivity tool to be, you want it to be invisible. the more time you are spending trying to think of productivtiy&lt;br&gt;
that much time you are not being productive&lt;/p&gt;

&lt;p&gt;The project has an MVP rn (ikik ironic that the project to fix my projects is unfinished)&lt;br&gt;
But honestly, qLog could help me complete qLog. bootstrapping productivity.&lt;/p&gt;

&lt;p&gt;Here's the github for those who want to check it out:&lt;br&gt;
&lt;a href="https://github.com/PranavDesai-Git/qLog" rel="noopener noreferrer"&gt;https://github.com/PranavDesai-Git/qLog&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>llm</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
