Skip to content
Alexander Leontiev edited this page May 13, 2026 · 8 revisions

JavaWho-Compiler Wiki

JavaWho is a small, Java-like, object-oriented language that compiles to JavaScript. It was built as a two-person course project to learn how object-oriented languages work in practice — specifically how classes, inheritance, type checking, and return-path analysis fit together inside a compiler.

The compiler is written in C# (.NET 10) and currently runs through tokenization, parsing, type checking, and code generation to JavaScript.

Contents


Building and Running

JavaWho is a .NET 10 project written in C#. There are two ways to build and run it, depending on which one you prefer.

Prerequisites

  • The .NET 10 SDK.
  • Either Visual Studio 2026 (or newer with .NET 10 support) or the dotnet CLI.

Repository layout

JavaWho-Compiler/
  JavaWhoCompiler/      The compiler itself: Tokenizer, Parser, TypeChecker
  CompilerTests/        xUnit tests for each compiler stage
  ConsolePlayground/    Console host used to drive the compiler manually
  CodeExamples/         Sample JavaWho programs (.txt)

Option 1: Visual Studio

  1. Open JavaWhoCompiler/JavaWhoCompiler.slnx in Visual Studio.
  2. Set ConsolePlayground as the startup project.
  3. Build the solution (Ctrl+Shift+B).
  4. Run with F5 (debug) or Ctrl+F5 (without debugger).

To run the test suite, open Test Explorer and run the tests in CompilerTests.

Option 2: .NET CLI

To build (from /JavaWhoCompiler):

dotnet build

To run the playground (from repository root):

dotnet run --project ConsolePlayground -- [input-path] [output-path]

To run the tests (from /CompilerTests):

dotnet test

Running a JavaWho program

The intended workflow is to load a .txt file from CodeExamples/ or create one on your own, and run it through the ConsolePlayground to generate the JavaScript for the program. Once you have the JavaScript, you can use tools like node or bun to run your program as such:

# node
node out.js
# bun
bun out.js

The ConsolePlayground will run Tokenizer.Tokenize, Parser.Parse, TypeChecker.Check, CodeGenerator.Generate, and write the generated JavaScript to a file or throw an error.


Syntax and Examples

For the full grammar, see Appendix: Formal Syntax. This section walks through the language by example, including a few cases that are intentionally rejected.

A minimal class

class Pet
{
    String Name;
    Int Age;

    init(String name, Int age)
    {
        Name = name;
        Age = age;
    }

    method Info() Void
    {
        println("Pet Name: " + Name + ", Age: " + Age);
    }
}

Pet john;
john = new Pet("john", 8);
john.Info();

Notes:

  • init is the constructor. Every class must declare one.
  • Method headers are method Name(params) ReturnType. The return type comes after the parameter list, not before the name.
  • Fields are declared at the top of the class with Type Name;.
  • All local fields must be assigned in the constructor.
  • Statements after the last class are the program's entry point.

this and self-dispatch

Inside a method, fields are accessed by their bare name. this is used to call another method on the same instance:

class Counter
{
    Int Value;

    init(Int start)
    {
        Value = start;
    }

    method Increment() Void
    {
        this.SetValue(Value + 1);
    }

    method SetValue(Int v) Void
    {
        Value = v;
    }
}

Counter c;
c = new Counter(0);
c.Increment();
c.Increment();

There is no this.Value field syntax — fields are only accessible by name from inside the class. Outside, all field access goes through methods.

Since all fields must be assigned in the constructor and there is no this.Value field syntax, shadowing a field name in a constructor is prohibited.

class Counter
{
    Int Value;

    // shadowing a field in the constructor parameters is rejected
    init(Int Value)
    {
        Value = 5;
        
        // shadowing inside the constructor body is also rejected
        Int Value;
    }
}

Inheritance and super

class Cat extends Pet
{
    Int Lives;

    init(String name, Int age, Int lives)
    {
        super(name, age);
        Lives = lives;
    }

