Compilers

Type Systems

Static vs. dynamic typing, assignability, covariance, boxing, and top/bottom types

Static vs. dynamic typing

  Dynamic (Python, JavaScript) Static (Java, CatScript)
When checked Runtime Compile time
Variable reassignment Any type, anytime Must be compatible with declared type
Tooling Limited autocomplete Full autocomplete, refactoring, x. shows available methods
Maintenance Tests needed for safety Compiler catches type mismatches
Speed of writing Fast - no fighting the type system More overhead, but type inference (var) helps
// JavaScript  -  perfectly fine
let x = 10;
x = "foo";
console.log(x.length);  // 3
x = [1, 2, 3];
console.log(x.length);  // 3
// Java  -  compilation error
var x = 10;
x = "foo";  // ERROR: incompatible types

Dynamic typing uses duck typing - if it walks like a duck and quacks like a duck, it’s a duck. Static typing requires compile-time guarantees about what types are.


The core question: is X assignable to Y?

Almost every type system operation comes down to assignability:

var x = 10;
x = "foo";  // Is String assignable to int? No -> error
Object x = 10;
x = "foo";  // Is String assignable to Object? Yes -> fine

The Java type system exposes this directly:

String.class.isAssignableFrom(Object.class);  // false
Object.class.isAssignableFrom(String.class);  // true

Downcasting

When a variable’s compile-time type is too broad, you can downcast:

Object x = "hello";
System.out.println(((String) x).length());  // works at runtime

But downcasts can fail:

Object x = List.of(1, 2, 3);
System.out.println(((String) x).length());  // ClassCastException at runtime!

The Java type system can be subverted through downcasting. Class cast exceptions are the symptom.


Java arrays are covariant (and unsound)

String[] strings = {"a", "b", "c"};
Object[] objects = strings;       // legal  -  arrays are covariant
objects[0] = "hello";             // fine  -  String assignable to Object
objects[1] = true;                // compiles! but: ArrayStoreException at runtime

Covariance means: if String is assignable to Object, then String[] is assignable to Object[].

This is convenient - you can write a single printArray(Object[] values) method that works with any array type. But it’s unsound because you can insert incompatible types through the wider reference.

In practice, arrays are rarely mutated, so ArrayStoreException is almost never seen.


Java generics are invariant (but also broken)

List<String> strings = new ArrayList<>();
List<Object> objects = strings;  // ERROR: incompatible types

Generics learned from the array mistake - List<String> is not assignable to List<Object>.

But raw type casts bypass everything:

List<String> strings = new ArrayList<>();
List rawList = (List) strings;
rawList.add(true);  // no runtime error  -  generics are erased!

Java generics give the worst of both worlds: inconvenient invariance, and type safety can still be broken through raw types. The erasure-based implementation means the JVM doesn’t even know the generic type at runtime.


Primitive vs. reference types

  Primitives (int, boolean, float) Reference types (String, List, Integer)
Storage Value stored directly (on the stack) Pointer to heap-allocated object
Nullable Cannot be null Can be null
Performance Fast - no pointer chasing Slower - heap allocation + indirection

Boxing and autoboxing

Java automatically converts between primitives and their reference wrappers:

int x = 10;
Integer y = x;         // autoboxing: equivalent to Integer.valueOf(x)
int z = y;             // unboxing: equivalent to y.intValue()

The danger:

Integer y = null;
int z = y;             // NullPointerException!  -  unboxing null

This produces a NullPointerException on a line with no method call, which is confusing unless you understand autoboxing.


Top type and bottom type

Type systems form a hierarchy. Two special types anchor the extremes:

Type Definition Java CatScript
Top Every type is assignable to it Object object
Bottom It is assignable to every type null type null
        Object          (top  -  everything assignable to it)
       /   |   \
    String Int  List
       \   |   /
        null type       (bottom  -  assignable to everything)

You can assign null to any reference type variable in Java. The null literal has a special type (you can’t declare a variable as null type) that is assignable to all reference types.


Practical implications

The type system is a set of rules for assigning types to programming constructs and enforcing constraints. CatScript adopts these rules:

  • Statically typed - all types known at compile time
  • List types are covariant on their component type (convenient but unsound)
  • null is the bottom type (assignable to everything)
  • object is the top type (everything assignable to it)
  • Type inference supported (var x = 10 -> x is int)

Tooling > correctness?

Static type systems enable powerful tooling - type x. and see every available method. Refactoring tools, jump-to-definition, and inline documentation all depend on static type information.

The correctness guarantees matter, but the tooling is arguably the biggest practical advantage of static typing.


Summary

Concept Key point
Dynamic vs. static Runtime checking vs. compile-time checking
Assignability The fundamental question - is X assignable to Y?
Covariance String[] assignable to Object[] - convenient but unsound
Generics Invariant (List<String> not assignable to List<Object>) but still breakable via raw types
Primitives vs. references Direct values vs. heap pointers; autoboxing bridges them
Top type Object / object - everything assignable to it
Bottom type null type - assignable to everything
Boxing pitfall Unboxing null causes NullPointerException on innocent-looking lines