From 1deb35da8dfbcca81ecedced24b9a3e884ec4505 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Dec 2025 14:07:26 +0000 Subject: [PATCH 1/2] docs: add publishable spec and reference implementation docs Add a condensed language specification (SPEC.md) and release documentation (RELEASE.md) for the first public release of the AffineScript reference parser. New examples demonstrate additional language features: - effects.as: Effect handling with State and Exn - traits.as: Traits, type classes, and bounded polymorphism - refinements.as: Refinement types and compile-time verification This provides a minimal publishable package with: - Complete lexer and parser (reference implementation) - Language specification covering syntax and semantics - Comprehensive examples covering all major features --- RELEASE.md | 256 +++++++++++++++++++ SPEC.md | 538 ++++++++++++++++++++++++++++++++++++++++ examples/effects.as | 96 +++++++ examples/refinements.as | 115 +++++++++ examples/traits.as | 161 ++++++++++++ 5 files changed, 1166 insertions(+) create mode 100644 RELEASE.md create mode 100644 SPEC.md create mode 100644 examples/effects.as create mode 100644 examples/refinements.as create mode 100644 examples/traits.as diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..43c4a51 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,256 @@ +# AffineScript v0.1.0 - Reference Parser Release + +This is the first public release of AffineScript, featuring a complete specification and reference parser. + +## What's Included + +### Specification +- `SPEC.md` - Condensed language specification (essential grammar and semantics) +- `affinescript-spec.md` - Complete language specification v2.0 + +### Reference Implementation +- **Lexer** (sedlex) - Complete tokenization +- **Parser** (menhir) - Complete parsing to AST +- **AST** - Full abstract syntax tree definitions +- **Error Handling** - Structured diagnostics with source locations + +### Examples +- `examples/hello.as` - Hello World with effects +- `examples/vectors.as` - Dependent types with length-indexed vectors +- `examples/ownership.as` - Ownership and borrowing patterns +- `examples/rows.as` - Row polymorphism +- `examples/effects.as` - Effect handling and state +- `examples/traits.as` - Traits and type classes +- `examples/refinements.as` - Refinement types + +## Building from Source + +### Prerequisites + +- OCaml 5.1+ +- opam 2.1+ +- dune 3.14+ + +### Quick Start + +```bash +# Clone the repository +git clone https://github.com/hyperpolymath/affinescript.git +cd affinescript + +# Install dependencies +opam install . --deps-only + +# Build +dune build + +# Run tests +dune runtest + +# Install locally +dune install +``` + +### Using Guix (Preferred) + +```bash +guix shell -f guix.scm +dune build +``` + +### Using Nix + +```bash +nix develop +dune build +``` + +## Usage + +### Lex a File + +```bash +dune exec affinescript -- lex examples/hello.as +``` + +Output: +``` +EFFECT @ 1:1-1:7 +UPPER_IDENT(IO) @ 1:8-1:10 +LBRACE @ 1:11-1:12 +FN @ 2:3-2:5 +... +``` + +### Parse a File + +```bash +dune exec affinescript -- parse examples/hello.as +``` + +Output: +``` +{ prog_module = None; prog_imports = []; + prog_decls = + [TopEffect { ed_vis = Private; ed_name = { name = "IO"; ... }; ...}; + TopFn { fd_vis = Private; fd_total = false; fd_name = { name = "main"; ...}; ...}] +} +``` + +### Check a File (WIP) + +```bash +dune exec affinescript -- check examples/hello.as +``` + +Note: Type checking is not yet implemented in this release. + +## Implementation Status + +| Component | Status | Notes | +|-----------|--------|-------| +| Lexer | Complete | All tokens, comments, string escapes | +| Parser | Complete | Full grammar, 50+ test cases | +| AST | Complete | All language constructs | +| Diagnostics | Complete | Structured errors with locations | +| Name Resolution | Not Started | Planned for v0.2 | +| Type Checker | Not Started | Planned for v0.2 | +| Borrow Checker | Not Started | Planned for v0.3 | +| Effect Checker | Not Started | Planned for v0.3 | +| WASM Codegen | Not Started | Planned for v0.4 | + +## Language Features + +### Affine Types (Ownership) + +```affinescript +type File = own { fd: Int } + +fn processFile(file: own File) -> () / IO { + // file is consumed here - cannot use after + close(file) +} + +fn readFile(file: ref File) -> String / IO { + // Borrows file - doesn't consume it + read(file) +} +``` + +### Dependent Types + +```affinescript +type Vec[n: Nat, T: Type] = + | Nil : Vec[0, T] + | Cons(T, Vec[n, T]) : Vec[n + 1, T] + +// Type system prevents calling on empty vectors +total fn head[n: Nat, T](v: Vec[n + 1, T]) -> T / Pure { + match v { Cons(h, _) => h } +} +``` + +### Row Polymorphism + +```affinescript +// Works on any record with 'name' field +fn greet[..r](person: {name: String, ..r}) -> String / Pure { + "Hello, " ++ person.name +} + +// Both work: +greet({name: "Alice", age: 30}) +greet({name: "Bob", role: "Engineer"}) +``` + +### Extensible Effects + +```affinescript +effect State[S] { + fn get() -> S; + fn put(s: S); +} + +fn counter() -> Int / State[Int] { + let n = State.get(); + State.put(n + 1); + n +} + +// Handle the effect +handle counter() with { + return x => x, + get() => resume(0), + put(s) => resume(()) +} +``` + +## Running Tests + +```bash +# All tests +dune runtest + +# With verbose output +dune runtest --force --verbose + +# Specific test suite +dune exec test/test_main.exe -- test "Lexer" +dune exec test/test_main.exe -- test "Parser" +``` + +## Documentation + +```bash +# Generate documentation +dune build @doc + +# View in browser +open _build/default/_doc/_html/index.html +``` + +## File Structure + +``` +affinescript/ +├── lib/ # Core compiler library +│ ├── ast.ml # Abstract syntax tree +│ ├── token.ml # Token definitions +│ ├── lexer.ml # Sedlex-based lexer +│ ├── parser.mly # Menhir grammar +│ ├── parse.ml # Parser wrapper +│ ├── span.ml # Source locations +│ └── error.ml # Diagnostics +├── bin/ # CLI executable +│ └── main.ml # Command-line interface +├── test/ # Test suite +│ ├── test_lexer.ml # Lexer tests +│ └── test_parser.ml # Parser tests +├── examples/ # Example programs +├── wiki/ # Documentation +├── SPEC.md # Condensed specification +├── affinescript-spec.md # Full specification +└── RELEASE.md # This file +``` + +## Contributing + +Contributions welcome! Areas of interest: + +1. **Type Checker** - Bidirectional type checking with dependent types +2. **Borrow Checker** - Ownership verification +3. **Effect Checker** - Effect tracking and handling +4. **Standard Library** - Core types and functions +5. **WASM Backend** - Code generation + +See `affinescript-spec.md` Part 10 for implementation guidance. + +## License + +MIT License - see LICENSE file. + +## Links + +- Repository: https://github.com/hyperpolymath/affinescript +- Specification: See `SPEC.md` or `affinescript-spec.md` +- Issues: https://github.com/hyperpolymath/affinescript/issues diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..9c6f878 --- /dev/null +++ b/SPEC.md @@ -0,0 +1,538 @@ +# AffineScript Language Specification v0.1 + +A programming language combining affine types, dependent types, row polymorphism, and extensible effects. + +## Overview + +AffineScript is designed for safe, efficient systems programming with: + +- **Affine Types**: Rust-style ownership ensuring memory safety without GC +- **Dependent Types**: Types that depend on values (e.g., `Vec[n, T]`) +- **Row Polymorphism**: Extensible records with compile-time field tracking +- **Extensible Effects**: User-defined, tracked side effects +- **WASM Target**: Compiles to WebAssembly for portable execution + +## 1. Lexical Grammar + +### 1.1 Identifiers + +``` +lower_ident = [a-z][a-zA-Z0-9_]* +upper_ident = [A-Z][a-zA-Z0-9_]* +row_var = ".." lower_ident +``` + +### 1.2 Keywords + +``` +fn let mut own ref type struct enum trait impl effect handle +resume handler match if else while for return break continue in +true false where total module use pub as unsafe assume transmute +forget Nat Int Bool Float String Type Row +``` + +### 1.3 Literals + +``` +int_lit = [0-9]+ | 0x[0-9a-fA-F]+ | 0b[01]+ | 0o[0-7]+ +float_lit = [0-9]+ "." [0-9]+ ([eE][+-]?[0-9]+)? +char_lit = "'" (escape | [^'\\]) "'" +string_lit = '"' (escape | [^"\\])* '"' +bool_lit = "true" | "false" +unit_lit = "()" +``` + +### 1.4 Operators + +``` +Arithmetic: + - * / % +Comparison: == != < > <= >= +Logical: && || ! +Bitwise: & | ^ ~ << >> +Type-level: -> => : / +Special: \ (row restriction) +``` + +### 1.5 Quantity Annotations + +``` +0 Erased (compile-time only) +1 Linear (use exactly once) +ω Unrestricted (use any number of times) +``` + +## 2. Syntactic Grammar + +### 2.1 Program Structure + +```ebnf +program = [module_decl] {import_decl} {top_level} +top_level = fn_decl | type_decl | trait_decl | impl_block | effect_decl +``` + +### 2.2 Type Declarations + +```ebnf +type_decl = [visibility] "type" UPPER_IDENT [type_params] "=" type_body + +type_params = "[" type_param {"," type_param} "]" +type_param = [quantity] IDENT [":" kind] + +kind = "Type" | "Nat" | "Row" | "Effect" | kind "->" kind + +type_body = type_expr (* alias *) + | struct_body (* record *) + | enum_body (* variant *) + +struct_body = "{" field {"," field} "}" +enum_body = ["|"] variant {"|" variant} +variant = UPPER_IDENT ["(" type_expr {"," type_expr} ")"] [":" type_expr] +``` + +### 2.3 Type Expressions + +```ebnf +type_expr = type_atom + | type_expr "->" type_expr ["/" effects] (* function *) + | "(" [quantity] IDENT ":" type_expr ")" "->" type_expr ["/" effects] + | type_expr "where" "(" predicate ")" (* refinement *) + +type_atom = PRIM_TYPE | UPPER_IDENT | TYPE_VAR + | UPPER_IDENT "[" type_arg {"," type_arg} "]" + | "own" type_atom | "ref" type_atom | "mut" type_atom + | "{" row_fields "}" (* record *) + | "(" type_expr {"," type_expr} ")" (* tuple *) + +row_fields = field_type {"," field_type} ["," row_var] + | row_var + +effects = effect_term {"+" effect_term} +effect_term = UPPER_IDENT ["[" type_arg {"," type_arg} "]"] +``` + +### 2.4 Function Declarations + +```ebnf +fn_decl = [visibility] ["total"] "fn" LOWER_IDENT + [type_params] "(" [param_list] ")" + ["->" type_expr] ["/" effects] + [where_clause] fn_body + +param_list = param {"," param} +param = [quantity] [ownership] IDENT ":" type_expr +ownership = "own" | "ref" | "mut" + +where_clause = "where" constraint {"," constraint} +constraint = predicate | TYPE_VAR ":" trait_bounds + +fn_body = block | "=" expr +``` + +### 2.5 Expressions + +```ebnf +expr = let_expr | if_expr | match_expr | fn_expr + | handle_expr | return_expr | binary_expr + +let_expr = "let" ["mut"] pattern [":" type_expr] "=" expr ["in" expr] +if_expr = "if" expr block ["else" (if_expr | block)] +match_expr = "match" expr "{" {match_arm} "}" +match_arm = pattern ["if" expr] "=>" expr [","] + +fn_expr = "|" [param_list] "|" expr + | "fn" "(" [param_list] ")" fn_body + +handle_expr = "handle" expr "with" "{" {handler_arm} "}" +handler_arm = "return" pattern "=>" expr [","] + | LOWER_IDENT "(" [pattern {"," pattern}] ")" "=>" expr [","] + +block = "{" {statement} [expr] "}" + +binary_expr = unary_expr {BINARY_OP unary_expr} +unary_expr = [UNARY_OP] postfix_expr +postfix_expr = primary_expr {postfix} +postfix = "." IDENT | "." INT | "[" expr "]" | "(" [args] ")" + | "::" UPPER_IDENT | "\\" IDENT + +primary_expr = literal | IDENT + | "(" expr ")" | "(" expr {"," expr} ")" + | "[" [expr {"," expr}] "]" + | "{" [field_init {"," field_init}] [".." expr] "}" + | "resume" "(" [expr] ")" +``` + +### 2.6 Patterns + +```ebnf +pattern = "_" (* wildcard *) + | IDENT (* binding *) + | literal (* literal match *) + | UPPER_IDENT ["(" pattern {"," pattern} ")"] + | "(" pattern {"," pattern} ")" (* tuple *) + | "{" field_pat {"," field_pat} [".." ] "}" + | pattern "|" pattern (* or-pattern *) + | IDENT "@" pattern (* binding with pattern *) +``` + +### 2.7 Effect Declarations + +```ebnf +effect_decl = [visibility] "effect" UPPER_IDENT [type_params] + "{" {effect_op} "}" +effect_op = "fn" LOWER_IDENT "(" [param_list] ")" ["->" type_expr] ";" +``` + +### 2.8 Trait Declarations + +```ebnf +trait_decl = [visibility] "trait" UPPER_IDENT [type_params] + [":" trait_bounds] "{" {trait_item} "}" +trait_bounds = UPPER_IDENT {"+" UPPER_IDENT} +trait_item = fn_sig ";" | fn_decl | assoc_type + +impl_block = "impl" [type_params] [trait_ref "for"] type_expr + [where_clause] "{" {impl_item} "}" +``` + +## 3. Type System + +### 3.1 Judgement Forms + +``` +Γ ⊢ e : τ / ε Expression e has type τ with effects ε +Γ ⊢ τ : κ Type τ has kind κ +Γ ⊢ P true Predicate P is satisfied +``` + +### 3.2 Quantities (QTT) + +| Quantity | Meaning | Usage | +|----------|---------|-------| +| `0` | Erased | Compile-time only, no runtime cost | +| `1` | Linear | Must use exactly once | +| `ω` | Unrestricted | Use any number of times | + +**Algebra:** +``` +0 + q = q 0 * q = 0 +1 + 1 = ω 1 * q = q +ω + ω = ω ω * ω = ω +``` + +### 3.3 Ownership + +| Modifier | Meaning | +|----------|---------| +| `own T` | Owned value - caller transfers ownership | +| `ref T` | Immutable borrow - cannot modify | +| `mut T` | Mutable borrow - exclusive access | + +**Rules:** +- Owned values are consumed on use +- Multiple `ref` borrows allowed simultaneously +- Only one `mut` borrow at a time +- Borrows cannot outlive owner + +### 3.4 Effects + +Functions declare effects after `/`: + +```affinescript +fn pure_fn(x: Int) -> Int / Pure { x + 1 } +fn io_fn() -> () / IO { println("hello") } +fn fallible() -> Int / Exn[Error] { throw(Error::new()) } +fn combined() -> () / IO + Exn[Error] { ... } +``` + +**Partial by Default:** +- Functions are partial by default (may not terminate) +- `total` functions must provably terminate + +### 3.5 Dependent Types + +Types can depend on values: + +```affinescript +type Vec[n: Nat, T: Type] = + | Nil : Vec[0, T] + | Cons(T, Vec[n, T]) : Vec[n + 1, T] + +// Can only call on non-empty vectors +fn head[n: Nat, T](v: Vec[n + 1, T]) -> T +``` + +### 3.6 Refinement Types + +Constrain types with predicates: + +```affinescript +type PosInt = Int where (self > 0) +fn safeDiv(a: Int, b: Int where (b != 0)) -> Int +``` + +### 3.7 Row Polymorphism + +Extensible records with row variables: + +```affinescript +// Works on any record with 'name' field +fn greet[..r](person: {name: String, ..r}) -> String { + "Hello, " ++ person.name +} + +// Add fields +fn addAge[..r](rec: {..r}) -> {age: Int, ..r} { + {age: 0, ..rec} +} + +// Remove fields +fn removeName[..r](rec: {name: String, ..r}) -> {..r} { + rec \ name +} +``` + +## 4. Core Typing Rules + +### Variables +``` +Γ, x :q τ ⊢ x : τ / ∅ +``` + +### Functions +``` +Γ, x :q τ₁ ⊢ e : τ₂ / ε +──────────────────────────────── +Γ ⊢ fn(x: τ₁) => e : τ₁ -> τ₂ / ε +``` + +### Application +``` +Γ ⊢ f : τ₁ -> τ₂ / ε₁ Γ ⊢ e : τ₁ / ε₂ +────────────────────────────────────────── +Γ ⊢ f(e) : τ₂ / ε₁ + ε₂ +``` + +### Records +``` +Γ ⊢ e : {ℓ: τ, ..r} / ε +──────────────────────── +Γ ⊢ e.ℓ : τ / ε + +Γ ⊢ e : {..r} / ε +─────────────────────────────── +Γ ⊢ {ℓ: v, ..e} : {ℓ: τ, ..r} / ε +``` + +### Effect Handling +``` +handle (return v) with { return x => eᵣ, ... } + → eᵣ[x ↦ v] + +handle E[op(v)] with { op(x) => eₒₚ, ... } + → eₒₚ[x ↦ v, resume ↦ fn(y) => handle E[y] with {...}] +``` + +## 5. Standard Library (Core) + +### 5.1 Primitive Types + +```affinescript +type Nat // Natural numbers (0, 1, 2, ...) +type Int // 64-bit signed integers +type Float // 64-bit floats +type Bool // true | false +type String // UTF-8 string +type Char // Unicode scalar value +type Never // Uninhabited type +``` + +### 5.2 Standard Effects + +```affinescript +effect IO { + fn print(s: String); + fn println(s: String); + fn readLine() -> String; +} + +effect Exn[E] { + fn throw(err: E) -> Never; +} + +effect State[S] { + fn get() -> S; + fn put(s: S); +} +``` + +### 5.3 Option and Result + +```affinescript +type Option[T] = None | Some(T) + +type Result[T, E] = Ok(T) | Err(E) +``` + +### 5.4 Core Traits + +```affinescript +trait Eq { + fn eq(ref self, other: ref Self) -> Bool; +} + +trait Ord: Eq { + fn cmp(ref self, other: ref Self) -> Ordering; +} + +trait Show { + fn show(ref self) -> String; +} + +trait Clone { + fn clone(ref self) -> Self; +} + +trait Drop { + fn drop(own self); +} +``` + +## 6. Example Programs + +### 6.1 Hello World + +```affinescript +effect IO { + fn println(s: String); +} + +fn main() -> () / IO { + println("Hello, AffineScript!") +} +``` + +### 6.2 Length-Indexed Vector + +```affinescript +type Vec[n: Nat, T: Type] = + | Nil : Vec[0, T] + | Cons(head: T, tail: Vec[n, T]) : Vec[n + 1, T] + +total fn head[n: Nat, T](v: Vec[n + 1, T]) -> T / Pure { + match v { Cons(h, _) => h } +} + +total fn append[n: Nat, m: Nat, T]( + a: Vec[n, T], b: Vec[m, T] +) -> Vec[n + m, T] / Pure { + match a { + Nil => b, + Cons(h, t) => Cons(h, append(t, b)) + } +} +``` + +### 6.3 Ownership and Resources + +```affinescript +type File = own { fd: Int } + +fn open(path: ref String) -> Result[own File, IOError] / IO +fn read(file: ref File) -> Result[String, IOError] / IO +fn close(file: own File) -> Result[(), IOError] / IO + +fn withFile[T]( + path: ref String, + action: (ref File) -> Result[T, IOError] +) -> Result[T, IOError] / IO + Exn[IOError] { + let file = open(path)?; + let result = action(ref file); + close(file)?; + result +} +``` + +### 6.4 Row Polymorphism + +```affinescript +fn greet[..r](person: {name: String, ..r}) -> String / Pure { + "Hello, " ++ person.name +} + +fn addField[..r](rec: {..r}, age: Int) -> {age: Int, ..r} / Pure { + {age: age, ..rec} +} + +fn main() -> () / Pure { + let alice = {name: "Alice", role: "Engineer"}; + let bob = {name: "Bob", dept: "Sales"}; + + // Both work despite different shapes + greet(alice); // "Hello, Alice" + greet(bob); // "Hello, Bob" +} +``` + +### 6.5 Effect Handlers + +```affinescript +effect State[S] { + fn get() -> S; + fn put(s: S); +} + +fn counter() -> Int / State[Int] { + let n = State.get(); + State.put(n + 1); + n +} + +fn runState[S, T](init: S, comp: () -> T / State[S]) -> (T, S) / Pure { + handle comp() with { + return x => (x, init), + get() => resume(init), + put(s) => resume(()) + } +} +``` + +## 7. WASM Compilation + +### 7.1 Type Mapping + +| AffineScript | WASM | +|--------------|------| +| `Int`, `Nat` | `i64` | +| `Float` | `f64` | +| `Bool` | `i32` | +| `String` | `(ref (array i8))` | +| `{fields}` | `(ref (struct ...))` | +| `own T` | `(ref T)` (ownership is erased) | +| `T -> U` | `(ref (struct $func $env))` | + +### 7.2 Ownership Erasure + +Ownership and quantity annotations exist only at compile time: + +```affinescript +fn useFile(own file: File) -> () / IO { close(file) } +``` + +Compiles to (ownership removed): +```wat +(func $useFile (param $file (ref $File)) + (call $close (local.get $file))) +``` + +## Appendix: Grammar Reference + +See the full specification at `affinescript-spec.md` for: +- Complete EBNF grammar +- Detailed typing rules +- Operational semantics +- Error message catalog +- Implementation guide + +--- + +*AffineScript Specification v0.1 - Reference Implementation* diff --git a/examples/effects.as b/examples/effects.as new file mode 100644 index 0000000..0a090bf --- /dev/null +++ b/examples/effects.as @@ -0,0 +1,96 @@ +// Effect handling in AffineScript + +// Define a custom exception effect +effect Exn[E] { + fn throw(err: E) -> Never; +} + +// Define a state effect +effect State[S] { + fn get() -> S; + fn put(s: S); + fn modify(f: S -> S); +} + +// A computation that uses state +fn increment() -> Int / State[Int] { + let n = State.get(); + State.put(n + 1); + n +} + +fn triple() -> (Int, Int, Int) / State[Int] { + let a = increment(); + let b = increment(); + let c = increment(); + (a, b, c) +} + +// Run state effect with an initial value +fn runState[S, T](init: S, comp: () -> T / State[S]) -> (T, S) / Pure { + let mut state = init; + handle comp() with { + return x => (x, state), + get() => resume(state), + put(s) => { + state = s; + resume(()) + }, + modify(f) => { + state = f(state); + resume(()) + } + } +} + +// Error handling with effects +type ParseError = { line: Int, message: String } + +fn parseInt(s: String) -> Int / Exn[ParseError] { + // Simplified - would actually parse + if s == "42" { + 42 + } else { + Exn.throw(ParseError { line: 1, message: "not a number" }) + } +} + +fn parseTwo(a: String, b: String) -> (Int, Int) / Exn[ParseError] { + (parseInt(a), parseInt(b)) +} + +// Convert exceptions to Result +fn catchExn[E, T](comp: () -> T / Exn[E]) -> Result[T, E] / Pure { + handle comp() with { + return x => Ok(x), + throw(e) => Err(e) + } +} + +// Combine multiple effects +effect Log { + fn log(msg: String); +} + +fn processWithLogging(x: Int) -> Int / State[Int] + Log { + Log.log("Processing input"); + let current = State.get(); + let result = current + x; + State.put(result); + Log.log("Updated state"); + result +} + +fn main() -> () / IO { + // Run the state computation + let (result, finalState) = runState(0, || triple()); + println("Result: " ++ result.show()); + println("Final state: " ++ finalState.show()); + + // Handle exceptions + let parsed = catchExn(|| parseInt("42")); + match parsed { + Ok(n) => println("Parsed: " ++ n.show()), + Err(e) => println("Error: " ++ e.message) + } +} diff --git a/examples/refinements.as b/examples/refinements.as new file mode 100644 index 0000000..edd664d --- /dev/null +++ b/examples/refinements.as @@ -0,0 +1,115 @@ +// Refinement types in AffineScript + +// Positive integers - enforced at compile time +type PosInt = Int where (self > 0) + +// Non-zero integers for division +type NonZero = Int where (self != 0) + +// Percentage values (0-100) +type Percentage = Int where (self >= 0 && self <= 100) + +// Safe division - cannot divide by zero +fn safeDiv(a: Int, b: NonZero) -> Int / Pure { + a / b +} + +// Square root only on non-negative numbers +fn sqrt(n: Int where (n >= 0)) -> Float / Pure { + // Implementation + 0.0 +} + +// Array indexing with bounds checking at compile time +total fn safeGet[n: Nat, T]( + arr: ref Vec[n, T], + i: Nat where (i < n) +) -> ref T / Pure { + match (arr, i) { + (Cons(h, _), 0) => ref h, + (Cons(_, t), _) => safeGet(ref t, i - 1) + } +} + +// Bounded integers +type Age = Int where (self >= 0 && self <= 150) + +type Port = Int where (self >= 0 && self <= 65535) + +// Create validated values +fn mkPosInt(n: Int) -> Option[PosInt] / Pure { + if n > 0 { + Some(n) // Compiler verifies the refinement + } else { + None + } +} + +fn mkPercentage(n: Int) -> Option[Percentage] / Pure { + if n >= 0 && n <= 100 { + Some(n) + } else { + None + } +} + +// Using unsafe assume when you know better than the compiler +fn unsafePos(n: Int) -> PosInt / Pure { + unsafe { + assume(n > 0) + } + n +} + +// Length-indexed strings +type BoundedString[max: Nat] = String where (len(self) <= max) + +type Username = BoundedString[32] +type Email = BoundedString[254] + +// Matrices with compile-time dimension checking +type Matrix[rows: Nat, cols: Nat, T: Type] = Vec[rows, Vec[cols, T]] + +// Matrix multiplication with dimension constraints +total fn matmul[m: Nat, n: Nat, p: Nat]( + a: ref Matrix[m, n, Float], + b: ref Matrix[n, p, Float] +) -> Matrix[m, p, Float] / Pure { + // The types guarantee dimensions are compatible + // n in matrix 'a' must equal n in matrix 'b' + // ... + Nil +} + +// Refined function parameters +fn processAge(age: Age) -> String / Pure { + if age < 18 { + "minor" + } else if age < 65 { + "adult" + } else { + "senior" + } +} + +// Contracts on return values +fn double(n: PosInt) -> Int where (self > n) / Pure { + n * 2 // Compiler proves 2n > n for positive n +} + +fn main() -> () / IO { + // These compile - refinements satisfied + let age: Age = 25; + let port: Port = 8080; + let pct: Percentage = 75; + + // This would fail at compile time: + // let bad: Age = 200; // Error: cannot prove 200 <= 150 + + // Runtime validation for unknown values + let input = readLine(); + match mkPercentage(parseInt(input)) { + Some(p) => println("Valid percentage: " ++ p.show()), + None => println("Invalid percentage") + } +} diff --git a/examples/traits.as b/examples/traits.as new file mode 100644 index 0000000..c8b9a5a --- /dev/null +++ b/examples/traits.as @@ -0,0 +1,161 @@ +// Traits and type classes in AffineScript + +// Equality trait +trait Eq { + fn eq(ref self, other: ref Self) -> Bool / Pure; + + fn neq(ref self, other: ref Self) -> Bool / Pure { + !self.eq(other) + } +} + +// Ordering trait (requires Eq) +trait Ord: Eq { + fn cmp(ref self, other: ref Self) -> Ordering / Pure; + + fn lt(ref self, other: ref Self) -> Bool / Pure { + self.cmp(other) == Less + } + + fn le(ref self, other: ref Self) -> Bool / Pure { + self.cmp(other) != Greater + } + + fn gt(ref self, other: ref Self) -> Bool / Pure { + self.cmp(other) == Greater + } + + fn ge(ref self, other: ref Self) -> Bool / Pure { + self.cmp(other) != Less + } +} + +type Ordering = Less | Equal | Greater + +// Show trait for string representation +trait Show { + fn show(ref self) -> String / Pure; +} + +// Implement traits for Int +impl Eq for Int { + fn eq(ref self, other: ref Int) -> Bool / Pure { + *self == *other + } +} + +impl Ord for Int { + fn cmp(ref self, other: ref Int) -> Ordering / Pure { + if *self < *other { Less } + else if *self > *other { Greater } + else { Equal } + } +} + +impl Show for Int { + fn show(ref self) -> String / Pure { + intToString(*self) + } +} + +// Implement Show for Option +impl[T: Show] Show for Option[T] { + fn show(ref self) -> String / Pure { + match self { + None => "None", + Some(x) => "Some(" ++ x.show() ++ ")" + } + } +} + +// Implement Show for Result +impl[T: Show, E: Show] Show for Result[T, E] { + fn show(ref self) -> String / Pure { + match self { + Ok(x) => "Ok(" ++ x.show() ++ ")", + Err(e) => "Err(" ++ e.show() ++ ")" + } + } +} + +// Functor trait +trait Functor[F: Type -> Type] { + fn map[A, B](self: F[A], f: A -> B / Pure) -> F[B] / Pure; +} + +// Monad trait +trait Monad[M: Type -> Type]: Functor[M] { + fn pure[A](value: A) -> M[A] / Pure; + fn flatMap[A, B](self: M[A], f: A -> M[B] / Pure) -> M[B] / Pure; +} + +// Implement Functor for Option +impl Functor[Option] for Option { + fn map[A, B](self: Option[A], f: A -> B / Pure) -> Option[B] / Pure { + match self { + None => None, + Some(x) => Some(f(x)) + } + } +} + +// Implement Monad for Option +impl Monad[Option] for Option { + fn pure[A](value: A) -> Option[A] / Pure { + Some(value) + } + + fn flatMap[A, B](self: Option[A], f: A -> Option[B] / Pure) -> Option[B] / Pure { + match self { + None => None, + Some(x) => f(x) + } + } +} + +// Generic functions using trait bounds +fn max[T: Ord](a: T, b: T) -> T / Pure { + if a.gt(ref b) { a } else { b } +} + +fn min[T: Ord](a: T, b: T) -> T / Pure { + if a.lt(ref b) { a } else { b } +} + +fn printAny[T: Show](value: ref T) -> () / IO { + println(value.show()) +} + +fn sortBy[T, K: Ord](items: mut [T], key: (ref T) -> K / Pure) -> () / Pure { + // Sorting implementation using Ord trait + // ... +} + +// Custom type with trait implementations +type Point = { x: Int, y: Int } + +impl Eq for Point { + fn eq(ref self, other: ref Point) -> Bool / Pure { + self.x == other.x && self.y == other.y + } +} + +impl Show for Point { + fn show(ref self) -> String / Pure { + "(" ++ self.x.show() ++ ", " ++ self.y.show() ++ ")" + } +} + +fn main() -> () / IO { + let p1 = Point { x: 1, y: 2 }; + let p2 = Point { x: 3, y: 4 }; + + printAny(ref p1); + printAny(ref p2); + + let m = max(10, 20); + println("Max: " ++ m.show()); + + let opt = Some(42); + printAny(ref opt); +} From 63f4d5d0b5d3ae7b6bbb520631a070d52ad3a018 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Dec 2025 18:04:01 +0000 Subject: [PATCH 2/2] test: add parser/AST golden tests Add golden test infrastructure that compares parser AST output against expected files for regression testing. New test infrastructure: - test/test_golden.ml: Golden test runner with span normalization - test/golden/: Directory containing .as source and .expected AST files Golden tests cover: - hello.as: Basic function declaration - literals.as: All literal types (int, float, bool, string, char, unit) - binary_ops.as: Operators and precedence - functions.as: Function declarations (simple, total, generic, public) - types.as: Type declarations (alias, struct, enum) - effects.as: Effect declarations with operations - patterns.as: Pattern matching (literals, destructuring, constructors) - traits.as: Trait declarations and implementations - ownership.as: Ownership modifiers (own, ref, mut) - rows.as: Row polymorphism syntax - lambda.as: Lambda expressions - imports.as: Module imports Also adds example parse tests that verify all examples/*.as files parse successfully without checking specific AST structure. --- test/dune | 5 +- test/golden/binary_ops.as | 8 +++ test/golden/binary_ops.expected | 44 +++++++++++++ test/golden/effects.as | 10 +++ test/golden/effects.expected | 36 +++++++++++ test/golden/functions.as | 16 +++++ test/golden/functions.expected | 66 +++++++++++++++++++ test/golden/hello.as | 4 ++ test/golden/hello.expected | 13 ++++ test/golden/imports.as | 8 +++ test/golden/imports.expected | 31 +++++++++ test/golden/lambda.as | 10 +++ test/golden/lambda.expected | 68 ++++++++++++++++++++ test/golden/literals.as | 9 +++ test/golden/literals.expected | 44 +++++++++++++ test/golden/ownership.as | 8 +++ test/golden/ownership.expected | 34 ++++++++++ test/golden/patterns.as | 20 ++++++ test/golden/patterns.expected | 88 +++++++++++++++++++++++++ test/golden/rows.as | 8 +++ test/golden/rows.expected | 58 +++++++++++++++++ test/golden/traits.as | 14 ++++ test/golden/traits.expected | 75 ++++++++++++++++++++++ test/golden/types.as | 12 ++++ test/golden/types.expected | 36 +++++++++++ test/test_golden.ml | 110 ++++++++++++++++++++++++++++++++ test/test_main.ml | 2 + 27 files changed, 836 insertions(+), 1 deletion(-) create mode 100644 test/golden/binary_ops.as create mode 100644 test/golden/binary_ops.expected create mode 100644 test/golden/effects.as create mode 100644 test/golden/effects.expected create mode 100644 test/golden/functions.as create mode 100644 test/golden/functions.expected create mode 100644 test/golden/hello.as create mode 100644 test/golden/hello.expected create mode 100644 test/golden/imports.as create mode 100644 test/golden/imports.expected create mode 100644 test/golden/lambda.as create mode 100644 test/golden/lambda.expected create mode 100644 test/golden/literals.as create mode 100644 test/golden/literals.expected create mode 100644 test/golden/ownership.as create mode 100644 test/golden/ownership.expected create mode 100644 test/golden/patterns.as create mode 100644 test/golden/patterns.expected create mode 100644 test/golden/rows.as create mode 100644 test/golden/rows.expected create mode 100644 test/golden/traits.as create mode 100644 test/golden/traits.expected create mode 100644 test/golden/types.as create mode 100644 test/golden/types.expected create mode 100644 test/test_golden.ml diff --git a/test/dune b/test/dune index 2157077..247e17d 100644 --- a/test/dune +++ b/test/dune @@ -1,3 +1,6 @@ (test (name test_main) - (libraries affinescript alcotest)) + (libraries affinescript alcotest str unix) + (deps + (source_tree golden) + (source_tree ../examples))) diff --git a/test/golden/binary_ops.as b/test/golden/binary_ops.as new file mode 100644 index 0000000..ef6a0b9 --- /dev/null +++ b/test/golden/binary_ops.as @@ -0,0 +1,8 @@ +// Test binary operators and precedence +fn test_ops() -> Int { + 1 + 2 * 3 +} + +fn test_compare() -> Bool { + x == y && a < b +} diff --git a/test/golden/binary_ops.expected b/test/golden/binary_ops.expected new file mode 100644 index 0000000..710d8b5 --- /dev/null +++ b/test/golden/binary_ops.expected @@ -0,0 +1,44 @@ +{ Ast.prog_module = None; prog_imports = []; + prog_decls = + [Ast.TopFn + { Ast.fd_vis = Ast.Private; fd_total = false; + fd_name = { Ast.name = "test_ops"; span = }; + fd_type_params = []; fd_params = []; + fd_ret_ty = (Some (Ast.TyCon { Ast.name = "Int"; span = })); + fd_eff = None; fd_where = []; + fd_body = + (Ast.FnBlock + { Ast.blk_stmts = []; + blk_expr = + (Some (Ast.ExprBinary ( + (Ast.ExprLit (Ast.LitInt (1, ))), Ast.OpAdd, + (Ast.ExprBinary ( + (Ast.ExprLit (Ast.LitInt (2, ))), Ast.OpMul, + (Ast.ExprLit (Ast.LitInt (3, ))))) + ))) + }) + }; + Ast.TopFn + { Ast.fd_vis = Ast.Private; fd_total = false; + fd_name = { Ast.name = "test_compare"; span = }; + fd_type_params = []; fd_params = []; + fd_ret_ty = (Some (Ast.TyCon { Ast.name = "Bool"; span = })); + fd_eff = None; fd_where = []; + fd_body = + (Ast.FnBlock + { Ast.blk_stmts = []; + blk_expr = + (Some (Ast.ExprBinary ( + (Ast.ExprBinary ( + (Ast.ExprVar { Ast.name = "x"; span = }), + Ast.OpEq, + (Ast.ExprVar { Ast.name = "y"; span = }))), + Ast.OpAnd, + (Ast.ExprBinary ( + (Ast.ExprVar { Ast.name = "a"; span = }), + Ast.OpLt, + (Ast.ExprVar { Ast.name = "b"; span = }))) + ))) + }) + }] + } diff --git a/test/golden/effects.as b/test/golden/effects.as new file mode 100644 index 0000000..e16836d --- /dev/null +++ b/test/golden/effects.as @@ -0,0 +1,10 @@ +// Test effect declarations +effect IO { + fn print(s: String) -> (); + fn read() -> String; +} + +effect State[S] { + fn get() -> S; + fn put(s: S) -> (); +} diff --git a/test/golden/effects.expected b/test/golden/effects.expected new file mode 100644 index 0000000..b037187 --- /dev/null +++ b/test/golden/effects.expected @@ -0,0 +1,36 @@ +{ Ast.prog_module = None; prog_imports = []; + prog_decls = + [Ast.TopEffect + { Ast.ed_vis = Ast.Private; + ed_name = { Ast.name = "IO"; span = }; + ed_type_params = []; + ed_ops = + [{ Ast.eod_name = { Ast.name = "print"; span = }; + eod_params = + [{ Ast.p_quantity = None; p_ownership = None; + p_name = { Ast.name = "s"; span = }; + p_ty = (Ast.TyCon { Ast.name = "String"; span = }) }]; + eod_ret_ty = (Some (Ast.TyTuple [])) }; + { Ast.eod_name = { Ast.name = "read"; span = }; + eod_params = []; + eod_ret_ty = + (Some (Ast.TyCon { Ast.name = "String"; span = })) }] + }; + Ast.TopEffect + { Ast.ed_vis = Ast.Private; + ed_name = { Ast.name = "State"; span = }; + ed_type_params = + [{ Ast.tp_quantity = None; + tp_name = { Ast.name = "S"; span = }; tp_kind = None }]; + ed_ops = + [{ Ast.eod_name = { Ast.name = "get"; span = }; + eod_params = []; + eod_ret_ty = (Some (Ast.TyVar { Ast.name = "S"; span = })) }; + { Ast.eod_name = { Ast.name = "put"; span = }; + eod_params = + [{ Ast.p_quantity = None; p_ownership = None; + p_name = { Ast.name = "s"; span = }; + p_ty = (Ast.TyVar { Ast.name = "S"; span = }) }]; + eod_ret_ty = (Some (Ast.TyTuple [])) }] + }] + } diff --git a/test/golden/functions.as b/test/golden/functions.as new file mode 100644 index 0000000..25e7a40 --- /dev/null +++ b/test/golden/functions.as @@ -0,0 +1,16 @@ +// Test function declarations +fn simple(x: Int) -> Int { + x + 1 +} + +total fn safe(n: Nat) -> Nat { + n +} + +fn generic[T](x: T) -> T { + x +} + +pub fn visible() -> () { + () +} diff --git a/test/golden/functions.expected b/test/golden/functions.expected new file mode 100644 index 0000000..9525063 --- /dev/null +++ b/test/golden/functions.expected @@ -0,0 +1,66 @@ +{ Ast.prog_module = None; prog_imports = []; + prog_decls = + [Ast.TopFn + { Ast.fd_vis = Ast.Private; fd_total = false; + fd_name = { Ast.name = "simple"; span = }; + fd_type_params = []; + fd_params = + [{ Ast.p_quantity = None; p_ownership = None; + p_name = { Ast.name = "x"; span = }; + p_ty = (Ast.TyCon { Ast.name = "Int"; span = }) }]; + fd_ret_ty = (Some (Ast.TyCon { Ast.name = "Int"; span = })); + fd_eff = None; fd_where = []; + fd_body = + (Ast.FnBlock + { Ast.blk_stmts = []; + blk_expr = + (Some (Ast.ExprBinary ( + (Ast.ExprVar { Ast.name = "x"; span = }), + Ast.OpAdd, (Ast.ExprLit (Ast.LitInt (1, )))))) + }) + }; + Ast.TopFn + { Ast.fd_vis = Ast.Private; fd_total = true; + fd_name = { Ast.name = "safe"; span = }; + fd_type_params = []; + fd_params = + [{ Ast.p_quantity = None; p_ownership = None; + p_name = { Ast.name = "n"; span = }; + p_ty = (Ast.TyCon { Ast.name = "Nat"; span = }) }]; + fd_ret_ty = (Some (Ast.TyCon { Ast.name = "Nat"; span = })); + fd_eff = None; fd_where = []; + fd_body = + (Ast.FnBlock + { Ast.blk_stmts = []; + blk_expr = (Some (Ast.ExprVar { Ast.name = "n"; span = })) + }) + }; + Ast.TopFn + { Ast.fd_vis = Ast.Private; fd_total = false; + fd_name = { Ast.name = "generic"; span = }; + fd_type_params = + [{ Ast.tp_quantity = None; + tp_name = { Ast.name = "T"; span = }; tp_kind = None }]; + fd_params = + [{ Ast.p_quantity = None; p_ownership = None; + p_name = { Ast.name = "x"; span = }; + p_ty = (Ast.TyVar { Ast.name = "T"; span = }) }]; + fd_ret_ty = (Some (Ast.TyVar { Ast.name = "T"; span = })); + fd_eff = None; fd_where = []; + fd_body = + (Ast.FnBlock + { Ast.blk_stmts = []; + blk_expr = (Some (Ast.ExprVar { Ast.name = "x"; span = })) + }) + }; + Ast.TopFn + { Ast.fd_vis = Ast.Public; fd_total = false; + fd_name = { Ast.name = "visible"; span = }; + fd_type_params = []; fd_params = []; + fd_ret_ty = (Some (Ast.TyTuple [])); fd_eff = None; fd_where = []; + fd_body = + (Ast.FnBlock + { Ast.blk_stmts = []; + blk_expr = (Some (Ast.ExprLit (Ast.LitUnit ))) }) + }] + } diff --git a/test/golden/hello.as b/test/golden/hello.as new file mode 100644 index 0000000..400c13f --- /dev/null +++ b/test/golden/hello.as @@ -0,0 +1,4 @@ +// Simple hello world +fn main() -> () { + () +} diff --git a/test/golden/hello.expected b/test/golden/hello.expected new file mode 100644 index 0000000..dff5ad2 --- /dev/null +++ b/test/golden/hello.expected @@ -0,0 +1,13 @@ +{ Ast.prog_module = None; prog_imports = []; + prog_decls = + [Ast.TopFn + { Ast.fd_vis = Ast.Private; fd_total = false; + fd_name = { Ast.name = "main"; span = }; + fd_type_params = []; fd_params = []; + fd_ret_ty = (Some (Ast.TyTuple [])); fd_eff = None; fd_where = []; + fd_body = + (Ast.FnBlock + { Ast.blk_stmts = []; + blk_expr = (Some (Ast.ExprLit (Ast.LitUnit ))) }) + }] + } diff --git a/test/golden/imports.as b/test/golden/imports.as new file mode 100644 index 0000000..ce3b919 --- /dev/null +++ b/test/golden/imports.as @@ -0,0 +1,8 @@ +// Test module imports +use std.io; +use std.collections.Vec as Vector; +use std.prelude::{Option, Result}; + +fn main() -> () { + () +} diff --git a/test/golden/imports.expected b/test/golden/imports.expected new file mode 100644 index 0000000..252ce88 --- /dev/null +++ b/test/golden/imports.expected @@ -0,0 +1,31 @@ +{ Ast.prog_module = None; + prog_imports = + [(Ast.ImportSimple ( + [{ Ast.name = "std"; span = }; + { Ast.name = "io"; span = }], + None)); + (Ast.ImportSimple ( + [{ Ast.name = "std"; span = }; + { Ast.name = "collections"; span = }; + { Ast.name = "Vec"; span = }], + (Some { Ast.name = "Vector"; span = }))); + (Ast.ImportList ( + [{ Ast.name = "std"; span = }; + { Ast.name = "prelude"; span = }], + [{ Ast.ii_name = { Ast.name = "Option"; span = }; + ii_alias = None }; + { Ast.ii_name = { Ast.name = "Result"; span = }; + ii_alias = None }])) + ]; + prog_decls = + [Ast.TopFn + { Ast.fd_vis = Ast.Private; fd_total = false; + fd_name = { Ast.name = "main"; span = }; + fd_type_params = []; fd_params = []; + fd_ret_ty = (Some (Ast.TyTuple [])); fd_eff = None; fd_where = []; + fd_body = + (Ast.FnBlock + { Ast.blk_stmts = []; + blk_expr = (Some (Ast.ExprLit (Ast.LitUnit ))) }) + }] + } diff --git a/test/golden/lambda.as b/test/golden/lambda.as new file mode 100644 index 0000000..7fd853a --- /dev/null +++ b/test/golden/lambda.as @@ -0,0 +1,10 @@ +// Test lambda expressions +fn test_lambda() -> Int { + let f = |x| x + 1; + f(42) +} + +fn test_lambda_typed() -> Int { + let f = |x: Int| x * 2; + f(21) +} diff --git a/test/golden/lambda.expected b/test/golden/lambda.expected new file mode 100644 index 0000000..26129a2 --- /dev/null +++ b/test/golden/lambda.expected @@ -0,0 +1,68 @@ +{ Ast.prog_module = None; prog_imports = []; + prog_decls = + [Ast.TopFn + { Ast.fd_vis = Ast.Private; fd_total = false; + fd_name = { Ast.name = "test_lambda"; span = }; + fd_type_params = []; fd_params = []; + fd_ret_ty = (Some (Ast.TyCon { Ast.name = "Int"; span = })); + fd_eff = None; fd_where = []; + fd_body = + (Ast.FnBlock + { Ast.blk_stmts = + [Ast.StmtLet + { Ast.sl_mut = false; + sl_pat = (Ast.PatVar { Ast.name = "f"; span = }); + sl_ty = None; + sl_value = + (Ast.ExprLambda + { Ast.elam_params = + [{ Ast.p_quantity = None; p_ownership = None; + p_name = { Ast.name = "x"; span = }; + p_ty = Ast.TyHole }]; + elam_ret_ty = None; + elam_body = + (Ast.ExprBinary ( + (Ast.ExprVar { Ast.name = "x"; span = }), + Ast.OpAdd, (Ast.ExprLit (Ast.LitInt (1, ))))) + }) + }]; + blk_expr = + (Some (Ast.ExprApp ( + (Ast.ExprVar { Ast.name = "f"; span = }), + [(Ast.ExprLit (Ast.LitInt (42, )))]))) + }) + }; + Ast.TopFn + { Ast.fd_vis = Ast.Private; fd_total = false; + fd_name = { Ast.name = "test_lambda_typed"; span = }; + fd_type_params = []; fd_params = []; + fd_ret_ty = (Some (Ast.TyCon { Ast.name = "Int"; span = })); + fd_eff = None; fd_where = []; + fd_body = + (Ast.FnBlock + { Ast.blk_stmts = + [Ast.StmtLet + { Ast.sl_mut = false; + sl_pat = (Ast.PatVar { Ast.name = "f"; span = }); + sl_ty = None; + sl_value = + (Ast.ExprLambda + { Ast.elam_params = + [{ Ast.p_quantity = None; p_ownership = None; + p_name = { Ast.name = "x"; span = }; + p_ty = + (Ast.TyCon { Ast.name = "Int"; span = }) }]; + elam_ret_ty = None; + elam_body = + (Ast.ExprBinary ( + (Ast.ExprVar { Ast.name = "x"; span = }), + Ast.OpMul, (Ast.ExprLit (Ast.LitInt (2, ))))) + }) + }]; + blk_expr = + (Some (Ast.ExprApp ( + (Ast.ExprVar { Ast.name = "f"; span = }), + [(Ast.ExprLit (Ast.LitInt (21, )))]))) + }) + }] + } diff --git a/test/golden/literals.as b/test/golden/literals.as new file mode 100644 index 0000000..bba50e7 --- /dev/null +++ b/test/golden/literals.as @@ -0,0 +1,9 @@ +// Test all literal types +fn test_literals() -> () { + let i = 42; + let f = 3.14; + let b = true; + let s = "hello"; + let c = 'x'; + let u = (); +} diff --git a/test/golden/literals.expected b/test/golden/literals.expected new file mode 100644 index 0000000..287c954 --- /dev/null +++ b/test/golden/literals.expected @@ -0,0 +1,44 @@ +{ Ast.prog_module = None; prog_imports = []; + prog_decls = + [Ast.TopFn + { Ast.fd_vis = Ast.Private; fd_total = false; + fd_name = { Ast.name = "test_literals"; span = }; + fd_type_params = []; fd_params = []; + fd_ret_ty = (Some (Ast.TyTuple [])); fd_eff = None; fd_where = []; + fd_body = + (Ast.FnBlock + { Ast.blk_stmts = + [Ast.StmtLet + { Ast.sl_mut = false; + sl_pat = (Ast.PatVar { Ast.name = "i"; span = }); + sl_ty = None; + sl_value = (Ast.ExprLit (Ast.LitInt (42, ))) }; + Ast.StmtLet + { Ast.sl_mut = false; + sl_pat = (Ast.PatVar { Ast.name = "f"; span = }); + sl_ty = None; + sl_value = (Ast.ExprLit (Ast.LitFloat (3.14, ))) }; + Ast.StmtLet + { Ast.sl_mut = false; + sl_pat = (Ast.PatVar { Ast.name = "b"; span = }); + sl_ty = None; + sl_value = (Ast.ExprLit (Ast.LitBool (true, ))) }; + Ast.StmtLet + { Ast.sl_mut = false; + sl_pat = (Ast.PatVar { Ast.name = "s"; span = }); + sl_ty = None; + sl_value = (Ast.ExprLit (Ast.LitString ("hello", ))) }; + Ast.StmtLet + { Ast.sl_mut = false; + sl_pat = (Ast.PatVar { Ast.name = "c"; span = }); + sl_ty = None; + sl_value = (Ast.ExprLit (Ast.LitChar ('x', ))) }; + Ast.StmtLet + { Ast.sl_mut = false; + sl_pat = (Ast.PatVar { Ast.name = "u"; span = }); + sl_ty = None; + sl_value = (Ast.ExprLit (Ast.LitUnit )) } + ]; + blk_expr = None }) + }] + } diff --git a/test/golden/ownership.as b/test/golden/ownership.as new file mode 100644 index 0000000..e0527c0 --- /dev/null +++ b/test/golden/ownership.as @@ -0,0 +1,8 @@ +// Test ownership modifiers +fn test_ownership(x: own String, y: ref Int, z: mut Bool) -> () { + () +} + +struct File { + fd: own Int +} diff --git a/test/golden/ownership.expected b/test/golden/ownership.expected new file mode 100644 index 0000000..a786e87 --- /dev/null +++ b/test/golden/ownership.expected @@ -0,0 +1,34 @@ +{ Ast.prog_module = None; prog_imports = []; + prog_decls = + [Ast.TopFn + { Ast.fd_vis = Ast.Private; fd_total = false; + fd_name = { Ast.name = "test_ownership"; span = }; + fd_type_params = []; + fd_params = + [{ Ast.p_quantity = None; p_ownership = (Some Ast.Own); + p_name = { Ast.name = "x"; span = }; + p_ty = (Ast.TyCon { Ast.name = "String"; span = }) }; + { Ast.p_quantity = None; p_ownership = (Some Ast.Ref); + p_name = { Ast.name = "y"; span = }; + p_ty = (Ast.TyCon { Ast.name = "Int"; span = }) }; + { Ast.p_quantity = None; p_ownership = (Some Ast.Mut); + p_name = { Ast.name = "z"; span = }; + p_ty = (Ast.TyCon { Ast.name = "Bool"; span = }) }]; + fd_ret_ty = (Some (Ast.TyTuple [])); fd_eff = None; fd_where = []; + fd_body = + (Ast.FnBlock + { Ast.blk_stmts = []; + blk_expr = (Some (Ast.ExprLit (Ast.LitUnit ))) }) + }; + Ast.TopType + { Ast.td_vis = Ast.Private; + td_name = { Ast.name = "File"; span = }; + td_type_params = []; + td_body = + (Ast.TyStruct + [{ Ast.sf_vis = Ast.Private; + sf_name = { Ast.name = "fd"; span = }; + sf_ty = + (Ast.TyOwn (Ast.TyCon { Ast.name = "Int"; span = })) }]) + }] + } diff --git a/test/golden/patterns.as b/test/golden/patterns.as new file mode 100644 index 0000000..fee417c --- /dev/null +++ b/test/golden/patterns.as @@ -0,0 +1,20 @@ +// Test pattern matching +fn test_match() -> Int { + match x { + 0 => 1, + n => n + 1 + } +} + +fn test_destructure() -> Int { + match pair { + (a, b) => a + b + } +} + +fn test_constructor() -> Int { + match opt { + Some(x) => x, + None => 0 + } +} diff --git a/test/golden/patterns.expected b/test/golden/patterns.expected new file mode 100644 index 0000000..fe9de53 --- /dev/null +++ b/test/golden/patterns.expected @@ -0,0 +1,88 @@ +{ Ast.prog_module = None; prog_imports = []; + prog_decls = + [Ast.TopFn + { Ast.fd_vis = Ast.Private; fd_total = false; + fd_name = { Ast.name = "test_match"; span = }; + fd_type_params = []; fd_params = []; + fd_ret_ty = (Some (Ast.TyCon { Ast.name = "Int"; span = })); + fd_eff = None; fd_where = []; + fd_body = + (Ast.FnBlock + { Ast.blk_stmts = []; + blk_expr = + (Some (Ast.ExprMatch + { Ast.em_scrutinee = + (Ast.ExprVar { Ast.name = "x"; span = }); + em_arms = + [{ Ast.ma_pat = + (Ast.PatLit (Ast.LitInt (0, ))); + ma_guard = None; + ma_body = (Ast.ExprLit (Ast.LitInt (1, ))) }; + { Ast.ma_pat = + (Ast.PatVar { Ast.name = "n"; span = }); + ma_guard = None; + ma_body = + (Ast.ExprBinary ( + (Ast.ExprVar { Ast.name = "n"; span = }), + Ast.OpAdd, + (Ast.ExprLit (Ast.LitInt (1, ))))) }] + })) + }) + }; + Ast.TopFn + { Ast.fd_vis = Ast.Private; fd_total = false; + fd_name = { Ast.name = "test_destructure"; span = }; + fd_type_params = []; fd_params = []; + fd_ret_ty = (Some (Ast.TyCon { Ast.name = "Int"; span = })); + fd_eff = None; fd_where = []; + fd_body = + (Ast.FnBlock + { Ast.blk_stmts = []; + blk_expr = + (Some (Ast.ExprMatch + { Ast.em_scrutinee = + (Ast.ExprVar { Ast.name = "pair"; span = }); + em_arms = + [{ Ast.ma_pat = + (Ast.PatTuple + [(Ast.PatVar { Ast.name = "a"; span = }); + (Ast.PatVar { Ast.name = "b"; span = })]); + ma_guard = None; + ma_body = + (Ast.ExprBinary ( + (Ast.ExprVar { Ast.name = "a"; span = }), + Ast.OpAdd, + (Ast.ExprVar { Ast.name = "b"; span = }))) }] + })) + }) + }; + Ast.TopFn + { Ast.fd_vis = Ast.Private; fd_total = false; + fd_name = { Ast.name = "test_constructor"; span = }; + fd_type_params = []; fd_params = []; + fd_ret_ty = (Some (Ast.TyCon { Ast.name = "Int"; span = })); + fd_eff = None; fd_where = []; + fd_body = + (Ast.FnBlock + { Ast.blk_stmts = []; + blk_expr = + (Some (Ast.ExprMatch + { Ast.em_scrutinee = + (Ast.ExprVar { Ast.name = "opt"; span = }); + em_arms = + [{ Ast.ma_pat = + (Ast.PatCon ( + { Ast.name = "Some"; span = }, + [(Ast.PatVar { Ast.name = "x"; span = })])); + ma_guard = None; + ma_body = + (Ast.ExprVar { Ast.name = "x"; span = }) }; + { Ast.ma_pat = + (Ast.PatCon ( + { Ast.name = "None"; span = }, [])); + ma_guard = None; + ma_body = (Ast.ExprLit (Ast.LitInt (0, ))) }] + })) + }) + }] + } diff --git a/test/golden/rows.as b/test/golden/rows.as new file mode 100644 index 0000000..bacbcab --- /dev/null +++ b/test/golden/rows.as @@ -0,0 +1,8 @@ +// Test row polymorphism +fn getX[..r](p: {x: Int, ..r}) -> Int { + p.x +} + +fn addY[..r](p: {..r}) -> {y: Int, ..r} { + {y: 0, ..p} +} diff --git a/test/golden/rows.expected b/test/golden/rows.expected new file mode 100644 index 0000000..df52d7b --- /dev/null +++ b/test/golden/rows.expected @@ -0,0 +1,58 @@ +{ Ast.prog_module = None; prog_imports = []; + prog_decls = + [Ast.TopFn + { Ast.fd_vis = Ast.Private; fd_total = false; + fd_name = { Ast.name = "getX"; span = }; + fd_type_params = + [{ Ast.tp_quantity = None; + tp_name = { Ast.name = "r"; span = }; tp_kind = None }]; + fd_params = + [{ Ast.p_quantity = None; p_ownership = None; + p_name = { Ast.name = "p"; span = }; + p_ty = + (Ast.TyRecord ( + [{ Ast.rf_name = { Ast.name = "x"; span = }; + rf_ty = (Ast.TyCon { Ast.name = "Int"; span = }) }], + (Some { Ast.name = "r"; span = }))) }]; + fd_ret_ty = (Some (Ast.TyCon { Ast.name = "Int"; span = })); + fd_eff = None; fd_where = []; + fd_body = + (Ast.FnBlock + { Ast.blk_stmts = []; + blk_expr = + (Some (Ast.ExprField ( + (Ast.ExprVar { Ast.name = "p"; span = }), + { Ast.name = "x"; span = }))) + }) + }; + Ast.TopFn + { Ast.fd_vis = Ast.Private; fd_total = false; + fd_name = { Ast.name = "addY"; span = }; + fd_type_params = + [{ Ast.tp_quantity = None; + tp_name = { Ast.name = "r"; span = }; tp_kind = None }]; + fd_params = + [{ Ast.p_quantity = None; p_ownership = None; + p_name = { Ast.name = "p"; span = }; + p_ty = + (Ast.TyRecord ([], (Some { Ast.name = "r"; span = }))) }]; + fd_ret_ty = + (Some (Ast.TyRecord ( + [{ Ast.rf_name = { Ast.name = "y"; span = }; + rf_ty = (Ast.TyCon { Ast.name = "Int"; span = }) }], + (Some { Ast.name = "r"; span = })))); + fd_eff = None; fd_where = []; + fd_body = + (Ast.FnBlock + { Ast.blk_stmts = []; + blk_expr = + (Some (Ast.ExprRecord + { Ast.er_fields = + [({ Ast.name = "y"; span = }, + (Some (Ast.ExprLit (Ast.LitInt (0, )))))]; + er_spread = + (Some (Ast.ExprVar { Ast.name = "p"; span = })) + })) + }) + }] + } diff --git a/test/golden/traits.as b/test/golden/traits.as new file mode 100644 index 0000000..a3cf159 --- /dev/null +++ b/test/golden/traits.as @@ -0,0 +1,14 @@ +// Test trait declarations +trait Eq { + fn eq(ref self, other: ref Self) -> Bool; +} + +trait Ord: Eq { + fn cmp(ref self, other: ref Self) -> Ordering; +} + +impl Eq for Int { + fn eq(ref self, other: ref Int) -> Bool { + true + } +} diff --git a/test/golden/traits.expected b/test/golden/traits.expected new file mode 100644 index 0000000..9e4de0d --- /dev/null +++ b/test/golden/traits.expected @@ -0,0 +1,75 @@ +{ Ast.prog_module = None; prog_imports = []; + prog_decls = + [Ast.TopTrait + { Ast.trd_vis = Ast.Private; + trd_name = { Ast.name = "Eq"; span = }; + trd_type_params = []; trd_super = []; + trd_items = + [(Ast.TraitFn + { Ast.fs_vis = Ast.Private; + fs_name = { Ast.name = "eq"; span = }; + fs_type_params = []; + fs_params = + [{ Ast.p_quantity = None; p_ownership = (Some Ast.Ref); + p_name = { Ast.name = "self"; span = }; + p_ty = (Ast.TyCon { Ast.name = "Self"; span = }) }; + { Ast.p_quantity = None; p_ownership = (Some Ast.Ref); + p_name = { Ast.name = "other"; span = }; + p_ty = (Ast.TyCon { Ast.name = "Self"; span = }) }]; + fs_ret_ty = + (Some (Ast.TyCon { Ast.name = "Bool"; span = })); + fs_eff = None })] + }; + Ast.TopTrait + { Ast.trd_vis = Ast.Private; + trd_name = { Ast.name = "Ord"; span = }; + trd_type_params = []; + trd_super = + [{ Ast.tb_name = { Ast.name = "Eq"; span = }; + tb_args = [] }]; + trd_items = + [(Ast.TraitFn + { Ast.fs_vis = Ast.Private; + fs_name = { Ast.name = "cmp"; span = }; + fs_type_params = []; + fs_params = + [{ Ast.p_quantity = None; p_ownership = (Some Ast.Ref); + p_name = { Ast.name = "self"; span = }; + p_ty = (Ast.TyCon { Ast.name = "Self"; span = }) }; + { Ast.p_quantity = None; p_ownership = (Some Ast.Ref); + p_name = { Ast.name = "other"; span = }; + p_ty = (Ast.TyCon { Ast.name = "Self"; span = }) }]; + fs_ret_ty = + (Some (Ast.TyCon { Ast.name = "Ordering"; span = })); + fs_eff = None })] + }; + Ast.TopImpl + { Ast.ib_type_params = []; + ib_trait_ref = + (Some { Ast.tr_name = { Ast.name = "Eq"; span = }; + tr_args = [] }); + ib_self_ty = (Ast.TyCon { Ast.name = "Int"; span = }); + ib_where = []; + ib_items = + [(Ast.ImplFn + { Ast.fd_vis = Ast.Private; fd_total = false; + fd_name = { Ast.name = "eq"; span = }; + fd_type_params = []; + fd_params = + [{ Ast.p_quantity = None; p_ownership = (Some Ast.Ref); + p_name = { Ast.name = "self"; span = }; + p_ty = (Ast.TyCon { Ast.name = "Self"; span = }) }; + { Ast.p_quantity = None; p_ownership = (Some Ast.Ref); + p_name = { Ast.name = "other"; span = }; + p_ty = (Ast.TyCon { Ast.name = "Int"; span = }) }]; + fd_ret_ty = + (Some (Ast.TyCon { Ast.name = "Bool"; span = })); + fd_eff = None; fd_where = []; + fd_body = + (Ast.FnBlock + { Ast.blk_stmts = []; + blk_expr = + (Some (Ast.ExprLit (Ast.LitBool (true, )))) }) + })] + }] + } diff --git a/test/golden/types.as b/test/golden/types.as new file mode 100644 index 0000000..efb439e --- /dev/null +++ b/test/golden/types.as @@ -0,0 +1,12 @@ +// Test type declarations +type Alias = Int; + +struct Point { + x: Int, + y: Int +} + +enum Option[T] { + None, + Some(T) +} diff --git a/test/golden/types.expected b/test/golden/types.expected new file mode 100644 index 0000000..2775b13 --- /dev/null +++ b/test/golden/types.expected @@ -0,0 +1,36 @@ +{ Ast.prog_module = None; prog_imports = []; + prog_decls = + [Ast.TopType + { Ast.td_vis = Ast.Private; + td_name = { Ast.name = "Alias"; span = }; + td_type_params = []; + td_body = + (Ast.TyAlias (Ast.TyCon { Ast.name = "Int"; span = })) }; + Ast.TopType + { Ast.td_vis = Ast.Private; + td_name = { Ast.name = "Point"; span = }; + td_type_params = []; + td_body = + (Ast.TyStruct + [{ Ast.sf_vis = Ast.Private; + sf_name = { Ast.name = "x"; span = }; + sf_ty = (Ast.TyCon { Ast.name = "Int"; span = }) }; + { Ast.sf_vis = Ast.Private; + sf_name = { Ast.name = "y"; span = }; + sf_ty = (Ast.TyCon { Ast.name = "Int"; span = }) }]) + }; + Ast.TopType + { Ast.td_vis = Ast.Private; + td_name = { Ast.name = "Option"; span = }; + td_type_params = + [{ Ast.tp_quantity = None; + tp_name = { Ast.name = "T"; span = }; tp_kind = None }]; + td_body = + (Ast.TyEnum + [{ Ast.vd_name = { Ast.name = "None"; span = }; + vd_fields = []; vd_ret_ty = None }; + { Ast.vd_name = { Ast.name = "Some"; span = }; + vd_fields = [(Ast.TyVar { Ast.name = "T"; span = })]; + vd_ret_ty = None }]) + }] + } diff --git a/test/test_golden.ml b/test/test_golden.ml new file mode 100644 index 0000000..cc9b069 --- /dev/null +++ b/test/test_golden.ml @@ -0,0 +1,110 @@ +(** Golden tests for parser/AST output *) + +open Affinescript + +(** Directory containing golden test files *) +let golden_dir = "test/golden" + +(** Read file contents *) +let read_file path = + let chan = open_in path in + let content = really_input_string chan (in_channel_length chan) in + close_in chan; + content + +(** Check if file exists *) +let file_exists path = + try + let _ = Unix.stat path in + true + with Unix.Unix_error (Unix.ENOENT, _, _) -> false + +(** Normalize AST output for comparison (remove span details) *) +let normalize_ast ast_str = + (* Remove span information for stable comparison *) + let re_span = Str.regexp {|{ Span.file = "[^"]*"; start_pos = [^}]*; end_pos = [^}]* }|} in + let without_spans = Str.global_replace re_span "" ast_str in + (* Normalize whitespace *) + let re_ws = Str.regexp "[ \t\n\r]+" in + Str.global_replace re_ws " " without_spans + +(** Parse a file and return the AST as a string *) +let parse_to_string path = + try + let ast = Parse_driver.parse_file path in + let ast_str = Ast.show_program ast in + Ok ast_str + with + | Parse_driver.Parse_error (msg, span) -> + Error (Printf.sprintf "Parse error at %s: %s" (Span.show span) msg) + | e -> + Error (Printf.sprintf "Error: %s" (Printexc.to_string e)) + +(** Run a single golden test *) +let run_golden_test ~source_path ~expected_path () = + match parse_to_string source_path with + | Error msg -> + Alcotest.fail (Printf.sprintf "Failed to parse %s: %s" source_path msg) + | Ok actual -> + if not (file_exists expected_path) then + Alcotest.fail (Printf.sprintf "Expected file not found: %s\nActual output:\n%s" expected_path actual) + else + let expected = read_file expected_path in + let norm_actual = normalize_ast actual in + let norm_expected = normalize_ast expected in + if norm_actual <> norm_expected then begin + Printf.eprintf "\n=== EXPECTED ===\n%s\n" expected; + Printf.eprintf "\n=== ACTUAL ===\n%s\n" actual; + Alcotest.fail "AST output does not match expected" + end + +(** Create a test case from a .as file *) +let test_case_of_file filename = + let base = Filename.chop_extension filename in + let source_path = Filename.concat golden_dir filename in + let expected_path = Filename.concat golden_dir (base ^ ".expected") in + Alcotest.test_case base `Quick (run_golden_test ~source_path ~expected_path) + +(** Discover all .as files in golden directory *) +let discover_tests () = + if not (Sys.file_exists golden_dir) then + [] + else + let files = Sys.readdir golden_dir in + files + |> Array.to_list + |> List.filter (fun f -> Filename.check_suffix f ".as") + |> List.sort String.compare + |> List.map test_case_of_file + +(** Run parser on examples directory and check they all parse *) +let examples_dir = "examples" + +let run_example_parse_test filename () = + let path = Filename.concat examples_dir filename in + match parse_to_string path with + | Error msg -> + Alcotest.fail (Printf.sprintf "Failed to parse %s: %s" path msg) + | Ok _ast -> + (* Just check it parses successfully *) + () + +(** Discover all examples and create parse tests *) +let discover_example_tests () = + if not (Sys.file_exists examples_dir) then + [] + else + let files = Sys.readdir examples_dir in + files + |> Array.to_list + |> List.filter (fun f -> Filename.check_suffix f ".as") + |> List.sort String.compare + |> List.map (fun f -> + let base = Filename.chop_extension f in + Alcotest.test_case ("parse " ^ base) `Quick (run_example_parse_test f)) + +(** All golden tests *) +let tests = discover_tests () + +(** All example parse tests *) +let example_tests = discover_example_tests () diff --git a/test/test_main.ml b/test/test_main.ml index 4de1762..9499e0c 100644 --- a/test/test_main.ml +++ b/test/test_main.ml @@ -5,4 +5,6 @@ let () = [ ("Lexer", Test_lexer.tests); ("Parser", Test_parser.tests); + ("Golden", Test_golden.tests); + ("Examples", Test_golden.example_tests); ]