    method LoseLives(Int livesToLose) Boolean
    {
        Int tempLives;
        tempLives = Lives - livesToLose;

        if (tempLives < 0)
        {
            return false;
        }

        Lives = tempLives;
        return true;
    }
}

Pet sam;
sam = new Cat("sam", 1, 9);
sam.Info();

Notes:

  • A subclass constructor calls super(...) to run the parent's init. The arguments are type-checked against the parent's constructor signature.
  • The super call must be first in the constructor body.
  • Pet sam; sam = new Cat(...) is allowed because Cat is a subtype of Pet. Calling sam.Info() dispatches normally.
  • A class can extend from JavaWho's built-in types String and Object.
    • By default, a class without extends will extend from Object.

super arguments are checked

super(...) is type-checked against the parent's init signature. Passing the wrong arity or wrong types is rejected:

class Cat extends Pet
{
    Int Lives;

    init(String name, Int age, Int lives)
    {
        super(name);       // rejected: Pet.init expects (String, Int)
        Lives = lives;
    }
}

The correct call matches the parent's parameter list:

init(String name, Int age, Int lives)
{
    super(name, age);
    Lives = lives;
}

Method overloading and shadowing

class Animal
{
    String name;
    init(String _name)
    {
        name = _name;
    }

    method printName() Void
    {
        println("Name: " + name);
    }

    method printInfo() Void
    {
        this.printName();
    }

    // overloaded printInfo
    method printInfo(String prefix) Void
    {
        println(prefix);
        this.printInfo();
    }
}

class Pet extends Animal
{
    String owner;
    init(String _name, String _owner)
    {
        super(_name);
        owner = _owner;
    }

    method printOwner() Void
    {
        println("Owner: " + owner);
    }

    // shadowed printInfo
    method printInfo() Void
    {
        this.printName();
        this.printOwner();
    }
}

// Dog inherits methods
class Dog extends Pet
{
    init(String _name, String _owner)
    {
        super(_name, _owner);
    }
}

Dog d;
d = new Dog("Coco", "Joe");
d.printInfo("Information:");
/* 
prints out:
    Information:
    Name: Coco
    Owner: Joe
*/

Notes:

  • No explicit override keyword. To keep things simple, users are allowed to shadow inherited methods rather than needing to explicitly override them.
  • When shadowing a method, the return type must be the same or a subtype of the original return type. This allows type guarantees when using methods while subtyping. For example:
Animal cat;
cat = new Cat();

Animal friend;
friend = cat.makeFriend();
  • Imagine Animal and Cat have a makeFriend method. Since cat is an Animal instance, it needs to be guaranteed that Cat's implementation of makeFriend returns an Animal or a subtype of Animal.

Loops, break, and string concatenation

Int a;
a = 0;

while (a < 5)
{
    println(a);
    a = a + 1;
}

String text;
text = "";

while (text != "aaaa")
{
    if (text == "aa")
    {
        println("double aa");
    }

    println(text);
    text = text + "a";
}

Notes:

  • + is overloaded: Int + Int is Int, and any combination of String with Int or String is String.
  • break exits the nearest enclosing while. There is no continue.
  • The only comparison operator is <. To express "greater than", swap the operands.

Return-path checking

A non-Void method must return on every path:

method Bad(Int x) Int
{
    if (x < 0)
    {
        return 0;
    }
    // No return on the else path -- type checker rejects this.
}

Adding the missing return makes it valid:

method Good(Int x) Int
{
    if (x < 0)
    {
        return 0;
    }
    return x;
}

Boolean conditional guards are also checked:

method Good(Int x) Int
{
    if (true)
    {
        // this code path is always hit
        return 0;
    }
}

method Bad(Int x) Int
{
    if (false)
    {
        // this code path is never hit, rejected
        return 0;
    }
}

A Void method is not required to return, but it must not return a value:

method Greet() Void
{
    println("hi");      // ok
    return;             // ok
    return println(0);  // rejected: cannot use a Void call as a value
}

Equality across unrelated types

== and != require both operands to share a type relationship. Comparing two unrelated class types is rejected at type-check time:

