Skip to content

Milestones

List view

  • # Static Typing for Compose ## Goal Introduce **static type checking with local type inference**, performed **before runtime**, without changing the interpreter execution model yet. The system should: * Catch type errors before execution * Support constraint-based inference inside functions * Produce rich diagnostics linked to spans (for LSP & errors) * Preserve the current AST + interpreter architecture --- ## High-level design ### Typing model * **Statically typed language** * **Local inference only** * Function parameters and return types must be annotated * No inference across function boundaries * **Closures** * Argument and return types inferred from usage * **Constraint-based inference** * Types are inferred by collecting and resolving constraints * Ambiguous expressions are resolved using contextual constraints (e.g. return type) --- ## Language rules ### Functions * Every function must declare: * Parameter types * Return type * The function body is type-checked independently * Return expressions must unify with the declared return type ```compose fn sum(a: Int, b: Int) -> Int { a + b } ``` --- ### Expressions & blocks * Every expression has: * An **original type** (before coercions) * A **final type** (after coercions / context) * Blocks: * If the last expression has **no trailing semicolon**, its value is returned * If it **has a trailing semicolon**, the block returns `()` This distinction must be preserved for diagnostics. ```compose { foo(); // produces T, coerced to () bar() // produces U, returned } ``` If a trailing semicolon causes a type mismatch, the error should suggest removing it. --- ### Branching (`if`, `match`) * Branches must unify to a single type unless context restricts it * If the surrounding context expects `T`, branch results may be coerced to `T` * Flow-sensitive narrowing is supported: ```compose if (v is Some(s)) { s.len() // s is available here } ``` --- ### Generics * Composite types (e.g. `List`, `Map`) are generic * Type parameters may be inferred locally ```compose fn f() -> List<Int> { List::empty() } ``` --- ### Interfaces * Interfaces introduce **constraints**, not concrete types * Values may be **erased to an interface type** ```compose let d: Drawable = Point::origin(); ``` * Interface values in collections are allowed * Method calls on interface values introduce trait constraints --- ## Type system representation ### Core entities * `TypeId` * `TypeVarId` * `Constraint` * `ConstraintSource` * `ExprId` ### Expression identity * Each expression is assigned an `ExprId` * Mapping: * `SpanId -> ExprId` * `ExprId -> TypeInfo` * Syntax tree remains unchanged * Semantic data is stored in side tables --- ### Constraint sources Each constraint records **where it came from**: * Operator usage (`+`, `>`, etc.) * Function or method calls * Interface method calls * Branch joins * Assignment * Return expressions * Semicolon coercions * Explicit type annotations This enables Rust-like diagnostics: ```text error: mismatched types | 2 | vec.push(1) | inferred Vec<Int> here 3 | vec.push(false) | expected Int, found Bool ``` --- ### Cycles & resolution * Constraints may not form chains or cycles * Resolution uses unification with: * Occurs-check (detecting constraint cycles) * Deferred resolution for unresolved type variables * Cycles that cannot be resolved are reported with full provenance --- ## Outputs of the type checker The static typing pass produces: * `ExprId -> TypeInfo` * original type * final (coerced) type * `ExprId -> ConstraintSources` * A list of diagnostics with spans * A stable data model usable by: * Interpreter * LSP * Future optimizations

    No due date
    0/2 issues closed
  • # Classes & interfaces in Compose Classes are defined with the `class` keyword. Classes can contain fields, methods and associated functions. A function is a method if it takes `self` as its first parameter. Functions and fields are private by default and can be made public with the `pub` keyword. ```compose pub class Point { pub x, pub y, pub fn origin() { new Point { x: 0, y: 0 } } pub fn magnitude(self) { math.sqrt(self.x.pow(2) + self.y.pow(2)) } } ``` Instances of classes are created with the `new` keyword. All fields must be specified. ```compose let p = new Point { x: 3, y: 4 }; ``` Since all fields must be specified, this means classes with private fields can only be created within their definition. To allow a class with private fields to be initialised outside the definition, it must expose an associated (factory) function that returns an instance. Fields can be accessed with field access syntax (`.`). Private fields can only be accessed within the type definition. ```compose pub class PositiveInt { inner, /// Create a positive int from the passed in value. /// /// Panics if the value is a negative int, or of a different type. pub fn create(value) { match (value) { Int v if v > 0 => new PositiveInt { inner: v }, Int v => panic("Cannot create a PositiveInt from an int <= 0: " + v.to_string()), other_ty => panic("Cannot create a PositiveInt from type: ", other_ty.ty().to_string()), } } pub fn get(self) { self.inner // accessing inner is allowed here } } let p = PositiveInt::create(2); p.inner; // Error: Not allowed to access private field `inner` outside its definition ``` Associated functions are called with path access (`::`) on the type. ```compose let origin = Point::origin(); ``` Methods can be called with method calling syntax as well as associated function syntax. ```compose let p = new Point { x: 3, y: 4 }; let mag1 = p.magnitude(); let mag2 = Point::magnitude(p); ``` ## Interfaces Interfaces can be defined to support polymorphic patterns. ```compose pub interface Drawable { fn draw(self); } pub class Point: Drawable { pub x, pub y, fn Drawable::draw(self) { // ... } } ``` Interface methods can be called like regular methods: ```compose let p = Point::origin(); p.draw(); ``` #### Name conflict resolution Take the following example: ```compose pub class Point: Drawable, ColorDrawable { pub fn draw(self) { // plain method } fn Drawable::draw(self) { // Drawable interface method } fn ColorDrawable::draw(self) { // ColorDrawable interface method } } ``` Calling `point.draw()` is ambiguous in this instance and will result in an error. This is resolved by using fully qualified names: ```compose point.Point::draw(); point.Drawable::draw(); point.ColorDrawable::draw(); // or as associated functions Point::draw(point); Drawable::draw(point); ColorDrawable::draw(point); ``` > Note: the fully qualified names do need to be in scope. - `fn method(self)` defines a plain class method - `fn Interface::method(self)` defines an interface method - Plain methods do not automatically satisfy interface requirements - Interface methods do not shadow plain methods - Conflicts can be resolved by using fully qualified names. ## Extension implementations Sometimes it makes sense to implement methods and associated functions outside the initial definition. To accomplish this Compose allows extending types. To extend a type, make sure it is imported and then create an `extend` block: ```compose pub extend Point as PointTransformationExt { fn scale(self, factor) { // ... } } ``` This extension then becomes available on instances of `Point`. To use these extension methods from other modules the extension first needs to be imported: ```compose import "point_extensions.cmps": PointTransformationExt; let point = new Point { x: 1, y: 2 }; point.scale(2); assert::eq(point.x, 2); // or as an associated function PointTransformationExt::scale(point, 2); ``` ## Extending types with interface implementations Sometimes it makes sense to implement an interface for a type outside its definition. Albeit implementing an interface for a type you don't own, or simply for organisation. Extension blocks can implement interfaces for other types: ```compose pub extend Point: Drawable as DrawablePointExt { fn draw(self) { // ... } } let point = new Point { ... }; point.draw(); // if there are no name conflicts // or point.DrawablePointExt::draw(); // fully unambiguous ``` ## Interface witnesses Because Compose supports implementing interfaces multiple times for types, disambiguation for which implementation to use when a type erased value is passed to a function is needed. ```compose pub fn use_drawable(drawable) { drawable.draw(); } pub class Point: Drawable { fn Drawable::draw(self) { "standard" } } pub extend Point: Drawable as FancyDrawablePointExt { fn draw(self) { "fancy" } } pub extend Point: Drawable as BasicDrawablePointExt { fn draw(self) { "basic" } } let p = new Point; use_drawable(p); // Which implementation should use_drawable pick? ``` In this case an interface witness can be used to specify which implementation to use. If the type itself implements an interface, and no witness is specified the base implementation will be used. ```compose use_drawable(p as FancyDrawablePointExt); // "fancy" use_drawable(p as BasicDrawablePointExt); // "basic" use_drawable(p): // "standard" ```

    No due date