projects/csquared/README.md
01Project · 2015
02

CSquared

03
A programming language, from scratch
05

CSquared is a small language with a big appetite: closures, prototype-style objects, and real concurrency, running on an interpreter written entirely from scratch in C#.

06

Source text goes in one end and a running program comes out the other. A reader feeds a lexer, the lexer feeds a parser, the parser feeds an evaluator. Every stage is hand-written. No generators, no grammar tools, no shortcuts.

07

I built it in 2015 to understand what a language actually is underneath the syntax. The answer turned out to be a thread I keep pulling on.

08
C#.NET 4.5LexerRecursive-descent parserTree-walking evaluatorClosuresConcurrency
notein-context.md

I built a language to learn what a language is

In 2015 I wanted to know what really happens between typing a line of code and a machine doing something about it. So I built the whole road myself: a reader that walks the source character by character, a lexer that groups those characters into tokens, a parser that folds the tokens into a tree, and an evaluator that walks the tree and makes it mean something. No parser generator. No grammar file. Every rule written by hand.

It did not stop at arithmetic. CSquared has functions that are values and remember where they were born, objects you can treat as stacks, and concurrency that actually runs in parallel: background tasks, parallel loops, and locks. The kind of features a weekend language usually never reaches.

That road, from source text to running behavior, is the same translation the rest of this site is about. Turning what a person means into what a machine does. Today you can describe a language to a model and watch it write the interpreter for you. Building one by hand, once, is how you learn what the model is quietly doing on your behalf.

  • PipelineReader → Lexer → Parser → Evaluator
  • Functionsfirst-class · closures · recursion
  • Typesint · real · string · bool · object · array · lambda
  • Concurrencyasync · parallel · lock
  • HostC# · .NET 4.5
  • WrittenBy hand · 2015
architecturecsquared.pipeline
one node, two files+lookup · setevery nodeprogram.c2source textReadercharacters · line, posLexerlexemes · pushbackParserrecursive descentASTprogram of statementsEvaluatortree-walk · evaluate(env)stdoutEnvironmentscope chain · closuresParser sideshape · the parsed fieldsEvaluator sidebehavior · Evaluate(env)
sourcephasesyntax treeenvironmentoutput

Four hand-written stages turn text into behavior. The quiet trick is on the right: every node in the tree is one partial class, its shape written on the parser side and its Evaluate(env) written on the evaluator side. Add a language feature by adding two small files, and the compiler fuses them into one.

walkthroughhow-it-runs-a-program
  1. 01
    read

    Walk the source

    The Reader streams the program file one character at a time, keeping a running line and position so any error can point straight at the place that caused it.

  2. 02
    scan

    Group into tokens

    The Lexer turns that stream of characters into lexemes: keywords, identifiers, numbers, strings, operators. A pushback stack lets it hand a token back, so the parser can peek ahead and change its mind.

  3. 03
    parse

    Fold into a tree

    The Parser is recursive descent with one token of lookahead. It reads the flat run of tokens and folds them into a tree of statements and expressions: the shape of the program, made explicit.

  4. 04
    evaluate

    Make it mean something

    The Evaluator walks that tree and asks each node to evaluate itself. A literal returns a value, an if chooses a branch, a lambda call binds arguments and runs a body. Behavior falls out of the walk.

  5. 05
    resolve

    Remember the scopes

    Every evaluation happens against an Environment: a chain of scopes linked to their parents. Lookups walk outward until they find a name, and a closure simply holds onto the scope it was born in.

notabledesign-choices

The decisions worth keeping

None of these ideas were invented here. First-class functions, closures, tree-walking evaluation are all old and well understood. What is worth showing is the shape they took when a young programmer wired them together by hand, and the one structural choice that still makes me smile.

  1. 01

    One node, two files

    Every construct in the language is a single C# class split in half. The parser side, under Parser/Types, holds only its shape: the fields it parsed. The evaluator side, under Evaluator/Evaluables, holds only its behavior: a single Evaluate method. The compiler fuses the two halves into one type.

    in contextThis is the old tension between adding new node types and adding new operations, quietly resolved with a language feature instead of a visitor or a giant switch. To add a feature you add two small files and nothing else changes. The parser never imports the runtime, and the runtime never re-describes the syntax.

  2. 02

    Functions are values, and they remember

    Lambdas are first-class. You can store them in variables, pass them as arguments, return them from other functions, and recurse through them. A declared lambda captures the environment it was written in, so an object's method can still see the object's fields long after the object was built.

    in contextFirst-class functions with real closures, hand-rolled in 2015. An object in CSquared is little more than an environment with a face, which is exactly why a method closes over its siblings. The same model JavaScript and Python lean on, reached by building it rather than reading about it.

  3. 03

    Objects you can push and pop

    An object doubles as a stack. The arrow operators add and remove anonymous members: list <- value pushes onto the end, and value <- list pops from the front. Their mirror image, ->, works from the other side. A handful of characters turn a plain object into a queue or a stack.

    in contextIt is a small, opinionated primitive, and it pays for itself immediately. The reverse-Polish calculator below is a dozen lines precisely because the operand stack is just an object with two arrows pointed at it.

  4. 04

    Real concurrency, not a toy

    async runs an expression on a background task and hands you a future; touch the value later and it blocks only if the work is still going. parallel foreach runs every iteration at once. lock guards a variable so two threads cannot tangle over it.

    in contextMost languages built for fun stop at sequential execution. CSquared shipped futures, data-parallel loops, and mutual exclusion. Under the hood each maps onto something the host already did well: async becomes a task, parallel foreach becomes a parallel query, and lock guards the variable's box directly.

resultsprograms-it-runs

Three small programs, each leaning on a different corner of the language. The output is what the interpreter actually prints.

person.c2csquared
// objects hold fields and methods that close over them
var person = {
    var name = "chris caruso";
    var age = 21;
    var greet = () => name + " · " + age;
};

println(person.greet());
chris caruso · 21
Prototype-style objects. The method greet still sees name and age because it closed over the object's scope.
rpn.c2csquared
// reverse-Polish calculator — an object is the stack
var stack = { };

stack <- 3;          // push
stack <- 4;          // push

var b; var a;
stack -> b;          // pop  4
stack -> a;          // pop  3

stack <- a + b;      // push the sum

var result;
stack -> result;
println(result);
7
The arrow operators turn a plain object into an operand stack. Real input parsing is one foreach away.
parallel.c2csquared
// four jobs, one counter, no tangles
var counter = 0;

var work = () => {
    var i = 0;
    while (i < 250000) {
        lock (counter) { counter = counter + 1; }
        i = i + 1;
    }
};

var jobs = { };
jobs <- work; jobs <- work;
jobs <- work; jobs <- work;

parallel foreach (job in jobs) { job(); }

println(counter);
1000000
parallel foreach runs every job at once; lock keeps the shared counter honest. The total always lands on a million.
try itrun-it-yourself.c2

The whole interpreter, reader to evaluator, ported to run in your browser. Pick an example or write your own, then press Run. Nothing leaves this page.

fib.c2csquared
Runs entirely in your browser. No operator precedence; operators read right to left.
Press Run to execute the program.