class Pet
{
    String Name;
    init(String name) { Name = name; }
}

class Car
{
    Int Speed;
    init(Int speed) { Speed = speed; }
}

Pet p;
Car c;
p = new Pet("rex");
c = new Car(60);

if (p == c)        // rejected: Pet and Car are unrelated types
{
    println("?");
}

Cat == Pet is allowed because the two types share a hierarchy (given Cat is a subtype of Pet).

Notably, every type's hierarchy will eventually lead back to Object. However, this does not mean that all types share a hierarchy. Two types only share a hierarchy when one is a subtype of the other.

Uninitialized variable use

JavaWho checks that a local variable has been assigned before it is read:

Int x;
println(x);  // rejected: x has been declared but never assigned

Assigning first makes it valid:

Int x;
x = 5;
println(x);

Using a variable that is not guaranteed to be assigned:

method Bad(Int x) Void
{
    Int y;
    if(x < 5)
    {
        y = 4;
    }

    println(y); //rejected: since y is not guaranteed to be assigned
}

Adding an else or assigning the variable before the if is okay:

//assignment with an else
method Good(Int x) Void
{
    Int y;
    if(x < 5)
    {
        y = 4;
    }
    else
    {
        y = 2;
    }

    println(y); //ok
}
//assignment outside the if
method Good(Int x) Void
{
    Int y;
    y = 2;

    if(x < 5)
    {
        y = 4;
    }

    println(y); //ok
}

Code Generation Notes

To allow for certain features and avoid JavaScript issues, there are some notable annotations that needed to be made that effect how JavaWho code is translated into JavaScript.

Field and class names

One major restriction of JavaWho is the lack of this.Value syntax. Since JavaScript has this feature, JavaWho generates this.Value field access in order to be more explicit in its code generation. Here is an example of this in practice:

JavaWho input:

class Pet
{
    String Name;

    init(String name)
    {
        Name = name;
    }
}

JavaScript output:

class _Pet {
    _Name;
    constructor(_name) {
        this._Name = _name;
    }
}

As you can see, this. is prepended on any instance field.

Clearly, though, another major change that is made to inputted JavaWho code is the _ prefix on class names, fields, variables, and parameters. This is in place to prevent collisions with JavaScript keywords such as let and also generated method names, which we'll get into next. This allows a user to do the following:

JavaWho input:

// let is a JavaScript keyword
Int let;
let = 5;

JavaScript output:

let _let;
_let = 5;

Method names

In order to allow for method overloading and overriding in a dynamically typed language like JavaScript, JavaWho annotates method names and method calls during type checking to ensure JavaScript uses the correct methods. Here is an example:

JavaWho input:

class Value
{
    Int Val;

    init(Int val)
    { 
        Val = val;
    }

    method add(Int other) Void
    {
        Val = Val + other;
    }

    method add(Int left, Int right) Void
    {
        Val = left + right;
    }
}

JavaScript output:

class _Value {
    _Val;
    constructor(_val) {
        this._Val = _val;
    }
    Int_add_Void(_other) {
        this._Val = this._Val + _other;
    }
    Int_Int_add_Void(_left, _right) {
        this._Val = _left + _right;
    }
}

Given method names are transformed as such:

  • Prepend original name with _ separated parameter types.
    • If parameter list is empty, prepend with Empty_.
  • Append previous step with _ then the return type.

Method calls are transformed in the same manner since JavaWho's type checker retrieves the exact method signature a method call is referencing.

String and println

JavaWho translates println statements into JavaScript's console.log. While this works perfectly in most scenarios, a not ideal output is printed when using a JavaScript String object rather than a string literal in a console.log, as JavaScript prints out the object information rather than the expected string. To avoid this, JavaWho wraps the expression argument within the console.log call in a .toString() call. For example:

JavaWho input:

String s;
s = new String("Hello, World!");

// we expect this to print "Hello, World!"
println(s);

JavaScript output:

let _s;
_s = new String("Hello, World!");
console.log((_s).toString());

Language Design

Why this language?

JavaWho started from a skeleton our class made available, called pOOP. Out of the options to select from, we picked the OOP track because object-oriented programming was the topic that felt most worth understanding from the inside. Most languages we already use are object-oriented, so the chance to take one apart — classes, inheritance, dispatch, type rules — and put it back together as a working compiler was the most interesting choice.

The README sums up the goal in one line: "(Pathetic) object-oriented programming. The goal is for us to better understand how object-oriented programming languages work."

We targeted JavaScript because it is everywhere, easy to run without a separate toolchain, and removes a lot of the noise that would otherwise distract from the parts we actually wanted to learn — the type system and the OOP semantics.

Why this design?

The design is deliberately small. We wanted enough surface area to make the OOP semantics interesting (classes, fields, methods, inheritance, super, subtyping) and as little else as we could get away with.

Here are the big parts that we took out:

  • One loop. while only. No for, no do/while.
  • One comparison operator. < only. >, <=, >= were left out because they reduce to < and add no new ideas.
  • No logical operators. No &&, ||, or !. Boolean expressions are built out of comparisons and equality.
  • No arrays, no generics, no statics. None of these change how OOP works, and all of them add real implementation cost.
  • Classes live at the top of the program. All classdefs come first, then the entry-point statements. There is no nesting and no forward-reference dance.

The two pieces we did spend our complexity budget on are the things that make OOP non-trivial:

  1. A type checker that understands subtyping. A Cat can be assigned to an Animal variable, a method can be called on a base-class type and dispatch correctly, and super(...) arguments are checked against the parent's constructor.
  2. Return-path analysis. A method declared as returning a non-Void type must return on every path. The checker walks if/else and block structure to confirm this, and it also rejects using a Void call as a value.
  3. Assign-path analysis. In a similar fashion to return-path analysis, variable assignment is checked to ensure that unassigned variables are not used in an expression. A walk through code paths allows the type-checker to understand when a variable is guaranteed to be assigned at different points in a block of code.

What is JavaWho realistic for?

Given the feature set, JavaWho is realistic for:

  • Small teaching examples that exercise OOP: linked-list classes, simple inheritance hierarchies, polymorphic dispatch through a base-class variable.
  • Programs that fit in one file and don't need I/O beyond println.
  • Anything where the point is to demonstrate type rules, subtyping, or return-path analysis.

It is not realistic for:

  • Programs that need collections — without arrays you build your own linked list.
  • Numerics-heavy work — no Float or Double.
  • File or network I/O — only println.

Known Limitations

JavaWho is intentionally small. Most of these limits are choices we made to keep the compiler simple for a course project.

Structural

  • Methods can only be defined inside classes. There are no top-level or free functions.
  • All classes must appear at the top of the file. The program is a sequence of class definitions followed by entry-point statements. You cannot interleave them.
  • One source file. There is no module or import system.
  • No static methods or static fields. Everything is per-instance.

Types and values

  • Built-in types are limited to Int, Boolean, Void, and String. Plus any class type the user defines.
  • No arrays, lists, maps, or other collections. There is no way to hold more than one value of a type without writing your own linked-list-style class.
  • No null. A class-typed variable is either uninitialized (and unreadable) or holds a real object.
  • No generics.

Expressions and operators

  • Only one comparison operator: <. There is no >, <=, or >=. To express "greater than", swap the operands.
  • Comparisons do not chain. a < b < c is a parse error. Each < expression takes exactly two operands.
  • No logical operators. There is no &&, ||, or !. Boolean expressions are built out of comparisons and == / !=.
  • No bitwise operators.
  • No compound assignment. No +=, -=, ++, --. You write x = x + 1.
  • + is the only string operation. No indexing, no length, no substring.

Control flow

  • while is the only loop. No for, no do/while, no foreach.
  • No continue. Only break.
  • No exceptions, no try/catch.
  • No switch.

OOP

  • No access modifiers. There is no public / private / protected. The "all access goes through methods" rule above is what stands in for visibility.
  • No interfaces or abstract classes. Inheritance is single-parent class inheritance only.
  • No constructor overloading. A class has exactly one init.
  • All local fields must be assigned in a constructor. To ensure fields are assigned, each class must assign its local fields in its constructor.
  • No this. method calls in constructors. To avoid complex assignment checks, this. method calls are prohibited within constructors.

Tooling

  • No optimization pass. This was an explicit non-goal listed in the original proposal.
  • No standard library beyond println.
  • No warnings, only errors. Certain detections such as unreachable code may be better off as a warning, but at the moment the compiler can only return errors.

Lessons Learned

Re-architect the type checker

If we started over, the biggest change would be the structure of the type checker. The current one works, but adding a new feature — a new statement, a new type rule, a new operator — means touching several places and re-reasoning about cases each time. We would design the next version around something easier to extend: clearer separation between expression typing, statement checking, and environment tracking, and a single place to add a new node's rule rather than several.

This was the part of the project that grew the fastest. The type checker is around 1,700 lines and the parser is around 800. As the language got more interesting (subtyping, super calls, return-path analysis, assignment analysis, void-as-value checks), the cost of every new addition kept climbing.

Need to do more rigorous testing before saying it's done

A pattern we hit repeatedly: write an implementation, write tests for the cases we thought of, watch them pass, move on — and then later, while writing an example program or thinking about a different feature, realize "wait, what happens if someone writes this?" The answer was usually "something wrong, and our tests didn't catch it."

The cases we kept missing the first time were almost always edge cases in control flow and types: a return inside one branch of an if but not the other, an expression statement whose value is Void, equality between two unrelated class types, a super call with the wrong parameters. The fix going forward is to spend more time generating "weird input" before writing the implementation, rather than after.

Things we would keep

  • Targeting JavaScript. It removed a lot of work that would not have taught us anything we cared about.
  • Keeping the language small. Every feature we cut was a feature we did not have to debug.
  • C-like syntax over S-expressions. Reading our own example programs was much easier this way.
  • Two-person team size. Communication overhead was low, and we could each own a layer (tokenizer/parser vs. type checker) without stepping on each other.

Appendix: Formal Syntax

The complete grammar for JavaWho. This is the authoritative version and is kept in sync with the parser.

var is a variable
classname is the name of a class
methodname is the name of a method
str is a string
i is an integer

comment ::= `//` (~newline)* newline
          | `/*` (~`*/`)* `*/`

type ::= `Int` | `Boolean` | `Void` | Built-in types
         classname class type; includes Object and String

comma_exp ::= [exp (`,` exp)*]

primary_exp ::=
  var | str | i | Variables, strings, and integers are
                  expressions
  `(` exp `)` | Parenthesized expressions
  `this` | Refers to my instance
  `true` | `false` | Booleans
  `println` `(` exp `)` | Prints something to the terminal
  `new` classname `(` comma_exp `)` Creates a new object

call_exp ::= primary_exp (`.` methodname `(` comma_exp `)`)*

mult_exp ::= call_exp ((`*` | `/`) call_exp)*

add_exp ::= mult_exp ((`+` | `-`) mult_exp)*

comp_exp ::= add_exp ['<' add_exp]

eq_exp ::= comp_exp [('==' | '!=') comp_exp]

exp ::= eq_exp

vardec ::= type var

stmt ::= exp `;` | Expression statements
         vardec `;` | Variable declaration
         var `=` exp `;` | Assignment
         `while` `(` exp `)` stmt | while loops
         `break` `;` | break
         `return` [exp] `;` | return, possibly void
         if with optional else
         `if` `(` exp `)` stmt [`else` stmt] |
         `{` stmt* `}` Block

comma_vardec ::= [vardec (`,` vardec)*]

methoddef ::= `method` methodname `(` comma_vardec `)` type
              `{` stmt* `}`

constructor ::= `init` `(` comma_vardec `)` `{`
                [`super` `(` comma_exp `)` `;` ]
                stmt*
                `}`

classdef ::= `class` classname [`extends` classname] `{`
             (vardec `;`)*
             constructor
             methoddef*
             `}`

program ::= classdef* stmt+  stmt+ is the entry point