diff --git a/DECISIONS.md b/DECISIONS.md new file mode 100644 index 0000000..6b52818 --- /dev/null +++ b/DECISIONS.md @@ -0,0 +1,257 @@ +# AffineScript Design Decisions + +This document records the key design decisions for AffineScript's implementation. + +## 1. Runtime Model + +**Decision**: Minimal runtime, rely on host for most services + +- Runtime is small and focused on AffineScript-specific needs +- No garbage collector for most data (ownership handles memory) +- Small tracing GC only for cyclic ω-quantity data (opt-in) +- Host provides: I/O, networking, filesystem, timers + +**Rationale**: Keeps WASM binaries small, maximizes portability, leverages host capabilities. + +## 2. Effect Compilation Strategy + +**Decision**: Evidence-passing (Koka-style) + +- Effects compiled via evidence-passing transformation +- Each effect operation receives an "evidence" parameter +- Handlers install evidence at runtime +- One-shot continuations optimized to avoid allocation + +**Rationale**: Better performance than CPS, proven in Koka, good balance of complexity/speed. + +**Implementation**: +``` +// Source +handle computation() with { + return x → x, + get(_, k) → resume(k, state) +} + +// Compiled (evidence-passing) +computation({ + get: (ev, k) => k(state) +}) +``` + +## 3. WASM Target + +**Decision**: WASM core + WASI, with Component Model readiness + +| Feature | Decision | +|---------|----------| +| WASM Core | ✅ Required baseline | +| WASM GC | ❌ Not required (ownership handles memory) | +| WASI | ✅ For CLI/server use cases | +| Component Model | ✅ Design for future compatibility | +| Threads | ⚠️ Optional, for parallel effects | + +**Rationale**: Broad compatibility now, future-proofed for Component Model. + +## 4. SMT Solver + +**Decision**: Z3 as optional external dependency + +- Z3 bindings (ocaml-z3) for refinement type checking +- SMT is optional: refinements work without it (runtime checks) +- Can be disabled for faster compilation +- Future: support CVC5 as alternative + +**Configuration**: +```nickel +# affinescript.ncl +{ + smt = { + enabled = true, + solver = "z3", + timeout_ms = 5000, + } +} +``` + +## 5. Package Manager + +**Decision**: Workspace-aware, Cargo-inspired + +- Single `affine.toml` manifest per package +- Workspace support for monorepos +- Lock file for reproducibility +- Content-addressed storage (like pnpm) +- Written in Rust + +**Manifest format**: +```toml +[package] +name = "my-project" +version = "0.1.0" +edition = "2024" + +[dependencies] +std = "1.0" +http = { version = "0.5", features = ["async"] } + +[dev-dependencies] +test = "1.0" +``` + +## 6. Standard Library Philosophy + +**Decision**: Small core + blessed packages + +**Core (always available)**: +- `Prelude`: Basic types, traits, operators +- `Option`, `Result`: Error handling +- `List`, `Vec`, `Array`: Collections +- `String`, `Char`: Text + +**Blessed Effects (in std)**: +- `IO`: Console, file system +- `Exn`: Exceptions +- `State`: Mutable state +- `Async`: Async/await +- `Reader`: Environment access + +**Community Packages**: +- HTTP, JSON, databases, etc. +- Not in std, but curated/recommended + +## 7. Self-Hosting + +**Decision**: Long-term goal, not immediate priority + +- Phase 1-4: OCaml compiler +- Phase 5+: Gradually rewrite in AffineScript +- Start with: lexer, parser (simpler) +- End with: type checker, codegen (complex) + +**Timeline**: After 1.0 stable release + +## 8. Interop Priority + +**Decision**: JavaScript first, Rust second + +| Target | Priority | Method | +|--------|----------|--------| +| JavaScript | 🥇 Primary | wasm-bindgen, host bindings | +| Rust | 🥈 Secondary | Native FFI for tools | +| C | 🥉 Tertiary | Via Rust FFI | + +**Rationale**: WASM's primary deployment is web/JS; tooling benefits from Rust. + +## 9. Error Messages + +**Decision**: Rust-style elaborate diagnostics + +- Multi-line errors with source context +- Color-coded by severity +- Suggestions for fixes +- Error codes with documentation links +- Machine-readable JSON output option + +**Example**: +``` +error[E0312]: cannot borrow `x` as mutable because it is already borrowed + --> src/main.afs:12:5 + | +10 | let r = &x; + | -- immutable borrow occurs here +11 | +12 | mutate(&mut x); + | ^^^^^^ mutable borrow occurs here +13 | +14 | use(r); + | - immutable borrow later used here + | + = help: consider moving the mutable borrow before the immutable borrow +``` + +## 10. Proof Assistant Mode + +**Decision**: Refinement types with SMT only (no interactive proving) + +- Refinements checked via SMT solver +- No tactic language or proof terms +- Proofs are erased (quantity 0) +- Future: optional Lean/Coq extraction for critical code + +**Rationale**: Practical verification without complexity of full theorem prover. + +## 11. Primary Use Cases + +**Decision**: Priority order + +1. **Web applications** (frontend + backend) +2. **CLI tools** +3. **Libraries/packages** +4. **Embedded/WASM plugins** +5. **Scientific computing** (future) + +## 12. Community Model + +**Decision**: Benevolent dictator initially, open governance post-1.0 + +- Pre-1.0: Core team makes decisions quickly +- Post-1.0: RFC process for major changes +- Open source from day one (Apache-2.0 OR MIT) +- Community contributions welcome + +## 13. Backward Compatibility + +**Decision**: Breaking changes during 0.x, strict semver from 1.0 + +- 0.x releases may break compatibility +- Migration guides for breaking changes +- 1.0+ follows strict semver +- Edition system for language-level changes (like Rust) + +--- + +## Technology Stack Summary + +| Layer | Technology | Notes | +|-------|------------|-------| +| Compiler | OCaml 5.1+ | Existing codebase | +| Parser | Menhir | Existing | +| Lexer | Sedlex | Existing | +| SMT | Z3 (ocaml-z3) | Optional | +| Runtime | Rust | WASM target | +| Allocator | Custom (Rust) | Ownership-optimized | +| Package Manager | Rust | CLI tool | +| LSP Server | Rust | Performance | +| Formatter | OCaml | Shares AST | +| REPL | OCaml | Interpreter mode | +| Web Tooling | ReScript + Deno | Per standards | +| Build Meta | Deno | Per standards | +| Config | Nickel | Per standards | +| Docs | Custom generator | From types | + +--- + +## File Format Decisions + +| Purpose | Format | Extension | +|---------|--------|-----------| +| Source code | AffineScript | `.afs` | +| Package manifest | TOML | `affine.toml` | +| Lock file | TOML | `affine.lock` | +| Configuration | Nickel | `*.ncl` | +| Build scripts | Deno/TS | `*.ts` | +| Documentation | Markdown | `*.md` | + +--- + +## Versioning + +- **Language**: `2024` edition (year-based) +- **Compiler**: Semver (0.1.0, 0.2.0, ... 1.0.0) +- **Stdlib**: Tied to compiler version +- **Packages**: Independent semver + +--- + +*Last updated: 2024* +*Status: Approved* diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..e936d6f --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,500 @@ +# AffineScript Implementation Roadmap + +## Overview + +This roadmap outlines the path from current state (lexer + parser) to a complete, production-ready language. + +``` +Current State Goal + │ │ + ▼ ▼ +┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ +│ Lexer │ → │ Parser │ → │ Type │ → │ Codegen │ → │ Runtime │ +│ ✅ │ │ ✅ │ │ Checker │ │ WASM │ │ Rust │ +└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ + │ + ┌────────────┼────────────┐ + ▼ ▼ ▼ + ┌────────┐ ┌─────────┐ ┌─────────┐ + │ Borrow │ │ Effect │ │Refinemt │ + │Checker │ │Inference│ │ SMT │ + └────────┘ └─────────┘ └─────────┘ +``` + +--- + +## Phase 0: Foundation ✅ COMPLETE + +**Status**: Done + +- [x] Project structure (Dune) +- [x] Lexer (Sedlex) +- [x] Parser (Menhir) +- [x] AST definitions +- [x] Error infrastructure +- [x] CLI skeleton +- [x] Test framework +- [x] Academic documentation + +--- + +## Phase 1: Core Type System + +**Goal**: Type check simple programs without effects or ownership + +**Duration**: 8-12 weeks + +### 1.1 Name Resolution +- [ ] Symbol table structure +- [ ] Scope management +- [ ] Module path resolution +- [ ] Import resolution +- [ ] Visibility checking (pub, pub(crate), etc.) + +**Files**: `lib/resolve.ml`, `lib/symbol.ml` + +### 1.2 Kind Checking +- [ ] Kind inference +- [ ] Kind unification +- [ ] Higher-kinded types +- [ ] Row kinds +- [ ] Effect kinds + +**Files**: `lib/kind.ml` + +### 1.3 Type Checker Core +- [ ] Bidirectional type checking +- [ ] Type synthesis +- [ ] Type checking mode +- [ ] Subsumption rule + +**Files**: `lib/typecheck.ml`, `lib/check.ml`, `lib/synth.ml` + +### 1.4 Unification Engine +- [ ] Type unification +- [ ] Occurs check +- [ ] Union-find structure +- [ ] Error messages for unification failures + +**Files**: `lib/unify.ml`, `lib/union_find.ml` + +### 1.5 Polymorphism +- [ ] Let-generalization +- [ ] Instantiation +- [ ] Value restriction +- [ ] Type application + +### 1.6 Row Unification +- [ ] Record row unification +- [ ] Variant row unification +- [ ] Lacks constraints +- [ ] Row rewriting + +**Files**: `lib/row_unify.ml` + +### 1.7 Basic Error Messages +- [ ] Type mismatch errors +- [ ] Undefined variable errors +- [ ] Source location tracking +- [ ] Suggested fixes + +**Milestone**: `affinescript check` works for simple programs + +--- + +## Phase 2: Quantities & Effects + +**Goal**: Full QTT and effect system + +**Duration**: 8-12 weeks + +### 2.1 Quantity Checking +- [ ] Quantity context tracking +- [ ] Context scaling +- [ ] Context addition +- [ ] Usage analysis +- [ ] Quantity error messages + +**Files**: `lib/quantity.ml` + +### 2.2 Effect Inference +- [ ] Effect signature checking +- [ ] Effect row inference +- [ ] Handler typing +- [ ] Effect unification +- [ ] Effect polymorphism + +**Files**: `lib/effect.ml`, `lib/effect_infer.ml` + +### 2.3 Handler Verification +- [ ] Handler completeness +- [ ] Return clause typing +- [ ] Operation clause typing +- [ ] Continuation typing (linear vs multi-shot) + +### 2.4 Effect Error Messages +- [ ] Unhandled effect errors +- [ ] Effect mismatch errors +- [ ] Handler clause errors + +**Milestone**: Effects and quantities fully checked + +--- + +## Phase 3: Ownership & Borrowing + +**Goal**: Memory safety verification + +**Duration**: 6-10 weeks + +### 3.1 Ownership Tracking +- [ ] Move semantics +- [ ] Ownership transfer +- [ ] Drop insertion +- [ ] Copy trait handling + +**Files**: `lib/ownership.ml` + +### 3.2 Borrow Checker +- [ ] Borrow tracking +- [ ] Conflict detection +- [ ] Non-lexical lifetimes +- [ ] Dataflow analysis + +**Files**: `lib/borrow.ml`, `lib/dataflow.ml` + +### 3.3 Lifetime Inference +- [ ] Lifetime constraints +- [ ] Lifetime solving +- [ ] Lifetime elision +- [ ] Lifetime bounds + +**Files**: `lib/lifetime.ml` + +### 3.4 Ownership-Quantity Integration +- [ ] Quantity affects ownership +- [ ] Linear ownership (1) +- [ ] Shared ownership (ω + Copy) +- [ ] Erased types (0) + +**Milestone**: `affinescript check` catches all ownership errors + +--- + +## Phase 4: Dependent Types & Refinements + +**Goal**: Dependent types with SMT verification + +**Duration**: 6-10 weeks + +### 4.1 Dependent Type Checking +- [ ] Π-type checking +- [ ] Σ-type checking +- [ ] Type-level computation +- [ ] Normalization + +**Files**: `lib/dependent.ml`, `lib/normalize.ml` + +### 4.2 SMT Integration +- [ ] Z3 OCaml bindings +- [ ] Predicate translation +- [ ] Validity checking +- [ ] Model extraction (for errors) + +**Files**: `lib/smt.ml`, `lib/smt_translate.ml` + +### 4.3 Refinement Checking +- [ ] Refinement subtyping +- [ ] VC generation +- [ ] SMT queries +- [ ] Refinement error messages + +**Files**: `lib/refinement.ml` + +### 4.4 Totality Checking (Optional) +- [ ] Termination analysis +- [ ] Structural recursion +- [ ] Custom measures +- [ ] Total annotation + +**Files**: `lib/totality.ml` + +**Milestone**: Full type system complete + +--- + +## Phase 5: Code Generation + +**Goal**: Compile to WebAssembly + +**Duration**: 10-14 weeks + +### 5.1 Intermediate Representation +- [ ] ANF transformation +- [ ] Effect evidence insertion +- [ ] Closure conversion +- [ ] Lambda lifting + +**Files**: `lib/ir.ml`, `lib/anf.ml`, `lib/closure.ml` + +### 5.2 Optimization +- [ ] Dead code elimination +- [ ] Inlining +- [ ] Constant folding +- [ ] Linearity-aware optimizations + +**Files**: `lib/optimize.ml` + +### 5.3 WASM Code Generation +- [ ] WASM module structure +- [ ] Function compilation +- [ ] Type mapping +- [ ] Memory layout + +**Files**: `lib/wasm.ml`, `lib/codegen.ml` + +### 5.4 Effect Compilation +- [ ] Evidence-passing transform +- [ ] Handler compilation +- [ ] Continuation representation +- [ ] One-shot optimization + +**Files**: `lib/effect_compile.ml` + +### 5.5 Ownership Erasure +- [ ] Drop insertion points +- [ ] Move compilation +- [ ] Borrow elimination + +**Milestone**: `affinescript compile` produces working WASM + +--- + +## Phase 6: Runtime + +**Goal**: Minimal Rust runtime for WASM + +**Duration**: 6-8 weeks + +### 6.1 Runtime Core (Rust) +- [ ] Project structure +- [ ] Memory allocator +- [ ] Panic handling +- [ ] Stack management + +**Location**: `runtime/` (new Rust crate) + +### 6.2 Effect Runtime +- [ ] Evidence structures +- [ ] Handler frames +- [ ] Continuation allocation +- [ ] Resume implementation + +### 6.3 GC (Optional) +- [ ] Mark-sweep for ω cycles +- [ ] Root tracking +- [ ] Finalization + +### 6.4 Host Bindings +- [ ] WASI integration +- [ ] JavaScript interop +- [ ] Console I/O +- [ ] File system + +**Milestone**: Programs run correctly + +--- + +## Phase 7: Standard Library + +**Goal**: Usable standard library + +**Duration**: Ongoing (8+ weeks initial) + +### 7.1 Core Types +- [ ] Prelude +- [ ] Option, Result +- [ ] Tuples +- [ ] Unit, Bool, Never + +**Location**: `stdlib/core/` + +### 7.2 Collections +- [ ] List +- [ ] Vec (growable array) +- [ ] HashMap +- [ ] HashSet +- [ ] BTreeMap + +**Location**: `stdlib/collections/` + +### 7.3 Text +- [ ] String +- [ ] Char +- [ ] Formatting (Display, Debug) +- [ ] Parsing + +**Location**: `stdlib/text/` + +### 7.4 Effects +- [ ] IO effect +- [ ] State effect +- [ ] Exn effect +- [ ] Async effect +- [ ] Reader effect + +**Location**: `stdlib/effects/` + +### 7.5 Numeric +- [ ] Int, Float +- [ ] Numeric traits +- [ ] Math functions + +**Location**: `stdlib/num/` + +**Milestone**: Self-sufficient programs possible + +--- + +## Phase 8: Tooling + +**Goal**: Developer experience + +**Duration**: Ongoing (10+ weeks initial) + +### 8.1 Language Server (Rust) +- [ ] LSP implementation +- [ ] Diagnostics +- [ ] Hover information +- [ ] Go to definition +- [ ] Find references +- [ ] Completion +- [ ] Rename + +**Location**: `tools/affinescript-lsp/` + +### 8.2 Formatter (OCaml) +- [ ] Canonical formatting +- [ ] Configuration options +- [ ] Editor integration + +**Location**: `lib/format.ml`, `bin/fmt.ml` + +### 8.3 REPL (OCaml) +- [ ] Expression evaluation +- [ ] Type printing +- [ ] Effect handling +- [ ] History + +**Location**: `bin/repl.ml` + +### 8.4 Package Manager (Rust) +- [ ] Manifest parsing +- [ ] Dependency resolution +- [ ] Package fetching +- [ ] Lock file +- [ ] Publishing + +**Location**: `tools/affine-pkg/` + +### 8.5 Documentation Generator +- [ ] Doc comments +- [ ] API documentation +- [ ] Search index + +**Location**: `tools/affine-doc/` + +**Milestone**: Professional development experience + +--- + +## Phase 9: Ecosystem + +**Goal**: Community and adoption + +**Duration**: Ongoing + +### 9.1 VS Code Extension +- [ ] Syntax highlighting +- [ ] LSP client +- [ ] Snippets +- [ ] Debugging + +**Location**: `editors/vscode/` + +### 9.2 Playground +- [ ] Web REPL +- [ ] Shareable links +- [ ] Examples + +**Location**: `playground/` + +### 9.3 Package Registry +- [ ] Registry backend +- [ ] Web frontend +- [ ] CLI publishing + +### 9.4 Documentation Site +- [ ] Tutorial +- [ ] Language reference +- [ ] API docs +- [ ] Blog + +### 9.5 Example Projects +- [ ] Hello World +- [ ] Web server +- [ ] CLI tool +- [ ] Library + +**Milestone**: Community can build real projects + +--- + +## Version Milestones + +| Version | Contents | Target | +|---------|----------|--------| +| 0.1.0 | Type checker (no effects/ownership) | Phase 1 | +| 0.2.0 | Full type system | Phase 2-4 | +| 0.3.0 | WASM compilation | Phase 5-6 | +| 0.4.0 | Standard library | Phase 7 | +| 0.5.0 | Tooling (LSP, formatter) | Phase 8 | +| 0.9.0 | Release candidate | Phase 9 | +| 1.0.0 | Stable release | All phases | + +--- + +## Resource Requirements + +### Core Team Skills Needed +- OCaml (compiler) +- Rust (runtime, tooling) +- Type theory (checker design) +- WASM (code generation) +- ReScript/Deno (web tooling) + +### Infrastructure +- CI/CD (GitHub Actions) +- Package registry hosting +- Documentation hosting +- Playground hosting + +--- + +## Success Criteria + +### 0.1.0 (Type Checker MVP) +- [ ] 100+ test programs type check correctly +- [ ] Error messages are helpful +- [ ] <1s for typical file + +### 1.0.0 (Stable Release) +- [ ] All language features work +- [ ] Performance competitive with Rust/Go +- [ ] 10+ community packages +- [ ] 3+ production users +- [ ] Complete documentation + +--- + +*Last updated: 2024* diff --git a/lib/borrow.ml b/lib/borrow.ml new file mode 100644 index 0000000..ed8f01d --- /dev/null +++ b/lib/borrow.ml @@ -0,0 +1,258 @@ +(* SPDX-License-Identifier: Apache-2.0 OR MIT *) +(* Copyright 2024 AffineScript Contributors *) + +(** Borrow checker for ownership verification. + + This module implements borrow checking to ensure memory safety: + - No use after move + - No conflicting borrows + - Borrows don't outlive owners + + [IMPL-DEP: Phase 3] +*) + +open Ast + +(** A place is an l-value that can be borrowed *) +type place = + | PlaceVar of Symbol.symbol_id + | PlaceField of place * string + | PlaceIndex of place * int option (** None for dynamic index *) + | PlaceDeref of place +[@@deriving show] + +(** Borrow kind *) +type borrow_kind = + | Shared (** Immutable borrow (&) *) + | Exclusive (** Mutable borrow (&mut) *) +[@@deriving show, eq] + +(** A borrow record *) +type borrow = { + b_place : place; + b_kind : borrow_kind; + b_span : Span.t; + b_id : int; +} +[@@deriving show] + +(** Borrow checker state *) +type state = { + (** Active borrows *) + mutable borrows : borrow list; + + (** Moved places *) + mutable moved : place list; + + (** Next borrow ID *) + mutable next_id : int; +} + +(** Borrow checker errors *) +type borrow_error = + | UseAfterMove of place * Span.t * Span.t (** place, use site, move site *) + | ConflictingBorrow of borrow * borrow + | BorrowOutlivesOwner of borrow * Symbol.symbol_id + | MoveWhileBorrowed of place * borrow + | CannotMoveOutOfBorrow of place * borrow + | CannotBorrowAsMutable of place * Span.t +[@@deriving show] + +type 'a result = ('a, borrow_error) Result.t + +(** Create a new borrow checker state *) +let create () : state = + { + borrows = []; + moved = []; + next_id = 0; + } + +(** Generate a fresh borrow ID *) +let fresh_id (state : state) : int = + let id = state.next_id in + state.next_id <- id + 1; + id + +(** Check if two places overlap *) +let rec places_overlap (p1 : place) (p2 : place) : bool = + match (p1, p2) with + | (PlaceVar v1, PlaceVar v2) -> v1 = v2 + | (PlaceField (base1, _), PlaceField (base2, _)) -> + places_overlap base1 base2 + | (PlaceVar _, PlaceField (base, _)) + | (PlaceField (base, _), PlaceVar _) -> + places_overlap p1 base || places_overlap base p2 + | (PlaceDeref p1', PlaceDeref p2') -> + places_overlap p1' p2' + | _ -> false + +(** Check if a place is moved *) +let is_moved (state : state) (place : place) : bool = + List.exists (fun moved_place -> places_overlap place moved_place) state.moved + +(** Check if a borrow conflicts with existing borrows *) +let find_conflicting_borrow (state : state) (new_borrow : borrow) : borrow option = + List.find_opt (fun existing -> + places_overlap new_borrow.b_place existing.b_place && + (new_borrow.b_kind = Exclusive || existing.b_kind = Exclusive) + ) state.borrows + +(** Record a move *) +let record_move (state : state) (place : place) (span : Span.t) : unit result = + (* Check for active borrows *) + match List.find_opt (fun b -> places_overlap place b.b_place) state.borrows with + | Some borrow -> Error (MoveWhileBorrowed (place, borrow)) + | None -> + state.moved <- place :: state.moved; + Ok () + +(** Record a borrow *) +let record_borrow (state : state) (place : place) (kind : borrow_kind) + (span : Span.t) : borrow result = + (* Check if moved *) + if is_moved state place then + Error (UseAfterMove (place, span, span)) (* TODO: Track move site *) + else + let new_borrow = { + b_place = place; + b_kind = kind; + b_span = span; + b_id = fresh_id state; + } in + match find_conflicting_borrow state new_borrow with + | Some conflict -> Error (ConflictingBorrow (new_borrow, conflict)) + | None -> + state.borrows <- new_borrow :: state.borrows; + Ok new_borrow + +(** End a borrow *) +let end_borrow (state : state) (borrow : borrow) : unit = + state.borrows <- List.filter (fun b -> b.b_id <> borrow.b_id) state.borrows + +(** Check a use of a place *) +let check_use (state : state) (place : place) (span : Span.t) : unit result = + if is_moved state place then + Error (UseAfterMove (place, span, span)) + else + Ok () + +(** Convert an expression to a place (if it's an l-value) *) +let rec expr_to_place (symbols : Symbol.t) (expr : expr) : place option = + match expr with + | EVar id -> + begin match Symbol.lookup symbols id.id_name with + | Some sym -> Some (PlaceVar sym.sym_id) + | None -> None + end + | ERecordAccess (base, field, _) -> + begin match expr_to_place symbols base with + | Some base_place -> Some (PlaceField (base_place, field.id_name)) + | None -> None + end + | EIndex (base, _, _) -> + begin match expr_to_place symbols base with + | Some base_place -> Some (PlaceIndex (base_place, None)) + | None -> None + end + | _ -> None + +(** Check borrows in an expression *) +let rec check_expr (state : state) (symbols : Symbol.t) (expr : expr) : unit result = + match expr with + | EVar id -> + begin match expr_to_place symbols expr with + | Some place -> check_use state place id.id_span + | None -> Ok () + end + + | ELit _ -> Ok () + + | EApp (func, arg, _) -> + let* () = check_expr state symbols func in + check_expr state symbols arg + + | ELam lam -> + check_expr state symbols lam.lam_body + + | ELet lb -> + let* () = check_expr state symbols lb.lb_rhs in + check_expr state symbols lb.lb_body + + | EIf (cond, then_, else_, _) -> + let* () = check_expr state symbols cond in + (* TODO: Proper branch handling - save/restore state *) + let* () = check_expr state symbols then_ in + check_expr state symbols else_ + + | ECase (scrut, branches, _) -> + let* () = check_expr state symbols scrut in + List.fold_left (fun acc branch -> + match acc with + | Error e -> Error e + | Ok () -> check_expr state symbols branch.cb_body + ) (Ok ()) branches + + | ETuple (exprs, _) -> + List.fold_left (fun acc e -> + match acc with + | Error e -> Error e + | Ok () -> check_expr state symbols e + ) (Ok ()) exprs + + | ERecord (fields, _) -> + List.fold_left (fun acc (_, e) -> + match acc with + | Error e -> Error e + | Ok () -> check_expr state symbols e + ) (Ok ()) fields + + | ERecordAccess (base, _, _) -> + check_expr state symbols base + + | ERecordUpdate (base, _, value, _) -> + let* () = check_expr state symbols base in + check_expr state symbols value + + | EBlock (exprs, _) -> + List.fold_left (fun acc e -> + match acc with + | Error e -> Error e + | Ok () -> check_expr state symbols e + ) (Ok ()) exprs + + | EBinOp (left, _, right, _) -> + let* () = check_expr state symbols left in + check_expr state symbols right + + | _ -> Ok () + +(* Result bind *) +let ( let* ) = Result.bind + +(** Check a function *) +let check_function (symbols : Symbol.t) (fd : fun_decl) : unit result = + let state = create () in + match fd.fd_body with + | Some body -> check_expr state symbols body + | None -> Ok () + +(** Check a program *) +let check_program (symbols : Symbol.t) (program : program) : unit result = + List.fold_left (fun acc decl -> + match acc with + | Error e -> Error e + | Ok () -> + match decl with + | DFun fd -> check_function symbols fd + | _ -> Ok () + ) (Ok ()) program.prog_decls + +(* TODO: Phase 3 implementation + - [ ] Non-lexical lifetimes + - [ ] Dataflow analysis for precise tracking + - [ ] Lifetime inference + - [ ] Better error messages with suggestions + - [ ] Integration with quantity checking + - [ ] Effect interaction with borrows +*) diff --git a/lib/quantity.ml b/lib/quantity.ml new file mode 100644 index 0000000..de17998 --- /dev/null +++ b/lib/quantity.ml @@ -0,0 +1,225 @@ +(* SPDX-License-Identifier: Apache-2.0 OR MIT *) +(* Copyright 2024 AffineScript Contributors *) + +(** Quantity checking for QTT. + + This module implements quantity checking for Quantitative Type Theory. + It tracks how many times each variable is used and verifies that usage + matches the declared quantity annotations. +*) + +open Ast + +(** Usage count for a variable *) +type usage = + | UZero (** Not used *) + | UOne (** Used exactly once *) + | UMany (** Used multiple times *) +[@@deriving show, eq] + +(** Combine two usages (for branching) *) +let join (u1 : usage) (u2 : usage) : usage = + match (u1, u2) with + | (UZero, u) | (u, UZero) -> u + | (UOne, UOne) -> UOne + | _ -> UMany + +(** Add two usages (for sequencing) *) +let add (u1 : usage) (u2 : usage) : usage = + match (u1, u2) with + | (UZero, u) | (u, UZero) -> u + | _ -> UMany + +(** Quantity errors *) +type quantity_error = + | LinearVariableUnused of ident + | LinearVariableUsedMultiple of ident + | ErasedVariableUsed of ident + | QuantityMismatch of ident * quantity * usage +[@@deriving show] + +type 'a result = ('a, quantity_error * Span.t) Result.t + +(** Quantity context: maps variables to their quantities and current usage *) +type context = { + quantities : (Symbol.symbol_id, quantity) Hashtbl.t; + usages : (Symbol.symbol_id, usage) Hashtbl.t; +} + +(** Create a new quantity context *) +let create () : context = + { + quantities = Hashtbl.create 32; + usages = Hashtbl.create 32; + } + +(** Record a variable's declared quantity *) +let declare (ctx : context) (sym : Symbol.symbol) (q : quantity) : unit = + Hashtbl.replace ctx.quantities sym.sym_id q; + Hashtbl.replace ctx.usages sym.sym_id UZero + +(** Record a use of a variable *) +let use (ctx : context) (sym : Symbol.symbol) : unit = + match Hashtbl.find_opt ctx.usages sym.sym_id with + | Some UZero -> Hashtbl.replace ctx.usages sym.sym_id UOne + | Some UOne -> Hashtbl.replace ctx.usages sym.sym_id UMany + | Some UMany -> () + | None -> () + +(** Check that a variable's usage matches its quantity *) +let check_variable (ctx : context) (sym : Symbol.symbol) (id : ident) + : unit result = + let q = Hashtbl.find_opt ctx.quantities sym.sym_id |> Option.value ~default:QOmega in + let u = Hashtbl.find_opt ctx.usages sym.sym_id |> Option.value ~default:UZero in + match (q, u) with + (* Erased: must not be used *) + | (QZero, UZero) -> Ok () + | (QZero, _) -> Error (ErasedVariableUsed id, id.id_span) + (* Linear: must be used exactly once (or zero for affine) *) + | (QOne, UZero) -> Ok () (* Affine: can drop *) + | (QOne, UOne) -> Ok () + | (QOne, UMany) -> Error (LinearVariableUsedMultiple id, id.id_span) + (* Unrestricted: any usage is fine *) + | (QOmega, _) -> Ok () + +(** Analyze usage in an expression *) +let rec analyze_expr (ctx : context) (symbols : Symbol.t) (expr : expr) : unit = + match expr with + | EVar id -> + begin match Symbol.lookup symbols id.id_name with + | Some sym -> use ctx sym + | None -> () + end + + | ELit _ -> () + + | EApp (func, arg, _) -> + analyze_expr ctx symbols func; + analyze_expr ctx symbols arg + + | ELam lam -> + (* Parameters are bound; analyze body *) + analyze_expr ctx symbols lam.lam_body + + | ELet lb -> + analyze_expr ctx symbols lb.lb_rhs; + analyze_expr ctx symbols lb.lb_body + + | EIf (cond, then_, else_, _) -> + analyze_expr ctx symbols cond; + (* For branches, we need to join usages *) + (* TODO: Proper branch handling *) + analyze_expr ctx symbols then_; + analyze_expr ctx symbols else_ + + | ECase (scrut, branches, _) -> + analyze_expr ctx symbols scrut; + List.iter (fun branch -> + analyze_expr ctx symbols branch.cb_body + ) branches + + | ETuple (exprs, _) -> + List.iter (analyze_expr ctx symbols) exprs + + | ERecord (fields, _) -> + List.iter (fun (_, e) -> analyze_expr ctx symbols e) fields + + | ERecordAccess (e, _, _) -> + analyze_expr ctx symbols e + + | ERecordUpdate (base, _, value, _) -> + analyze_expr ctx symbols base; + analyze_expr ctx symbols value + + | EBlock (exprs, _) -> + List.iter (analyze_expr ctx symbols) exprs + + | EBinOp (left, _, right, _) -> + analyze_expr ctx symbols left; + analyze_expr ctx symbols right + + | EUnaryOp (_, e, _) -> + analyze_expr ctx symbols e + + | EHandle (body, handler, _) -> + analyze_expr ctx symbols body; + begin match handler.h_return with + | Some (_, e) -> analyze_expr ctx symbols e + | None -> () + end; + List.iter (fun clause -> + analyze_expr ctx symbols clause.oc_body + ) handler.h_ops + + | EPerform (_, arg, _) -> + analyze_expr ctx symbols arg + + | _ -> () + +(** Check quantities for a function *) +let check_function (symbols : Symbol.t) (fd : fun_decl) : unit result = + let ctx = create () in + (* Declare parameter quantities *) + List.iter (fun (id, _, q_opt) -> + let q = Option.value q_opt ~default:QOmega in + match Symbol.lookup symbols id.id_name with + | Some sym -> declare ctx sym q + | None -> () + ) fd.fd_params; + (* Analyze body *) + begin match fd.fd_body with + | Some body -> analyze_expr ctx symbols body + | None -> () + end; + (* Check all parameters *) + List.fold_left (fun acc (id, _, _) -> + match acc with + | Error e -> Error e + | Ok () -> + match Symbol.lookup symbols id.id_name with + | Some sym -> check_variable ctx sym id + | None -> Ok () + ) (Ok ()) fd.fd_params + +(** Check quantities for a program *) +let check_program (symbols : Symbol.t) (program : program) : unit result = + List.fold_left (fun acc decl -> + match acc with + | Error e -> Error e + | Ok () -> + match decl with + | DFun fd -> check_function symbols fd + | _ -> Ok () + ) (Ok ()) program.prog_decls + +(* Semiring operations for quantity algebra *) + +(** Addition in the quantity semiring *) +let q_add (q1 : quantity) (q2 : quantity) : quantity = + match (q1, q2) with + | (QZero, q) | (q, QZero) -> q + | (QOne, QOne) -> QOmega + | (QOmega, _) | (_, QOmega) -> QOmega + +(** Multiplication in the quantity semiring *) +let q_mul (q1 : quantity) (q2 : quantity) : quantity = + match (q1, q2) with + | (QZero, _) | (_, QZero) -> QZero + | (QOne, q) | (q, QOne) -> q + | (QOmega, QOmega) -> QOmega + +(** Check if q1 ≤ q2 in the quantity ordering *) +let q_le (q1 : quantity) (q2 : quantity) : bool = + match (q1, q2) with + | (QZero, _) -> true + | (_, QOmega) -> true + | (QOne, QOne) -> true + | _ -> false + +(* TODO: Phase 2 implementation + - [ ] Proper branch handling for if/case + - [ ] Quantity polymorphism + - [ ] Integration with type checker + - [ ] Effect interaction with quantities + - [ ] Better error messages +*) diff --git a/lib/resolve.ml b/lib/resolve.ml new file mode 100644 index 0000000..ffbc328 --- /dev/null +++ b/lib/resolve.ml @@ -0,0 +1,356 @@ +(* SPDX-License-Identifier: Apache-2.0 OR MIT *) +(* Copyright 2024 AffineScript Contributors *) + +(** Name resolution pass. + + This module resolves all names in the AST to symbols in the symbol table. + It runs after parsing and before type checking. +*) + +open Ast + +(** Resolution errors *) +type resolve_error = + | UndefinedVariable of ident + | UndefinedType of ident + | UndefinedEffect of ident + | UndefinedModule of ident + | DuplicateDefinition of ident + | VisibilityError of ident * string + | ImportError of string +[@@deriving show] + +(** Resolution result *) +type 'a result = ('a, resolve_error * Span.t) Result.t + +(** Resolution context *) +type context = { + symbols : Symbol.t; + current_module : string list; + imports : (string * Symbol.symbol) list; +} + +(** Create a new resolution context *) +let create_context () : context = + { + symbols = Symbol.create (); + current_module = []; + imports = []; + } + +(** Resolve an identifier *) +let resolve_ident (ctx : context) (id : ident) : Symbol.symbol result = + let name = id.id_name in + match Symbol.lookup ctx.symbols name with + | Some sym -> Ok sym + | None -> Error (UndefinedVariable id, id.id_span) + +(** Resolve a type identifier *) +let resolve_type_ident (ctx : context) (id : ident) : Symbol.symbol result = + let name = id.id_name in + match Symbol.lookup ctx.symbols name with + | Some sym when sym.sym_kind = Symbol.SKType -> Ok sym + | Some sym when sym.sym_kind = Symbol.SKTypeVar -> Ok sym + | Some _ -> Error (UndefinedType id, id.id_span) + | None -> Error (UndefinedType id, id.id_span) + +(** Resolve an effect identifier *) +let resolve_effect_ident (ctx : context) (id : ident) : Symbol.symbol result = + let name = id.id_name in + match Symbol.lookup ctx.symbols name with + | Some sym when sym.sym_kind = Symbol.SKEffect -> Ok sym + | Some _ -> Error (UndefinedEffect id, id.id_span) + | None -> Error (UndefinedEffect id, id.id_span) + +(** Resolve a pattern, binding variables *) +let rec resolve_pattern (ctx : context) (pat : pattern) : context result = + match pat with + | PWild _ -> Ok ctx + | PVar id -> + if Symbol.is_defined_locally ctx.symbols id.id_name then + Error (DuplicateDefinition id, id.id_span) + else begin + let _ = Symbol.define ctx.symbols id.id_name + Symbol.SKVariable id.id_span Private in + Ok ctx + end + | PLit _ -> Ok ctx + | PTuple (pats, _) -> + List.fold_left (fun acc pat -> + match acc with + | Error e -> Error e + | Ok ctx -> resolve_pattern ctx pat + ) (Ok ctx) pats + | PRecord (fields, _, _) -> + List.fold_left (fun acc (_, pat) -> + match acc with + | Error e -> Error e + | Ok ctx -> resolve_pattern ctx pat + ) (Ok ctx) fields + | PConstructor (_, pats, _) -> + List.fold_left (fun acc pat -> + match acc with + | Error e -> Error e + | Ok ctx -> resolve_pattern ctx pat + ) (Ok ctx) pats + | POr (p1, p2, _) -> + (* Both branches must bind the same variables *) + let* ctx1 = resolve_pattern ctx p1 in + let* _ctx2 = resolve_pattern ctx p2 in + Ok ctx1 + | PAs (pat, id, _) -> + let* ctx = resolve_pattern ctx pat in + if Symbol.is_defined_locally ctx.symbols id.id_name then + Error (DuplicateDefinition id, id.id_span) + else begin + let _ = Symbol.define ctx.symbols id.id_name + Symbol.SKVariable id.id_span Private in + Ok ctx + end + +(** Resolve an expression *) +let rec resolve_expr (ctx : context) (expr : expr) : unit result = + match expr with + | EVar id -> + let* _ = resolve_ident ctx id in + Ok () + + | ELit _ -> Ok () + + | EApp (func, arg, _) -> + let* () = resolve_expr ctx func in + resolve_expr ctx arg + + | ELam lam -> + Symbol.enter_scope ctx.symbols (Symbol.ScopeFunction "lambda"); + (* Bind parameters *) + List.iter (fun (id, _, _) -> + let _ = Symbol.define ctx.symbols id.id_name + Symbol.SKVariable id.id_span Private in + () + ) lam.lam_params; + let result = resolve_expr ctx lam.lam_body in + Symbol.exit_scope ctx.symbols; + result + + | ELet lb -> + let* () = resolve_expr ctx lb.lb_rhs in + Symbol.enter_scope ctx.symbols Symbol.ScopeBlock; + let* _ = resolve_pattern ctx lb.lb_pat in + let result = resolve_expr ctx lb.lb_body in + Symbol.exit_scope ctx.symbols; + result + + | EIf (cond, then_, else_, _) -> + let* () = resolve_expr ctx cond in + let* () = resolve_expr ctx then_ in + resolve_expr ctx else_ + + | ECase (scrut, branches, _) -> + let* () = resolve_expr ctx scrut in + List.fold_left (fun acc branch -> + match acc with + | Error e -> Error e + | Ok () -> + Symbol.enter_scope ctx.symbols Symbol.ScopeMatch; + let result = + let* _ = resolve_pattern ctx branch.cb_pat in + let* () = match branch.cb_guard with + | Some g -> resolve_expr ctx g + | None -> Ok () + in + resolve_expr ctx branch.cb_body + in + Symbol.exit_scope ctx.symbols; + result + ) (Ok ()) branches + + | ETuple (exprs, _) -> + List.fold_left (fun acc e -> + match acc with + | Error e -> Error e + | Ok () -> resolve_expr ctx e + ) (Ok ()) exprs + + | ERecord (fields, _) -> + List.fold_left (fun acc (_, e) -> + match acc with + | Error e -> Error e + | Ok () -> resolve_expr ctx e + ) (Ok ()) fields + + | ERecordAccess (e, _, _) -> + resolve_expr ctx e + + | ERecordUpdate (base, _, value, _) -> + let* () = resolve_expr ctx base in + resolve_expr ctx value + + | EArray (elems, _) -> + List.fold_left (fun acc e -> + match acc with + | Error e -> Error e + | Ok () -> resolve_expr ctx e + ) (Ok ()) elems + + | EIndex (arr, idx, _) -> + let* () = resolve_expr ctx arr in + resolve_expr ctx idx + + | EHandle (body, handler, _) -> + let* () = resolve_expr ctx body in + resolve_handler ctx handler + + | EPerform (_, arg, _) -> + resolve_expr ctx arg + + | EResume (arg, _) -> + resolve_expr ctx arg + + | EBlock (exprs, _) -> + Symbol.enter_scope ctx.symbols Symbol.ScopeBlock; + let result = List.fold_left (fun acc e -> + match acc with + | Error e -> Error e + | Ok () -> resolve_expr ctx e + ) (Ok ()) exprs in + Symbol.exit_scope ctx.symbols; + result + + | EBinOp (left, _, right, _) -> + let* () = resolve_expr ctx left in + resolve_expr ctx right + + | EUnaryOp (_, e, _) -> + resolve_expr ctx e + + | ETyApp (e, _, _) -> + resolve_expr ctx e + + | EUnsafe (e, _) -> + resolve_expr ctx e + + | EUnsafeCoerce (e, _, _) -> + resolve_expr ctx e + +and resolve_handler (ctx : context) (handler : handler) : unit result = + (* Resolve return clause *) + let* () = match handler.h_return with + | Some (id, body) -> + Symbol.enter_scope ctx.symbols Symbol.ScopeHandler; + let _ = Symbol.define ctx.symbols id.id_name + Symbol.SKVariable id.id_span Private in + let result = resolve_expr ctx body in + Symbol.exit_scope ctx.symbols; + result + | None -> Ok () + in + (* Resolve operation clauses *) + List.fold_left (fun acc clause -> + match acc with + | Error e -> Error e + | Ok () -> + Symbol.enter_scope ctx.symbols Symbol.ScopeHandler; + (* Bind operation parameters and continuation *) + List.iter (fun (id, _) -> + let _ = Symbol.define ctx.symbols id.id_name + Symbol.SKVariable id.id_span Private in + () + ) clause.oc_params; + let _ = Symbol.define ctx.symbols clause.oc_resume.id_name + Symbol.SKVariable clause.oc_resume.id_span Private in + let result = resolve_expr ctx clause.oc_body in + Symbol.exit_scope ctx.symbols; + result + ) (Ok ()) handler.h_ops + +(** Resolve a top-level declaration *) +let resolve_decl (ctx : context) (decl : decl) : unit result = + match decl with + | DFun fd -> + (* First, define the function itself for recursion *) + let _ = Symbol.define ctx.symbols fd.fd_name.id_name + Symbol.SKFunction fd.fd_name.id_span fd.fd_vis in + (* Then resolve the body *) + Symbol.enter_scope ctx.symbols (Symbol.ScopeFunction fd.fd_name.id_name); + (* Bind type parameters *) + List.iter (fun tp -> + let _ = Symbol.define ctx.symbols tp.tp_name.id_name + Symbol.SKTypeVar tp.tp_name.id_span Private in + () + ) fd.fd_ty_params; + (* Bind parameters *) + List.iter (fun (id, _, _) -> + let _ = Symbol.define ctx.symbols id.id_name + Symbol.SKVariable id.id_span Private in + () + ) fd.fd_params; + let result = match fd.fd_body with + | Some body -> resolve_expr ctx body + | None -> Ok () + in + Symbol.exit_scope ctx.symbols; + result + + | DType td -> + let _ = Symbol.define ctx.symbols td.td_name.id_name + Symbol.SKType td.td_name.id_span td.td_vis in + Ok () + + | DEffect ed -> + let _ = Symbol.define ctx.symbols ed.ed_name.id_name + Symbol.SKEffect ed.ed_name.id_span ed.ed_vis in + (* Define each operation *) + List.iter (fun op -> + let _ = Symbol.define ctx.symbols op.eo_name.id_name + Symbol.SKEffectOp op.eo_name.id_span ed.ed_vis in + () + ) ed.ed_ops; + Ok () + + | DTrait td -> + let _ = Symbol.define ctx.symbols td.trd_name.id_name + Symbol.SKTrait td.trd_name.id_span td.trd_vis in + Ok () + + | DImpl _ -> + (* TODO: Resolve impl blocks *) + Ok () + + | DModule (name, decls, _) -> + let _ = Symbol.define ctx.symbols name.id_name + Symbol.SKModule name.id_span Private in + Symbol.enter_scope ctx.symbols (Symbol.ScopeModule name.id_name); + let result = List.fold_left (fun acc d -> + match acc with + | Error e -> Error e + | Ok () -> resolve_decl ctx d + ) (Ok ()) decls in + Symbol.exit_scope ctx.symbols; + result + + | DImport _ -> + (* TODO: Handle imports *) + Ok () + +(** Resolve an entire program *) +let resolve_program (program : program) : (context, resolve_error * Span.t) Result.t = + let ctx = create_context () in + match List.fold_left (fun acc decl -> + match acc with + | Error e -> Error e + | Ok () -> resolve_decl ctx decl + ) (Ok ()) program.prog_decls with + | Ok () -> Ok ctx + | Error e -> Error e + +(* Helper for Result bind *) +let ( let* ) = Result.bind + +(* TODO: Phase 1 implementation + - [ ] Module qualified lookups + - [ ] Import resolution (use, use as, use *) + - [ ] Visibility checking + - [ ] Forward references in mutual recursion + - [ ] Type alias expansion during resolution +*) diff --git a/lib/symbol.ml b/lib/symbol.ml new file mode 100644 index 0000000..12d97b5 --- /dev/null +++ b/lib/symbol.ml @@ -0,0 +1,157 @@ +(* SPDX-License-Identifier: Apache-2.0 OR MIT *) +(* Copyright 2024 AffineScript Contributors *) + +(** Symbol table for name resolution. + + This module provides the symbol table infrastructure for tracking + bindings during name resolution and type checking. +*) + +open Ast + +(** Unique identifier for symbols *) +type symbol_id = int +[@@deriving show, eq, ord] + +(** Symbol kinds *) +type symbol_kind = + | SKVariable (** Local or global variable *) + | SKFunction (** Function definition *) + | SKType (** Type definition *) + | SKTypeVar (** Type variable *) + | SKEffect (** Effect definition *) + | SKEffectOp (** Effect operation *) + | SKTrait (** Trait definition *) + | SKModule (** Module *) + | SKConstructor (** Data constructor *) +[@@deriving show, eq] + +(** A symbol entry in the symbol table *) +type symbol = { + sym_id : symbol_id; + sym_name : string; + sym_kind : symbol_kind; + sym_span : Span.t; + sym_visibility : visibility; + sym_type : type_expr option; (** Filled during type checking *) + sym_quantity : quantity option; +} +[@@deriving show] + +(** Symbol table scope *) +type scope = { + scope_parent : scope option; + scope_symbols : (string, symbol) Hashtbl.t; + scope_kind : scope_kind; +} + +and scope_kind = + | ScopeGlobal + | ScopeModule of string + | ScopeFunction of string + | ScopeBlock + | ScopeMatch + | ScopeHandler +[@@deriving show] + +(** The symbol table *) +type t = { + mutable current_scope : scope; + mutable next_id : symbol_id; + all_symbols : (symbol_id, symbol) Hashtbl.t; +} + +(** Create a new symbol table *) +let create () : t = + let global_scope = { + scope_parent = None; + scope_symbols = Hashtbl.create 64; + scope_kind = ScopeGlobal; + } in + { + current_scope = global_scope; + next_id = 0; + all_symbols = Hashtbl.create 256; + } + +(** Generate a fresh symbol ID *) +let fresh_id (table : t) : symbol_id = + let id = table.next_id in + table.next_id <- id + 1; + id + +(** Enter a new scope *) +let enter_scope (table : t) (kind : scope_kind) : unit = + let new_scope = { + scope_parent = Some table.current_scope; + scope_symbols = Hashtbl.create 16; + scope_kind = kind; + } in + table.current_scope <- new_scope + +(** Exit the current scope *) +let exit_scope (table : t) : unit = + match table.current_scope.scope_parent with + | Some parent -> table.current_scope <- parent + | None -> failwith "Cannot exit global scope" + +(** Define a new symbol in the current scope *) +let define (table : t) (name : string) (kind : symbol_kind) + (span : Span.t) (vis : visibility) : symbol = + let sym = { + sym_id = fresh_id table; + sym_name = name; + sym_kind = kind; + sym_span = span; + sym_visibility = vis; + sym_type = None; + sym_quantity = None; + } in + Hashtbl.replace table.current_scope.scope_symbols name sym; + Hashtbl.replace table.all_symbols sym.sym_id sym; + sym + +(** Look up a symbol in the current scope and parents *) +let rec lookup_in_scope (scope : scope) (name : string) : symbol option = + match Hashtbl.find_opt scope.scope_symbols name with + | Some sym -> Some sym + | None -> + match scope.scope_parent with + | Some parent -> lookup_in_scope parent name + | None -> None + +(** Look up a symbol by name *) +let lookup (table : t) (name : string) : symbol option = + lookup_in_scope table.current_scope name + +(** Look up a symbol by ID *) +let lookup_by_id (table : t) (id : symbol_id) : symbol option = + Hashtbl.find_opt table.all_symbols id + +(** Check if a name is defined in the current scope (not parents) *) +let is_defined_locally (table : t) (name : string) : bool = + Hashtbl.mem table.current_scope.scope_symbols name + +(** Update a symbol's type *) +let set_type (table : t) (id : symbol_id) (ty : type_expr) : unit = + match Hashtbl.find_opt table.all_symbols id with + | Some sym -> + let updated = { sym with sym_type = Some ty } in + Hashtbl.replace table.all_symbols id updated + | None -> () + +(** Update a symbol's quantity *) +let set_quantity (table : t) (id : symbol_id) (q : quantity) : unit = + match Hashtbl.find_opt table.all_symbols id with + | Some sym -> + let updated = { sym with sym_quantity = Some q } in + Hashtbl.replace table.all_symbols id updated + | None -> () + +(* TODO: Phase 1 implementation + - [ ] Module qualified lookups (Foo.Bar.x) + - [ ] Import handling + - [ ] Visibility checking across modules + - [ ] Type parameter scopes + - [ ] Effect operation resolution +*) diff --git a/lib/typecheck.ml b/lib/typecheck.ml new file mode 100644 index 0000000..8bf2579 --- /dev/null +++ b/lib/typecheck.ml @@ -0,0 +1,574 @@ +(* SPDX-License-Identifier: Apache-2.0 OR MIT *) +(* Copyright 2024 AffineScript Contributors *) + +(** Bidirectional type checker. + + This module implements bidirectional type checking for AffineScript. + It uses synthesis (inference) and checking modes, with the unification + engine handling type variable instantiation. +*) + +open Ast +open Types + +(** Type checking errors *) +type type_error = + | UnificationFailed of Unify.unify_error * Span.t + | ExpectedFunction of ty * Span.t + | ExpectedRecord of ty * Span.t + | ExpectedTuple of ty * Span.t + | UndefinedField of string * Span.t + | ArityMismatch of int * int * Span.t + | CannotInfer of Span.t + | TypeAnnotationRequired of Span.t + | InvalidPattern of Span.t + | QuantityError of string * Span.t + | EffectError of string * Span.t + | BorrowError of string * Span.t +[@@deriving show] + +type 'a result = ('a, type_error) Result.t + +(** Type checking context *) +type context = { + (** Symbol table with resolved names *) + symbols : Symbol.t; + + (** Current let-generalization level *) + level : int; + + (** Variable types *) + var_types : (Symbol.symbol_id, scheme) Hashtbl.t; + + (** Current effect context *) + current_effect : effect; +} + +(** Create a new type checking context *) +let create_context (symbols : Symbol.t) : context = + { + symbols; + level = 0; + var_types = Hashtbl.create 64; + current_effect = EPure; + } + +(** Enter a new let-binding level *) +let enter_level (ctx : context) : context = + { ctx with level = ctx.level + 1 } + +(** Generalize a type at the current level *) +let generalize (ctx : context) (ty : ty) : scheme = + (* Collect all unbound type variables at level > ctx.level *) + let rec collect_tyvars (ty : ty) (acc : (tyvar * kind) list) : (tyvar * kind) list = + match repr ty with + | TVar r -> + begin match !r with + | Unbound (v, lvl) when lvl > ctx.level -> + if List.mem_assoc v acc then acc + else (v, KType) :: acc (* TODO: Track actual kinds *) + | _ -> acc + end + | TApp (t, args) -> + List.fold_left (fun acc t -> collect_tyvars t acc) (collect_tyvars t acc) args + | TArrow (a, b, _) -> + collect_tyvars b (collect_tyvars a acc) + | TTuple ts -> + List.fold_left (fun acc t -> collect_tyvars t acc) acc ts + | TRecord row | TVariant row -> + collect_row_tyvars row acc + | TForall (_, _, body) | TExists (_, _, body) -> + collect_tyvars body acc + | TRef t | TMut t | TOwn t -> + collect_tyvars t acc + | TRefined (t, _) -> + collect_tyvars t acc + | _ -> acc + and collect_row_tyvars (row : row) (acc : (tyvar * kind) list) : (tyvar * kind) list = + match repr_row row with + | REmpty -> acc + | RExtend (_, ty, rest) -> + collect_row_tyvars rest (collect_tyvars ty acc) + | RVar _ -> acc + in + let tyvars = collect_tyvars ty [] in + { sc_tyvars = tyvars; sc_effvars = []; sc_rowvars = []; sc_body = ty } + +(** Instantiate a type scheme *) +let instantiate (ctx : context) (scheme : scheme) : ty = + let subst = List.map (fun (v, _k) -> + (v, fresh_tyvar ctx.level) + ) scheme.sc_tyvars in + let rec apply_subst (ty : ty) : ty = + match repr ty with + | TVar r -> + begin match !r with + | Unbound (v, _) -> + begin match List.assoc_opt v subst with + | Some ty' -> ty' + | None -> ty + end + | Link _ -> failwith "instantiate: unexpected Link" + end + | TApp (t, args) -> + TApp (apply_subst t, List.map apply_subst args) + | TArrow (a, b, eff) -> + TArrow (apply_subst a, apply_subst b, eff) + | TTuple ts -> + TTuple (List.map apply_subst ts) + | TRecord row -> + TRecord (apply_subst_row row) + | TVariant row -> + TVariant (apply_subst_row row) + | TForall (v, k, body) -> + TForall (v, k, apply_subst body) + | TRef t -> TRef (apply_subst t) + | TMut t -> TMut (apply_subst t) + | TOwn t -> TOwn (apply_subst t) + | TRefined (t, p) -> TRefined (apply_subst t, p) + | t -> t + and apply_subst_row (row : row) : row = + match repr_row row with + | REmpty -> REmpty + | RExtend (l, ty, rest) -> + RExtend (l, apply_subst ty, apply_subst_row rest) + | RVar _ as rv -> rv + in + apply_subst scheme.sc_body + +(** Look up a variable's type *) +let lookup_var (ctx : context) (id : ident) : ty result = + match Symbol.lookup ctx.symbols id.id_name with + | Some sym -> + begin match Hashtbl.find_opt ctx.var_types sym.sym_id with + | Some scheme -> Ok (instantiate ctx scheme) + | None -> + (* Variable exists but not yet typed - this shouldn't happen after resolve *) + Error (CannotInfer id.id_span) + end + | None -> + Error (CannotInfer id.id_span) + +(** Bind a variable with a type *) +let bind_var (ctx : context) (id : ident) (ty : ty) : unit = + match Symbol.lookup ctx.symbols id.id_name with + | Some sym -> + let scheme = { sc_tyvars = []; sc_effvars = []; sc_rowvars = []; sc_body = ty } in + Hashtbl.replace ctx.var_types sym.sym_id scheme + | None -> () + +(** Bind a variable with a scheme (polymorphic) *) +let bind_var_scheme (ctx : context) (id : ident) (scheme : scheme) : unit = + match Symbol.lookup ctx.symbols id.id_name with + | Some sym -> + Hashtbl.replace ctx.var_types sym.sym_id scheme + | None -> () + +(** Convert AST type to internal type *) +let rec ast_to_ty (ctx : context) (ty : type_expr) : ty = + match ty with + | TyVar id -> fresh_tyvar ctx.level (* TODO: Look up type variable *) + | TyCon id -> + begin match id.id_name with + | "Unit" -> ty_unit + | "Bool" -> ty_bool + | "Int" -> ty_int + | "Float" -> ty_float + | "Char" -> ty_char + | "String" -> ty_string + | "Never" -> ty_never + | name -> TCon name + end + | TyApp (id, args) -> + TApp (TCon id.id_name, List.map (ast_to_ty_arg ctx) args) + | TyArrow (a, b, eff) -> + let eff' = match eff with + | Some e -> ast_to_eff ctx e + | None -> EPure + in + TArrow (ast_to_ty ctx a, ast_to_ty ctx b, eff') + | TyTuple tys -> + TTuple (List.map (ast_to_ty ctx) tys) + | TyRecord (fields, rest) -> + let row = List.fold_right (fun field acc -> + RExtend (field.rf_name.id_name, ast_to_ty ctx field.rf_ty, acc) + ) fields (match rest with + | Some _ -> fresh_rowvar ctx.level + | None -> REmpty + ) in + TRecord row + | TyOwn t -> TOwn (ast_to_ty ctx t) + | TyRef t -> TRef (ast_to_ty ctx t) + | TyMut t -> TMut (ast_to_ty ctx t) + | TyRefined (t, _pred) -> + (* TODO: Convert predicate *) + TRefined (ast_to_ty ctx t, PTrue) + | _ -> fresh_tyvar ctx.level (* TODO: Handle other cases *) + +and ast_to_ty_arg (ctx : context) (arg : type_arg) : ty = + match arg with + | TaType ty -> ast_to_ty ctx ty + | TaNat _ -> TNat (NLit 0) (* TODO: Convert nat expr *) + +and ast_to_eff (ctx : context) (eff : effect_expr) : effect = + match eff with + | EffNamed id -> ESingleton id.id_name + | EffVar id -> fresh_effvar ctx.level + | EffUnion effs -> EUnion (List.map (ast_to_eff ctx) effs) + | EffApp (id, _) -> ESingleton id.id_name + +(** Synthesize (infer) the type of an expression *) +let rec synth (ctx : context) (expr : expr) : (ty * effect) result = + match expr with + | EVar id -> + let* ty = lookup_var ctx id in + Ok (ty, EPure) + + | ELit lit -> + let ty = synth_literal lit in + Ok (ty, EPure) + + | EApp (func, arg, span) -> + let* (func_ty, func_eff) = synth ctx func in + begin match repr func_ty with + | TArrow (param_ty, ret_ty, call_eff) -> + let* arg_eff = check ctx arg param_ty in + Ok (ret_ty, union_eff [func_eff; arg_eff; call_eff]) + | TVar _ as tv -> + (* Infer function type *) + let param_ty = fresh_tyvar ctx.level in + let ret_ty = fresh_tyvar ctx.level in + let call_eff = fresh_effvar ctx.level in + begin match Unify.unify tv (TArrow (param_ty, ret_ty, call_eff)) with + | Ok () -> + let* arg_eff = check ctx arg param_ty in + Ok (ret_ty, union_eff [func_eff; arg_eff; call_eff]) + | Error e -> + Error (UnificationFailed (e, span)) + end + | _ -> + Error (ExpectedFunction (func_ty, span)) + end + + | ELam lam -> + (* For lambdas, we need annotations or we infer fresh variables *) + let param_tys = List.map (fun (id, ty_opt, _q) -> + match ty_opt with + | Some ty -> (id, ast_to_ty ctx ty) + | None -> (id, fresh_tyvar ctx.level) + ) lam.lam_params in + (* Bind parameters *) + List.iter (fun (id, ty) -> bind_var ctx id ty) param_tys; + (* Infer body *) + let* (body_ty, body_eff) = synth ctx lam.lam_body in + (* Build arrow type *) + let ty = List.fold_right (fun (_, param_ty) acc -> + TArrow (param_ty, acc, body_eff) + ) param_tys body_ty in + Ok (ty, EPure) + + | ELet lb -> + (* Infer RHS at higher level for generalization *) + let ctx' = enter_level ctx in + let* (rhs_ty, rhs_eff) = synth ctx' lb.lb_rhs in + (* Generalize *) + let scheme = generalize ctx rhs_ty in + (* Bind pattern *) + let* () = bind_pattern ctx lb.lb_pat scheme in + (* Infer body *) + let* (body_ty, body_eff) = synth ctx lb.lb_body in + Ok (body_ty, union_eff [rhs_eff; body_eff]) + + | EIf (cond, then_, else_, span) -> + let* cond_eff = check ctx cond ty_bool in + let* (then_ty, then_eff) = synth ctx then_ in + let* else_eff = check ctx else_ then_ty in + Ok (then_ty, union_eff [cond_eff; then_eff; else_eff]) + + | ETuple (exprs, _) -> + let* results = synth_list ctx exprs in + let tys = List.map fst results in + let effs = List.map snd results in + Ok (TTuple tys, union_eff effs) + + | ERecord (fields, _) -> + let* field_results = synth_fields ctx fields in + let row = List.fold_right (fun (name, ty, _eff) acc -> + RExtend (name, ty, acc) + ) field_results REmpty in + let effs = List.map (fun (_, _, eff) -> eff) field_results in + Ok (TRecord row, union_eff effs) + + | ERecordAccess (expr, field, span) -> + let* (expr_ty, expr_eff) = synth ctx expr in + begin match repr expr_ty with + | TRecord row -> + begin match find_field field.id_name row with + | Some ty -> Ok (ty, expr_eff) + | None -> Error (UndefinedField (field.id_name, span)) + end + | TVar _ as tv -> + let field_ty = fresh_tyvar ctx.level in + let rest = fresh_rowvar ctx.level in + let row = RExtend (field.id_name, field_ty, rest) in + begin match Unify.unify tv (TRecord row) with + | Ok () -> Ok (field_ty, expr_eff) + | Error e -> Error (UnificationFailed (e, span)) + end + | _ -> + Error (ExpectedRecord (expr_ty, span)) + end + + | EBlock (exprs, _) -> + synth_block ctx exprs + + | EBinOp (left, op, right, span) -> + synth_binop ctx left op right span + + | EPerform (op, arg, span) -> + (* TODO: Look up effect operation type *) + let* (_arg_ty, arg_eff) = synth ctx arg in + let eff = ESingleton op.id_name in + let ret_ty = fresh_tyvar ctx.level in + Ok (ret_ty, union_eff [arg_eff; eff]) + + | EHandle (body, handler, span) -> + let* (body_ty, body_eff) = synth ctx body in + (* TODO: Check handler and compute resulting effect *) + let result_ty = match handler.h_return with + | Some _ -> fresh_tyvar ctx.level + | None -> body_ty + in + Ok (result_ty, EPure) (* TODO: Proper effect computation *) + + | _ -> + Error (CannotInfer (Span.dummy)) + +(** Check an expression against an expected type *) +and check (ctx : context) (expr : expr) (expected : ty) : effect result = + match (expr, repr expected) with + (* Lambda checking *) + | (ELam lam, TArrow (param_ty, ret_ty, arr_eff)) -> + (* Bind parameters with expected types *) + begin match lam.lam_params with + | [(id, _, _)] -> + bind_var ctx id param_ty; + let* body_eff = check ctx lam.lam_body ret_ty in + begin match Unify.unify_eff body_eff arr_eff with + | Ok () -> Ok EPure + | Error e -> Error (UnificationFailed (e, Span.dummy)) + end + | _ -> + (* TODO: Multi-param lambdas *) + check_subsumption ctx expr expected + end + + (* If checking *) + | (EIf (cond, then_, else_, _), _) -> + let* cond_eff = check ctx cond ty_bool in + let* then_eff = check ctx then_ expected in + let* else_eff = check ctx else_ expected in + Ok (union_eff [cond_eff; then_eff; else_eff]) + + (* Tuple checking *) + | (ETuple (exprs, _), TTuple tys) when List.length exprs = List.length tys -> + let* effs = check_list ctx exprs tys in + Ok (union_eff effs) + + (* Subsumption: synth and unify *) + | _ -> + check_subsumption ctx expr expected + +and check_subsumption (ctx : context) (expr : expr) (expected : ty) : effect result = + let* (actual, eff) = synth ctx expr in + match Unify.unify actual expected with + | Ok () -> Ok eff + | Error e -> Error (UnificationFailed (e, Span.dummy)) + +and synth_list (ctx : context) (exprs : expr list) : ((ty * effect) list) result = + List.fold_right (fun expr acc -> + match acc with + | Error e -> Error e + | Ok results -> + match synth ctx expr with + | Error e -> Error e + | Ok result -> Ok (result :: results) + ) exprs (Ok []) + +and check_list (ctx : context) (exprs : expr list) (tys : ty list) : (effect list) result = + List.fold_right2 (fun expr ty acc -> + match acc with + | Error e -> Error e + | Ok effs -> + match check ctx expr ty with + | Error e -> Error e + | Ok eff -> Ok (eff :: effs) + ) exprs tys (Ok []) + +and synth_fields (ctx : context) (fields : (ident * expr) list) + : ((string * ty * effect) list) result = + List.fold_right (fun (id, expr) acc -> + match acc with + | Error e -> Error e + | Ok results -> + match synth ctx expr with + | Error e -> Error e + | Ok (ty, eff) -> Ok ((id.id_name, ty, eff) :: results) + ) fields (Ok []) + +and synth_block (ctx : context) (exprs : expr list) : (ty * effect) result = + match exprs with + | [] -> Ok (ty_unit, EPure) + | [e] -> synth ctx e + | e :: rest -> + let* (_, eff1) = synth ctx e in + let* (ty, eff2) = synth_block ctx rest in + Ok (ty, union_eff [eff1; eff2]) + +and synth_binop (ctx : context) (left : expr) (op : binop) (right : expr) + (span : Span.t) : (ty * effect) result = + let* (left_ty, left_eff) = synth ctx left in + let* (right_ty, right_eff) = synth ctx right in + let eff = union_eff [left_eff; right_eff] in + match op with + | BAdd | BSub | BMul | BDiv | BMod -> + begin match Unify.unify left_ty ty_int, Unify.unify right_ty ty_int with + | Ok (), Ok () -> Ok (ty_int, eff) + | Error e, _ | _, Error e -> Error (UnificationFailed (e, span)) + end + | BEq | BNe | BLt | BLe | BGt | BGe -> + begin match Unify.unify left_ty right_ty with + | Ok () -> Ok (ty_bool, eff) + | Error e -> Error (UnificationFailed (e, span)) + end + | BAnd | BOr -> + begin match Unify.unify left_ty ty_bool, Unify.unify right_ty ty_bool with + | Ok (), Ok () -> Ok (ty_bool, eff) + | Error e, _ | _, Error e -> Error (UnificationFailed (e, span)) + end + | _ -> + (* TODO: Other operators *) + Ok (fresh_tyvar ctx.level, eff) + +and bind_pattern (ctx : context) (pat : pattern) (scheme : scheme) : unit result = + match pat with + | PVar id -> + bind_var_scheme ctx id scheme; + Ok () + | PWild _ -> Ok () + | PTuple (pats, _) -> + begin match scheme.sc_body with + | TTuple tys when List.length pats = List.length tys -> + List.fold_left2 (fun acc pat ty -> + match acc with + | Error e -> Error e + | Ok () -> + let sc = { scheme with sc_body = ty } in + bind_pattern ctx pat sc + ) (Ok ()) pats tys + | _ -> Error (InvalidPattern Span.dummy) + end + | _ -> + (* TODO: Other patterns *) + Ok () + +and synth_literal (lit : literal) : ty = + match lit with + | LUnit _ -> ty_unit + | LBool _ -> ty_bool + | LInt _ -> ty_int + | LFloat _ -> ty_float + | LChar _ -> ty_char + | LString _ -> ty_string + +and find_field (name : string) (row : row) : ty option = + match repr_row row with + | REmpty -> None + | RExtend (l, ty, rest) -> + if l = name then Some ty + else find_field name rest + | RVar _ -> None + +and union_eff (effs : effect list) : effect = + let effs = List.filter (fun e -> e <> EPure) effs in + match effs with + | [] -> EPure + | [e] -> e + | es -> EUnion es + +(* Result bind *) +let ( let* ) = Result.bind + +(** Type check a declaration *) +let check_decl (ctx : context) (decl : decl) : unit result = + match decl with + | DFun fd -> + (* Create function type from signature *) + let param_tys = List.map (fun (id, ty_opt, _q) -> + match ty_opt with + | Some ty -> (id, ast_to_ty ctx ty) + | None -> (id, fresh_tyvar ctx.level) + ) fd.fd_params in + let ret_ty = match fd.fd_ret_ty with + | Some ty -> ast_to_ty ctx ty + | None -> fresh_tyvar ctx.level + in + (* Build function type *) + let func_ty = List.fold_right (fun (_, param_ty) acc -> + TArrow (param_ty, acc, EPure) + ) param_tys ret_ty in + (* Bind function name *) + bind_var ctx fd.fd_name func_ty; + (* Bind parameters *) + List.iter (fun (id, ty) -> bind_var ctx id ty) param_tys; + (* Check body if present *) + begin match fd.fd_body with + | Some body -> + let* _ = check ctx body ret_ty in + Ok () + | None -> Ok () + end + + | DType _ -> + (* TODO: Check type definitions *) + Ok () + + | DEffect _ -> + (* TODO: Register effect *) + Ok () + + | DTrait _ -> + (* TODO: Check trait definitions *) + Ok () + + | DImpl _ -> + (* TODO: Check implementations *) + Ok () + + | DModule (_, decls, _) -> + List.fold_left (fun acc d -> + match acc with + | Error e -> Error e + | Ok () -> check_decl ctx d + ) (Ok ()) decls + + | DImport _ -> + Ok () + +(** Type check a program *) +let check_program (symbols : Symbol.t) (program : program) : unit result = + let ctx = create_context symbols in + List.fold_left (fun acc decl -> + match acc with + | Error e -> Error e + | Ok () -> check_decl ctx decl + ) (Ok ()) program.prog_decls + +(* TODO: Phase 1 implementation + - [ ] Better error messages with suggestions + - [ ] Type annotations on let bindings + - [ ] Effect inference integration + - [ ] Quantity checking integration + - [ ] Trait resolution + - [ ] Module type checking +*) diff --git a/lib/types.ml b/lib/types.ml new file mode 100644 index 0000000..fdfac52 --- /dev/null +++ b/lib/types.ml @@ -0,0 +1,212 @@ +(* SPDX-License-Identifier: Apache-2.0 OR MIT *) +(* Copyright 2024 AffineScript Contributors *) + +(** Internal type representation for type checking. + + This module defines the internal type representation used during + type checking, separate from the AST types. It includes type variables + with levels for let-generalization. +*) + +(** Type variable identifier *) +type tyvar = int +[@@deriving show, eq, ord] + +(** Row variable identifier *) +type rowvar = int +[@@deriving show, eq, ord] + +(** Effect variable identifier *) +type effvar = int +[@@deriving show, eq, ord] + +(** Quantity (for QTT) *) +type quantity = + | QZero (** 0 - erased at runtime *) + | QOne (** 1 - used exactly once *) + | QOmega (** ω - used arbitrarily *) + | QVar of int (** Quantity variable *) +[@@deriving show, eq] + +(** Kind *) +type kind = + | KType (** Type kind *) + | KNat (** Natural number kind *) + | KRow (** Row kind *) + | KEffect (** Effect kind *) + | KArrow of kind * kind (** Higher-order kind *) +[@@deriving show, eq] + +(** Type representation *) +type ty = + | TVar of tyvar_state ref (** Type variable (mutable for unification) *) + | TCon of string (** Type constructor (Int, Bool, etc.) *) + | TApp of ty * ty list (** Type application *) + | TArrow of ty * ty * effect (** Function type with effect *) + | TDepArrow of string * ty * ty * effect (** Dependent function type *) + | TTuple of ty list (** Tuple type *) + | TRecord of row (** Record type *) + | TVariant of row (** Variant type *) + | TForall of tyvar * kind * ty (** Universal quantification *) + | TExists of tyvar * kind * ty (** Existential quantification *) + | TRef of ty (** Immutable reference *) + | TMut of ty (** Mutable reference *) + | TOwn of ty (** Owned type *) + | TRefined of ty * predicate (** Refinement type *) + | TNat of nat_expr (** Type-level natural *) +[@@deriving show] + +(** Type variable state (for unification) *) +and tyvar_state = + | Unbound of tyvar * int (** Unbound with level *) + | Link of ty (** Linked to another type *) +[@@deriving show] + +(** Row type *) +and row = + | REmpty (** Empty row *) + | RExtend of string * ty * row (** Row extension *) + | RVar of rowvar_state ref (** Row variable *) +[@@deriving show] + +and rowvar_state = + | RUnbound of rowvar * int + | RLink of row +[@@deriving show] + +(** Effect type *) +and effect = + | EPure (** No effects *) + | EVar of effvar_state ref (** Effect variable *) + | ESingleton of string (** Single effect *) + | EUnion of effect list (** Union of effects *) +[@@deriving show] + +and effvar_state = + | EUnbound of effvar * int + | ELink of effect +[@@deriving show] + +(** Type-level natural expression *) +and nat_expr = + | NLit of int + | NVar of string + | NAdd of nat_expr * nat_expr + | NSub of nat_expr * nat_expr + | NMul of nat_expr * nat_expr + | NLen of string +[@@deriving show] + +(** Predicate for refinement types *) +and predicate = + | PTrue + | PFalse + | PEq of nat_expr * nat_expr + | PLt of nat_expr * nat_expr + | PLe of nat_expr * nat_expr + | PGt of nat_expr * nat_expr + | PGe of nat_expr * nat_expr + | PAnd of predicate * predicate + | POr of predicate * predicate + | PNot of predicate + | PImpl of predicate * predicate +[@@deriving show] + +(** Type scheme (polymorphic type) *) +type scheme = { + sc_tyvars : (tyvar * kind) list; + sc_effvars : effvar list; + sc_rowvars : rowvar list; + sc_body : ty; +} +[@@deriving show] + +(** Fresh variable generation *) +let next_tyvar = ref 0 +let next_rowvar = ref 0 +let next_effvar = ref 0 + +let fresh_tyvar (level : int) : ty = + let id = !next_tyvar in + next_tyvar := id + 1; + TVar (ref (Unbound (id, level))) + +let fresh_rowvar (level : int) : row = + let id = !next_rowvar in + next_rowvar := id + 1; + RVar (ref (RUnbound (id, level))) + +let fresh_effvar (level : int) : effect = + let id = !next_effvar in + next_effvar := id + 1; + EVar (ref (EUnbound (id, level))) + +(** Reset all counters (for testing) *) +let reset () = + next_tyvar := 0; + next_rowvar := 0; + next_effvar := 0 + +(** Primitive types *) +let ty_unit = TCon "Unit" +let ty_bool = TCon "Bool" +let ty_int = TCon "Int" +let ty_float = TCon "Float" +let ty_char = TCon "Char" +let ty_string = TCon "String" +let ty_never = TCon "Never" + +(** Construct an arrow type *) +let arrow ?(eff = EPure) (a : ty) (b : ty) : ty = + TArrow (a, b, eff) + +(** Construct a tuple type *) +let tuple (tys : ty list) : ty = + TTuple tys + +(** Follow links in a type variable *) +let rec repr (ty : ty) : ty = + match ty with + | TVar r -> + begin match !r with + | Link ty' -> + let ty'' = repr ty' in + r := Link ty''; (* Path compression *) + ty'' + | Unbound _ -> ty + end + | _ -> ty + +(** Follow links in a row *) +let rec repr_row (row : row) : row = + match row with + | RVar r -> + begin match !r with + | RLink row' -> + let row'' = repr_row row' in + r := RLink row''; + row'' + | RUnbound _ -> row + end + | _ -> row + +(** Follow links in an effect *) +let rec repr_eff (eff : effect) : effect = + match eff with + | EVar r -> + begin match !r with + | ELink eff' -> + let eff'' = repr_eff eff' in + r := ELink eff''; + eff'' + | EUnbound _ -> eff + end + | _ -> eff + +(* TODO: Phase 1 implementation + - [ ] Pretty printing for types + - [ ] Type substitution + - [ ] Free variable collection + - [ ] Occurs check helpers + - [ ] Type normalization for dependent types +*) diff --git a/lib/unify.ml b/lib/unify.ml new file mode 100644 index 0000000..b975e67 --- /dev/null +++ b/lib/unify.ml @@ -0,0 +1,317 @@ +(* SPDX-License-Identifier: Apache-2.0 OR MIT *) +(* Copyright 2024 AffineScript Contributors *) + +(** Type unification. + + This module implements unification for types, rows, and effects. + It uses mutable references for efficient union-find style unification. +*) + +open Types + +(** Unification errors *) +type unify_error = + | TypeMismatch of ty * ty + | OccursCheck of tyvar * ty + | RowMismatch of row * row + | RowOccursCheck of rowvar * row + | EffectMismatch of effect * effect + | EffectOccursCheck of effvar * effect + | KindMismatch of kind * kind + | LabelNotFound of string * row +[@@deriving show] + +type 'a result = ('a, unify_error) Result.t + +(** Check if a type variable occurs in a type (occurs check) *) +let rec occurs_in_ty (var : tyvar) (ty : ty) : bool = + match repr ty with + | TVar r -> + begin match !r with + | Unbound (v, _) -> v = var + | Link _ -> failwith "occurs_in_ty: unexpected Link after repr" + end + | TCon _ -> false + | TApp (t, args) -> + occurs_in_ty var t || List.exists (occurs_in_ty var) args + | TArrow (a, b, eff) -> + occurs_in_ty var a || occurs_in_ty var b || occurs_in_eff var eff + | TDepArrow (_, a, b, eff) -> + occurs_in_ty var a || occurs_in_ty var b || occurs_in_eff var eff + | TTuple tys -> + List.exists (occurs_in_ty var) tys + | TRecord row | TVariant row -> + occurs_in_row var row + | TForall (_, _, body) | TExists (_, _, body) -> + occurs_in_ty var body + | TRef t | TMut t | TOwn t -> + occurs_in_ty var t + | TRefined (t, _) -> + occurs_in_ty var t + | TNat _ -> false + +and occurs_in_row (var : tyvar) (row : row) : bool = + match repr_row row with + | REmpty -> false + | RExtend (_, ty, rest) -> + occurs_in_ty var ty || occurs_in_row var rest + | RVar _ -> false + +and occurs_in_eff (var : tyvar) (eff : effect) : bool = + match repr_eff eff with + | EPure -> false + | EVar _ -> false + | ESingleton _ -> false + | EUnion effs -> List.exists (occurs_in_eff var) effs + +(** Check if a row variable occurs in a row *) +let rec rowvar_occurs_in_row (var : rowvar) (row : row) : bool = + match repr_row row with + | REmpty -> false + | RExtend (_, _, rest) -> rowvar_occurs_in_row var rest + | RVar r -> + begin match !r with + | RUnbound (v, _) -> v = var + | RLink _ -> failwith "rowvar_occurs_in_row: unexpected Link" + end + +(** Check if an effect variable occurs in an effect *) +let rec effvar_occurs_in_eff (var : effvar) (eff : effect) : bool = + match repr_eff eff with + | EPure -> false + | ESingleton _ -> false + | EVar r -> + begin match !r with + | EUnbound (v, _) -> v = var + | ELink _ -> failwith "effvar_occurs_in_eff: unexpected Link" + end + | EUnion effs -> List.exists (effvar_occurs_in_eff var) effs + +(** Unify two types *) +let rec unify (t1 : ty) (t2 : ty) : unit result = + let t1 = repr t1 in + let t2 = repr t2 in + match (t1, t2) with + (* Same variable *) + | (TVar r1, TVar r2) when r1 == r2 -> + Ok () + + (* Variable on left *) + | (TVar r, t) -> + begin match !r with + | Unbound (var, _level) -> + if occurs_in_ty var t then + Error (OccursCheck (var, t)) + else begin + r := Link t; + Ok () + end + | Link _ -> failwith "unify: unexpected Link after repr" + end + + (* Variable on right *) + | (t, TVar r) -> + begin match !r with + | Unbound (var, _level) -> + if occurs_in_ty var t then + Error (OccursCheck (var, t)) + else begin + r := Link t; + Ok () + end + | Link _ -> failwith "unify: unexpected Link after repr" + end + + (* Same constructor *) + | (TCon c1, TCon c2) when c1 = c2 -> + Ok () + + (* Type application *) + | (TApp (t1, args1), TApp (t2, args2)) + when List.length args1 = List.length args2 -> + let* () = unify t1 t2 in + unify_list args1 args2 + + (* Arrow types *) + | (TArrow (a1, b1, e1), TArrow (a2, b2, e2)) -> + let* () = unify a1 a2 in + let* () = unify b1 b2 in + unify_eff e1 e2 + + (* Dependent arrow types *) + | (TDepArrow (_, a1, b1, e1), TDepArrow (_, a2, b2, e2)) -> + (* TODO: Handle the binding properly *) + let* () = unify a1 a2 in + let* () = unify b1 b2 in + unify_eff e1 e2 + + (* Tuple types *) + | (TTuple ts1, TTuple ts2) when List.length ts1 = List.length ts2 -> + unify_list ts1 ts2 + + (* Record types *) + | (TRecord r1, TRecord r2) -> + unify_row r1 r2 + + (* Variant types *) + | (TVariant r1, TVariant r2) -> + unify_row r1 r2 + + (* Forall types *) + | (TForall (v1, k1, body1), TForall (v2, k2, body2)) -> + if k1 <> k2 then + Error (KindMismatch (k1, k2)) + else + (* TODO: Alpha-equivalence *) + unify body1 body2 + + (* Reference types *) + | (TRef t1, TRef t2) -> unify t1 t2 + | (TMut t1, TMut t2) -> unify t1 t2 + | (TOwn t1, TOwn t2) -> unify t1 t2 + + (* Refinement types *) + | (TRefined (t1, _p1), TRefined (t2, _p2)) -> + (* TODO: Unify predicates via SMT *) + unify t1 t2 + + (* Type-level naturals *) + | (TNat n1, TNat n2) -> + (* TODO: Normalize and compare *) + if n1 = n2 then Ok () + else Error (TypeMismatch (t1, t2)) + + (* Mismatch *) + | _ -> + Error (TypeMismatch (t1, t2)) + +and unify_list (ts1 : ty list) (ts2 : ty list) : unit result = + match (ts1, ts2) with + | ([], []) -> Ok () + | (t1 :: rest1, t2 :: rest2) -> + let* () = unify t1 t2 in + unify_list rest1 rest2 + | _ -> failwith "unify_list: length mismatch" + +(** Unify two rows *) +and unify_row (r1 : row) (r2 : row) : unit result = + let r1 = repr_row r1 in + let r2 = repr_row r2 in + match (r1, r2) with + (* Both empty *) + | (REmpty, REmpty) -> Ok () + + (* Same variable *) + | (RVar rv1, RVar rv2) when rv1 == rv2 -> Ok () + + (* Variable on left *) + | (RVar r, row) -> + begin match !r with + | RUnbound (var, _level) -> + if rowvar_occurs_in_row var row then + Error (RowOccursCheck (var, row)) + else begin + r := RLink row; + Ok () + end + | RLink _ -> failwith "unify_row: unexpected RLink" + end + + (* Variable on right *) + | (row, RVar r) -> + begin match !r with + | RUnbound (var, _level) -> + if rowvar_occurs_in_row var row then + Error (RowOccursCheck (var, row)) + else begin + r := RLink row; + Ok () + end + | RLink _ -> failwith "unify_row: unexpected RLink" + end + + (* Both extend with same label *) + | (RExtend (l1, t1, rest1), RExtend (l2, t2, rest2)) when l1 = l2 -> + let* () = unify t1 t2 in + unify_row rest1 rest2 + + (* Extend with different labels - row rewriting *) + | (RExtend (l1, t1, rest1), RExtend (l2, t2, rest2)) -> + (* l1 ≠ l2, so we need to find l1 in r2 and l2 in r1 *) + let level = 0 in (* TODO: Get proper level *) + let new_rest = fresh_rowvar level in + let* () = unify_row rest1 (RExtend (l2, t2, new_rest)) in + unify_row rest2 (RExtend (l1, t1, new_rest)) + + (* Empty vs extend - error *) + | (REmpty, RExtend (l, _, _)) -> + Error (LabelNotFound (l, r1)) + | (RExtend (l, _, _), REmpty) -> + Error (LabelNotFound (l, r2)) + +(** Unify two effects *) +and unify_eff (e1 : effect) (e2 : effect) : unit result = + let e1 = repr_eff e1 in + let e2 = repr_eff e2 in + match (e1, e2) with + (* Both pure *) + | (EPure, EPure) -> Ok () + + (* Same variable *) + | (EVar r1, EVar r2) when r1 == r2 -> Ok () + + (* Variable on left *) + | (EVar r, eff) -> + begin match !r with + | EUnbound (var, _level) -> + if effvar_occurs_in_eff var eff then + Error (EffectOccursCheck (var, eff)) + else begin + r := ELink eff; + Ok () + end + | ELink _ -> failwith "unify_eff: unexpected ELink" + end + + (* Variable on right *) + | (eff, EVar r) -> + begin match !r with + | EUnbound (var, _level) -> + if effvar_occurs_in_eff var eff then + Error (EffectOccursCheck (var, eff)) + else begin + r := ELink eff; + Ok () + end + | ELink _ -> failwith "unify_eff: unexpected ELink" + end + + (* Same singleton *) + | (ESingleton e1, ESingleton e2) when e1 = e2 -> + Ok () + + (* Union vs union *) + | (EUnion es1, EUnion es2) -> + (* TODO: Proper set-based unification *) + if List.length es1 = List.length es2 then + List.fold_left2 (fun acc e1 e2 -> + match acc with + | Error e -> Error e + | Ok () -> unify_eff e1 e2 + ) (Ok ()) es1 es2 + else + Error (EffectMismatch (e1, e2)) + + (* Mismatch *) + | _ -> + Error (EffectMismatch (e1, e2)) + +(* Result bind operator *) +let ( let* ) = Result.bind + +(* TODO: Phase 1 implementation + - [ ] Level-based generalization + - [ ] Proper handling of dependent types + - [ ] Effect row set-based unification + - [ ] Better error messages with source locations +*) diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml new file mode 100644 index 0000000..5306e71 --- /dev/null +++ b/runtime/Cargo.toml @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: Apache-2.0 OR MIT +# Copyright 2024 AffineScript Contributors + +[package] +name = "affinescript-runtime" +version = "0.1.0" +edition = "2021" +authors = ["AffineScript Contributors"] +description = "Runtime library for AffineScript, compiled to WebAssembly" +license = "Apache-2.0 OR MIT" +repository = "https://github.com/hyperpolymath/affinescript" +keywords = ["affinescript", "runtime", "wasm"] +categories = ["wasm", "no-std"] + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["std"] +std = [] +# Enable small garbage collector for cyclic ω-quantity data +gc = [] +# Enable WASI support +wasi = ["dep:wasi"] + +[dependencies] +# Small allocator for WASM +wee_alloc = { version = "0.4", optional = true } + +# WASI bindings (optional) +wasi = { version = "0.11", optional = true } + +[dev-dependencies] +# Testing +wasm-bindgen-test = "0.3" + +[profile.release] +# Optimize for size in WASM +opt-level = "s" +lto = true +codegen-units = 1 +panic = "abort" + +[profile.release-speed] +inherits = "release" +opt-level = 3 diff --git a/runtime/src/alloc.rs b/runtime/src/alloc.rs new file mode 100644 index 0000000..e1b4bd0 --- /dev/null +++ b/runtime/src/alloc.rs @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright 2024 AffineScript Contributors + +//! Memory Allocator for AffineScript Runtime +//! +//! This module provides memory allocation optimized for linear/affine values. +//! Since most values are used exactly once, we can use a simple bump allocator +//! with explicit deallocation. +//! +//! # Architecture +//! +//! ```text +//! ┌─────────────────────────────────────────────────────────────┐ +//! │ WASM Linear Memory │ +//! ├─────────────────────────────────────────────────────────────┤ +//! │ Stack │ Heap (bump) │ Free List │ Reserved │ +//! └─────────────────────────────────────────────────────────────┘ +//! ``` +//! +//! # Features +//! +//! - **Bump allocation**: Fast O(1) allocation for short-lived values +//! - **Free list**: Reuse deallocated blocks for longer-lived values +//! - **Size classes**: Segregated free lists for common sizes +//! - **Linear optimization**: Skip reference counting for linear values + +#[cfg(not(feature = "std"))] +use core::{alloc::Layout, ptr::NonNull}; + +#[cfg(feature = "std")] +use std::{alloc::Layout, ptr::NonNull}; + +/// Memory block header +#[repr(C)] +struct BlockHeader { + /// Size of the block (excluding header) + size: usize, + /// Flags (in_use, is_linear, etc.) + flags: u32, +} + +/// Size classes for segregated free lists +const SIZE_CLASSES: [usize; 8] = [16, 32, 64, 128, 256, 512, 1024, 2048]; + +/// Global allocator state +struct AllocatorState { + /// Start of heap + heap_start: usize, + /// Current bump pointer + bump_ptr: usize, + /// End of available heap + heap_end: usize, + /// Free lists by size class + free_lists: [Option>; 8], + /// Total allocated bytes + allocated: usize, + /// Total freed bytes + freed: usize, +} + +static mut ALLOCATOR: AllocatorState = AllocatorState { + heap_start: 0, + bump_ptr: 0, + heap_end: 0, + free_lists: [None; 8], + allocated: 0, + freed: 0, +}; + +/// Initialize the allocator +/// +/// Called once at program startup. +pub fn init() { + // TODO: Phase 6 implementation + // - [ ] Query WASM memory size + // - [ ] Set up heap region + // - [ ] Initialize free lists + // - [ ] Set up guard pages (if available) +} + +/// Allocate memory +/// +/// # Arguments +/// +/// * `size` - Number of bytes to allocate +/// * `align` - Required alignment (must be power of 2) +/// +/// # Returns +/// +/// Pointer to allocated memory, or null on failure +#[no_mangle] +pub extern "C" fn allocate(size: usize, align: usize) -> *mut u8 { + // TODO: Phase 6 implementation + // - [ ] Check free list for matching size class + // - [ ] Fall back to bump allocation + // - [ ] Handle out-of-memory (grow memory or fail) + // - [ ] Track allocation statistics + + core::ptr::null_mut() +} + +/// Deallocate memory +/// +/// # Arguments +/// +/// * `ptr` - Pointer previously returned by `allocate` +/// * `size` - Size of the allocation +/// * `align` - Alignment of the allocation +/// +/// # Safety +/// +/// The pointer must have been allocated by this allocator and not yet freed. +#[no_mangle] +pub unsafe extern "C" fn deallocate(ptr: *mut u8, size: usize, align: usize) { + // TODO: Phase 6 implementation + // - [ ] Validate pointer is in heap range + // - [ ] Add to appropriate free list + // - [ ] Coalesce adjacent free blocks (optional) + // - [ ] Track deallocation statistics +} + +/// Reallocate memory +/// +/// # Arguments +/// +/// * `ptr` - Pointer previously returned by `allocate` +/// * `old_size` - Current size of the allocation +/// * `new_size` - Desired new size +/// * `align` - Alignment requirement +/// +/// # Returns +/// +/// Pointer to reallocated memory (may be different from input) +#[no_mangle] +pub unsafe extern "C" fn reallocate( + ptr: *mut u8, + old_size: usize, + new_size: usize, + align: usize, +) -> *mut u8 { + // TODO: Phase 6 implementation + // - [ ] If shrinking, just update size + // - [ ] If growing and space available, extend in place + // - [ ] Otherwise allocate new block and copy + + core::ptr::null_mut() +} + +/// Get allocation statistics +#[no_mangle] +pub extern "C" fn alloc_stats() -> (usize, usize, usize) { + unsafe { + (ALLOCATOR.allocated, ALLOCATOR.freed, ALLOCATOR.allocated - ALLOCATOR.freed) + } +} + +// TODO: Phase 6 implementation +// - [ ] Implement size class selection +// - [ ] Implement free list management +// - [ ] Add memory growth support +// - [ ] Add allocation tracking for debugging +// - [ ] Optimize for common patterns (small allocations, LIFO) diff --git a/runtime/src/effects.rs b/runtime/src/effects.rs new file mode 100644 index 0000000..54edd99 --- /dev/null +++ b/runtime/src/effects.rs @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright 2024 AffineScript Contributors + +//! Effect Handling Runtime for AffineScript +//! +//! This module implements the runtime support for algebraic effects, +//! using evidence-passing compilation (Koka-style). +//! +//! # Architecture +//! +//! ```text +//! ┌─────────────────────────────────────────────────────────────┐ +//! │ Handler Stack │ +//! ├─────────────────────────────────────────────────────────────┤ +//! │ Handler N │ Handler N-1 │ ... │ Handler 1 │ Base │ +//! └─────────────────────────────────────────────────────────────┘ +//! │ │ │ +//! ▼ ▼ ▼ +//! ┌──────────┐ ┌──────────┐ ┌──────────┐ +//! │ Evidence │ │ Evidence │ │ Evidence │ +//! │ (ops) │ │ (ops) │ │ (ops) │ +//! └──────────┘ └──────────┘ └──────────┘ +//! ``` +//! +//! # Evidence Passing +//! +//! Each effect operation receives an "evidence" parameter that contains: +//! - Pointer to the handler function for that operation +//! - Captured environment from the handler +//! - Continuation management data + +#[cfg(not(feature = "std"))] +use core::ptr::NonNull; + +#[cfg(feature = "std")] +use std::ptr::NonNull; + +/// Evidence for an effect operation +/// +/// This is passed to effectful functions and contains the handler +/// to invoke when an operation is performed. +#[repr(C)] +pub struct Evidence { + /// Pointer to the handler function + pub handler: *const (), + /// Captured environment + pub env: *mut (), + /// Parent evidence (for nested handlers) + pub parent: *mut Evidence, + /// Handler frame pointer + pub frame: *mut HandlerFrame, +} + +impl Evidence { + /// Create new evidence + pub fn new(handler: *const (), env: *mut (), parent: *mut Evidence) -> Self { + Evidence { + handler, + env, + parent, + frame: core::ptr::null_mut(), + } + } +} + +/// Handler frame on the stack +/// +/// Tracks the state needed to resume a continuation. +#[repr(C)] +pub struct HandlerFrame { + /// Saved stack pointer + pub sp: *mut u8, + /// Saved frame pointer + pub fp: *mut u8, + /// Return address + pub ra: *const (), + /// Effect signature being handled + pub effect_id: u32, + /// Whether this is a one-shot or multi-shot handler + pub is_linear: bool, + /// Parent frame + pub parent: *mut HandlerFrame, +} + +/// Continuation representation +/// +/// Captures the state needed to resume execution after an effect operation. +#[repr(C)] +pub struct Continuation { + /// Saved execution state + pub frame: HandlerFrame, + /// Captured stack segment (for multi-shot) + pub stack: Option>, + /// Stack segment size + pub stack_size: usize, + /// Whether this continuation has been used + pub used: bool, +} + +/// Handler for an effect +/// +/// Contains the implementation of each operation. +#[repr(C)] +pub struct Handler { + /// Operation implementations (function pointers) + pub operations: *const *const (), + /// Number of operations + pub num_ops: usize, + /// Return clause + pub return_fn: *const (), + /// Captured environment + pub env: *mut (), +} + +/// Global handler stack +struct HandlerStack { + /// Top of stack + top: *mut HandlerFrame, + /// Stack of installed handlers + handlers: [*mut Handler; 64], + /// Number of installed handlers + count: usize, +} + +static mut HANDLER_STACK: HandlerStack = HandlerStack { + top: core::ptr::null_mut(), + handlers: [core::ptr::null_mut(); 64], + count: 0, +}; + +/// Initialize the effect system +pub fn init() { + // TODO: Phase 6 implementation + // - [ ] Set up initial handler frame + // - [ ] Install default handlers for built-in effects + // - [ ] Initialize continuation pool +} + +/// Install a handler +/// +/// # Arguments +/// +/// * `handler` - The handler to install +/// * `evidence` - Evidence to populate +/// +/// # Returns +/// +/// Opaque handle for uninstalling +#[no_mangle] +pub extern "C" fn install_handler(handler: *mut Handler, evidence: *mut Evidence) -> u32 { + // TODO: Phase 6 implementation + // - [ ] Push handler onto stack + // - [ ] Set up evidence + // - [ ] Return handle + + 0 +} + +/// Uninstall a handler +/// +/// # Arguments +/// +/// * `handle` - Handle returned by `install_handler` +#[no_mangle] +pub extern "C" fn uninstall_handler(handle: u32) { + // TODO: Phase 6 implementation + // - [ ] Pop handler from stack + // - [ ] Restore previous evidence +} + +/// Perform an effect operation +/// +/// # Arguments +/// +/// * `evidence` - Evidence for the effect +/// * `op_index` - Index of the operation to perform +/// * `arg` - Argument to the operation +/// +/// # Returns +/// +/// Result of the operation +#[no_mangle] +pub extern "C" fn perform(evidence: *mut Evidence, op_index: u32, arg: *mut ()) -> *mut () { + // TODO: Phase 6 implementation + // - [ ] Look up handler in evidence + // - [ ] Create continuation + // - [ ] Call handler with continuation + + core::ptr::null_mut() +} + +/// Resume a continuation +/// +/// # Arguments +/// +/// * `k` - The continuation to resume +/// * `value` - Value to pass to the continuation +/// +/// # Returns +/// +/// Result of resuming +#[no_mangle] +pub extern "C" fn resume(k: *mut Continuation, value: *mut ()) -> *mut () { + // TODO: Phase 6 implementation + // - [ ] Check if continuation is linear and already used + // - [ ] Restore execution state + // - [ ] Jump to continuation point + + core::ptr::null_mut() +} + +/// Abort an effect (for one-shot handlers) +/// +/// # Arguments +/// +/// * `evidence` - Evidence for the effect +/// * `value` - Value to return +#[no_mangle] +pub extern "C" fn abort_effect(evidence: *mut Evidence, value: *mut ()) -> ! { + // TODO: Phase 6 implementation + // - [ ] Unwind to handler frame + // - [ ] Call return clause + + loop {} +} + +// TODO: Phase 6 implementation +// - [ ] Implement evidence passing transform in codegen +// - [ ] Implement handler frame management +// - [ ] Implement one-shot continuation optimization +// - [ ] Implement multi-shot continuation copying +// - [ ] Add effect row tracking at runtime (for debugging) +// - [ ] Implement tail-resumptive optimization diff --git a/runtime/src/ffi.rs b/runtime/src/ffi.rs new file mode 100644 index 0000000..85d618c --- /dev/null +++ b/runtime/src/ffi.rs @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright 2024 AffineScript Contributors + +//! Foreign Function Interface for AffineScript Runtime +//! +//! This module provides the FFI layer between AffineScript and host environments. +//! It handles: +//! - Type conversions between AffineScript and host types +//! - String interop +//! - Callback management +//! - Host function imports +//! +//! # Supported Hosts +//! +//! - **JavaScript**: Via wasm-bindgen or direct imports +//! - **WASI**: Standard WASI interfaces +//! - **Native**: For testing and tooling + +#[cfg(not(feature = "std"))] +use core::slice; + +#[cfg(feature = "std")] +use std::slice; + +/// String representation for FFI +/// +/// AffineScript strings are UTF-8, length-prefixed. +#[repr(C)] +pub struct FfiString { + /// Pointer to string data + pub ptr: *const u8, + /// Length in bytes + pub len: usize, +} + +impl FfiString { + /// Create from raw parts + pub const fn new(ptr: *const u8, len: usize) -> Self { + FfiString { ptr, len } + } + + /// Get as byte slice + pub unsafe fn as_bytes(&self) -> &[u8] { + slice::from_raw_parts(self.ptr, self.len) + } + + /// Get as str (unchecked) + pub unsafe fn as_str(&self) -> &str { + core::str::from_utf8_unchecked(self.as_bytes()) + } +} + +/// Array representation for FFI +#[repr(C)] +pub struct FfiArray { + /// Pointer to array data + pub ptr: *const T, + /// Number of elements + pub len: usize, +} + +/// Result type for FFI operations +#[repr(C)] +pub struct FfiResult { + /// Success value (if is_ok is true) + pub value: T, + /// Error message (if is_ok is false) + pub error: FfiString, + /// Whether operation succeeded + pub is_ok: bool, +} + +/// Callback type for host functions +pub type HostCallback = extern "C" fn(*const (), *mut ()) -> *mut (); + +/// Host function registry +struct HostRegistry { + /// Registered callbacks + callbacks: [Option; 256], + /// Number of registered callbacks + count: usize, +} + +static mut HOST_REGISTRY: HostRegistry = HostRegistry { + callbacks: [None; 256], + count: 0, +}; + +/// Initialize FFI layer +pub fn init() { + // TODO: Phase 6 implementation + // - [ ] Set up host function table + // - [ ] Initialize string interop + // - [ ] Register built-in imports +} + +/// Register a host callback +/// +/// # Arguments +/// +/// * `id` - Unique identifier for the callback +/// * `callback` - The callback function +/// +/// # Returns +/// +/// True if registration succeeded +#[no_mangle] +pub extern "C" fn register_host_callback(id: u32, callback: HostCallback) -> bool { + unsafe { + if (id as usize) < HOST_REGISTRY.callbacks.len() { + HOST_REGISTRY.callbacks[id as usize] = Some(callback); + HOST_REGISTRY.count += 1; + true + } else { + false + } + } +} + +/// Call a host function +/// +/// # Arguments +/// +/// * `id` - Callback identifier +/// * `arg` - Argument to pass +/// +/// # Returns +/// +/// Result from host function +#[no_mangle] +pub extern "C" fn call_host(id: u32, arg: *const ()) -> *mut () { + unsafe { + if let Some(callback) = HOST_REGISTRY.callbacks.get(id as usize).and_then(|c| *c) { + callback(arg, core::ptr::null_mut()) + } else { + core::ptr::null_mut() + } + } +} + +// ============================================================================ +// String operations +// ============================================================================ + +/// Allocate a string buffer +#[no_mangle] +pub extern "C" fn string_alloc(len: usize) -> *mut u8 { + crate::alloc::allocate(len, 1) +} + +/// Free a string buffer +#[no_mangle] +pub unsafe extern "C" fn string_free(ptr: *mut u8, len: usize) { + crate::alloc::deallocate(ptr, len, 1) +} + +/// Copy string to host +#[no_mangle] +pub extern "C" fn string_to_host(s: FfiString) -> *const u8 { + s.ptr +} + +// ============================================================================ +// JavaScript interop (wasm-bindgen compatible) +// ============================================================================ + +#[cfg(target_arch = "wasm32")] +mod js { + use super::*; + + extern "C" { + // These would be provided by wasm-bindgen or manual JS glue + + /// Log to console + #[link_name = "__affinescript_console_log"] + pub fn console_log(msg: *const u8, len: usize); + + /// Get current time in milliseconds + #[link_name = "__affinescript_now"] + pub fn now() -> f64; + + /// Call JavaScript function + #[link_name = "__affinescript_js_call"] + pub fn js_call(func_id: u32, arg: *const (), arg_len: usize) -> *mut (); + } + + /// Print to console + #[no_mangle] + pub extern "C" fn print(s: FfiString) { + unsafe { + console_log(s.ptr, s.len); + } + } +} + +// ============================================================================ +// WASI support +// ============================================================================ + +#[cfg(feature = "wasi")] +mod wasi_ffi { + use super::*; + + /// Write to stdout + #[no_mangle] + pub extern "C" fn wasi_print(s: FfiString) { + // TODO: Phase 6 implementation + // - [ ] Use fd_write to stdout + } + + /// Read from stdin + #[no_mangle] + pub extern "C" fn wasi_read_line() -> FfiString { + // TODO: Phase 6 implementation + // - [ ] Use fd_read from stdin + FfiString::new(core::ptr::null(), 0) + } + + /// Get environment variable + #[no_mangle] + pub extern "C" fn wasi_getenv(name: FfiString) -> FfiString { + // TODO: Phase 6 implementation + // - [ ] Use environ_get + FfiString::new(core::ptr::null(), 0) + } + + /// Get command line arguments + #[no_mangle] + pub extern "C" fn wasi_args() -> FfiArray { + // TODO: Phase 6 implementation + // - [ ] Use args_get + FfiArray { + ptr: core::ptr::null(), + len: 0, + } + } +} + +// ============================================================================ +// Type conversions +// ============================================================================ + +/// Convert AffineScript Int to host i64 +#[no_mangle] +pub extern "C" fn int_to_i64(value: *const ()) -> i64 { + // TODO: Phase 6 implementation + // AffineScript Ints may be arbitrary precision + 0 +} + +/// Convert host i64 to AffineScript Int +#[no_mangle] +pub extern "C" fn i64_to_int(value: i64) -> *mut () { + // TODO: Phase 6 implementation + core::ptr::null_mut() +} + +/// Convert AffineScript Float to host f64 +#[no_mangle] +pub extern "C" fn float_to_f64(value: *const ()) -> f64 { + // TODO: Phase 6 implementation + 0.0 +} + +/// Convert host f64 to AffineScript Float +#[no_mangle] +pub extern "C" fn f64_to_float(value: f64) -> *mut () { + // TODO: Phase 6 implementation + core::ptr::null_mut() +} + +// TODO: Phase 6 implementation +// - [ ] Implement wasm-bindgen integration +// - [ ] Add JSON serialization for complex types +// - [ ] Implement async callback support +// - [ ] Add TypeScript type generation +// - [ ] Implement Component Model interface types (future) diff --git a/runtime/src/gc.rs b/runtime/src/gc.rs new file mode 100644 index 0000000..5bceb3d --- /dev/null +++ b/runtime/src/gc.rs @@ -0,0 +1,279 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright 2024 AffineScript Contributors + +//! Optional Garbage Collector for AffineScript Runtime +//! +//! This module provides a simple mark-sweep garbage collector for +//! cyclic data structures with ω (unrestricted) quantity. +//! +//! # When GC is Needed +//! +//! Most AffineScript data is linear or affine and doesn't need GC. +//! However, ω-quantity data (unrestricted/shareable) can form cycles: +//! +//! ```affinescript +//! type Node = { +//! value: Int, +//! ω next: Option[Node] // Can be shared, may form cycles +//! } +//! ``` +//! +//! # Algorithm +//! +//! Simple mark-sweep: +//! 1. Mark: Trace from roots, mark reachable objects +//! 2. Sweep: Free unmarked objects +//! +//! # Performance +//! +//! - Collection is triggered when: +//! - Allocation fails and memory is low +//! - Explicit `gc::collect()` call +//! - Threshold of ω allocations reached +//! +//! - This GC is intentionally simple; most memory is managed +//! via ownership and doesn't need GC. + +#[cfg(not(feature = "std"))] +use core::ptr::NonNull; + +#[cfg(feature = "std")] +use std::ptr::NonNull; + +/// GC object header +/// +/// Prepended to all GC-managed allocations. +#[repr(C)] +pub struct GcHeader { + /// Mark bit (set during marking phase) + pub marked: bool, + /// Object size (excluding header) + pub size: u32, + /// Type tag (for tracing) + pub type_tag: u32, + /// Next object in allocation list + pub next: *mut GcHeader, +} + +/// GC-managed pointer +/// +/// Smart pointer for garbage-collected values. +#[repr(transparent)] +pub struct Gc { + ptr: NonNull, + _marker: core::marker::PhantomData, +} + +impl Gc { + /// Allocate a new GC-managed value + pub fn new(value: T) -> Self { + // TODO: Phase 6 implementation + // - [ ] Allocate space for header + value + // - [ ] Initialize header + // - [ ] Register in allocation list + // - [ ] Store value + + unimplemented!("GC allocation not yet implemented") + } +} + +/// Mutable GC cell (interior mutability) +pub struct GcCell { + inner: Gc>, +} + +/// GC statistics +#[derive(Default)] +pub struct GcStats { + /// Total collections performed + pub collections: usize, + /// Objects currently alive + pub live_objects: usize, + /// Bytes currently allocated + pub live_bytes: usize, + /// Objects freed in last collection + pub last_freed: usize, +} + +/// Global GC state +struct GcState { + /// Head of allocation list + alloc_list: *mut GcHeader, + /// Number of allocations since last GC + alloc_count: usize, + /// Threshold to trigger collection + threshold: usize, + /// Statistics + stats: GcStats, + /// Root set + roots: [*mut GcHeader; 256], + /// Number of roots + root_count: usize, +} + +static mut GC_STATE: GcState = GcState { + alloc_list: core::ptr::null_mut(), + alloc_count: 0, + threshold: 1000, + stats: GcStats { + collections: 0, + live_objects: 0, + live_bytes: 0, + last_freed: 0, + }, + roots: [core::ptr::null_mut(); 256], + root_count: 0, +}; + +/// Initialize the garbage collector +pub fn init() { + // TODO: Phase 6 implementation + // - [ ] Set up allocation list + // - [ ] Initialize root set + // - [ ] Set threshold based on available memory +} + +/// Perform garbage collection +#[no_mangle] +pub extern "C" fn collect() { + unsafe { + GC_STATE.stats.collections += 1; + + // Mark phase + mark_from_roots(); + + // Sweep phase + sweep(); + + // Reset allocation counter + GC_STATE.alloc_count = 0; + } +} + +/// Mark phase: trace from roots +unsafe fn mark_from_roots() { + // TODO: Phase 6 implementation + // - [ ] Mark all roots + // - [ ] Recursively mark reachable objects + // - [ ] Handle cycles (already marked = skip) +} + +/// Sweep phase: free unmarked objects +unsafe fn sweep() { + // TODO: Phase 6 implementation + // - [ ] Walk allocation list + // - [ ] Free unmarked objects + // - [ ] Clear marks on surviving objects + // - [ ] Update statistics +} + +/// Register a root +/// +/// Roots are objects that should not be collected even if +/// not reachable from other GC objects. +#[no_mangle] +pub extern "C" fn gc_add_root(obj: *mut GcHeader) { + unsafe { + if GC_STATE.root_count < GC_STATE.roots.len() { + GC_STATE.roots[GC_STATE.root_count] = obj; + GC_STATE.root_count += 1; + } + } +} + +/// Unregister a root +#[no_mangle] +pub extern "C" fn gc_remove_root(obj: *mut GcHeader) { + unsafe { + for i in 0..GC_STATE.root_count { + if GC_STATE.roots[i] == obj { + // Swap with last and decrease count + GC_STATE.roots[i] = GC_STATE.roots[GC_STATE.root_count - 1]; + GC_STATE.root_count -= 1; + break; + } + } + } +} + +/// Allocate GC-managed memory +#[no_mangle] +pub extern "C" fn gc_alloc(size: usize, type_tag: u32) -> *mut GcHeader { + // Check if we should collect first + unsafe { + if GC_STATE.alloc_count >= GC_STATE.threshold { + collect(); + } + } + + // Allocate header + data + let total_size = core::mem::size_of::() + size; + let ptr = crate::alloc::allocate(total_size, core::mem::align_of::()); + + if ptr.is_null() { + // Try collecting and retry + collect(); + let ptr = crate::alloc::allocate(total_size, core::mem::align_of::()); + if ptr.is_null() { + return core::ptr::null_mut(); + } + } + + // Initialize header + let header = ptr as *mut GcHeader; + unsafe { + (*header).marked = false; + (*header).size = size as u32; + (*header).type_tag = type_tag; + + // Add to allocation list + (*header).next = GC_STATE.alloc_list; + GC_STATE.alloc_list = header; + GC_STATE.alloc_count += 1; + GC_STATE.stats.live_objects += 1; + GC_STATE.stats.live_bytes += total_size; + } + + header +} + +/// Get data pointer from header +#[no_mangle] +pub extern "C" fn gc_data(header: *mut GcHeader) -> *mut u8 { + if header.is_null() { + return core::ptr::null_mut(); + } + unsafe { header.add(1) as *mut u8 } +} + +/// Get GC statistics +#[no_mangle] +pub extern "C" fn gc_stats() -> GcStats { + unsafe { GC_STATE.stats } +} + +/// Force a collection if above threshold +#[no_mangle] +pub extern "C" fn gc_maybe_collect() { + unsafe { + if GC_STATE.alloc_count >= GC_STATE.threshold { + collect(); + } + } +} + +/// Set collection threshold +#[no_mangle] +pub extern "C" fn gc_set_threshold(threshold: usize) { + unsafe { + GC_STATE.threshold = threshold; + } +} + +// TODO: Phase 6 implementation +// - [ ] Implement proper mark phase with type-based tracing +// - [ ] Implement sweep with proper memory deallocation +// - [ ] Add write barrier for generational GC (optional) +// - [ ] Add incremental collection (optional) +// - [ ] Add finalization support +// - [ ] Integrate with effect system for GC-safe points diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs new file mode 100644 index 0000000..a60639c --- /dev/null +++ b/runtime/src/lib.rs @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright 2024 AffineScript Contributors + +//! AffineScript Runtime Library +//! +//! This crate provides the runtime support for AffineScript programs +//! compiled to WebAssembly. +//! +//! # Features +//! +//! - `std`: Enable standard library support (default) +//! - `gc`: Enable garbage collector for cyclic data +//! - `wasi`: Enable WASI support for CLI programs +//! +//! # Modules +//! +//! - [`alloc`]: Memory allocation +//! - [`effects`]: Effect handling runtime +//! - [`panic`]: Panic handling +//! - [`ffi`]: Foreign function interface + +#![cfg_attr(not(feature = "std"), no_std)] +#![warn(missing_docs)] + +#[cfg(not(feature = "std"))] +extern crate alloc as std_alloc; + +pub mod alloc; +pub mod effects; +pub mod panic; +pub mod ffi; + +#[cfg(feature = "gc")] +pub mod gc; + +/// Re-exports for generated code +pub mod prelude { + pub use crate::effects::{Evidence, Handler, resume}; + pub use crate::alloc::{allocate, deallocate}; + + #[cfg(feature = "gc")] + pub use crate::gc::{Gc, GcCell}; +} + +/// Runtime initialization +/// +/// Called at the start of every AffineScript program. +#[no_mangle] +pub extern "C" fn __affinescript_init() { + // Initialize allocator + alloc::init(); + + // Initialize panic handler + panic::init(); + + // Initialize effect system + effects::init(); +} + +/// Runtime cleanup +/// +/// Called at the end of every AffineScript program. +#[no_mangle] +pub extern "C" fn __affinescript_cleanup() { + #[cfg(feature = "gc")] + gc::collect(); +} + +// TODO: Phase 6 implementation +// - [ ] Memory allocator optimized for linear values +// - [ ] Effect evidence passing runtime +// - [ ] Handler frame management +// - [ ] Continuation allocation/deallocation +// - [ ] WASI integration +// - [ ] JavaScript interop via wasm-bindgen diff --git a/runtime/src/panic.rs b/runtime/src/panic.rs new file mode 100644 index 0000000..efe6d97 --- /dev/null +++ b/runtime/src/panic.rs @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright 2024 AffineScript Contributors + +//! Panic Handling for AffineScript Runtime +//! +//! This module provides panic and error handling infrastructure. +//! Since AffineScript compiles to WASM, panics need special handling. +//! +//! # Panic Strategy +//! +//! 1. **Abort**: Immediately terminate execution (default for WASM) +//! 2. **Trap**: Trigger WASM trap instruction +//! 3. **Unwind**: Stack unwinding (requires exception handling proposal) +//! +//! # Error Reporting +//! +//! Errors are reported to the host via: +//! - WASI stderr (if available) +//! - Host-provided callback +//! - WASM trap with error code + +#[cfg(not(feature = "std"))] +use core::fmt::{self, Write}; + +#[cfg(feature = "std")] +use std::fmt::{self, Write}; + +/// Panic information +#[repr(C)] +pub struct PanicInfo { + /// Error message + pub message: *const u8, + /// Message length + pub message_len: usize, + /// Source file + pub file: *const u8, + /// File name length + pub file_len: usize, + /// Line number + pub line: u32, + /// Column number + pub column: u32, +} + +/// Panic hook type +pub type PanicHook = extern "C" fn(*const PanicInfo); + +/// Global panic hook +static mut PANIC_HOOK: Option = None; + +/// Error codes for WASM traps +#[repr(u32)] +pub enum ErrorCode { + /// General panic + Panic = 1, + /// Out of memory + OutOfMemory = 2, + /// Stack overflow + StackOverflow = 3, + /// Integer overflow + IntegerOverflow = 4, + /// Division by zero + DivisionByZero = 5, + /// Array index out of bounds + IndexOutOfBounds = 6, + /// Use after move (linear type violation) + UseAfterMove = 7, + /// Borrow checker violation + BorrowViolation = 8, + /// Unhandled effect + UnhandledEffect = 9, + /// Assertion failure + AssertionFailed = 10, + /// Unreachable code + Unreachable = 11, +} + +/// Initialize panic handling +pub fn init() { + // TODO: Phase 6 implementation + // - [ ] Set up default panic hook + // - [ ] Register with host for error reporting + // - [ ] Initialize stack canaries (if enabled) +} + +/// Set custom panic hook +/// +/// # Arguments +/// +/// * `hook` - Function to call on panic +#[no_mangle] +pub extern "C" fn set_panic_hook(hook: PanicHook) { + unsafe { + PANIC_HOOK = Some(hook); + } +} + +/// Panic with message +/// +/// # Arguments +/// +/// * `message` - Error message +/// * `file` - Source file name +/// * `line` - Line number +/// * `column` - Column number +#[no_mangle] +pub extern "C" fn panic( + message: *const u8, + message_len: usize, + file: *const u8, + file_len: usize, + line: u32, + column: u32, +) -> ! { + let info = PanicInfo { + message, + message_len, + file, + file_len, + line, + column, + }; + + unsafe { + if let Some(hook) = PANIC_HOOK { + hook(&info); + } + } + + // Abort execution + abort() +} + +/// Panic with error code +/// +/// # Arguments +/// +/// * `code` - Error code indicating the type of error +#[no_mangle] +pub extern "C" fn panic_code(code: ErrorCode) -> ! { + // TODO: Phase 6 implementation + // - [ ] Convert code to message + // - [ ] Call panic hook + // - [ ] Trap with code + + abort() +} + +/// Abort execution +#[no_mangle] +pub extern "C" fn abort() -> ! { + #[cfg(target_arch = "wasm32")] + { + core::arch::wasm32::unreachable() + } + + #[cfg(not(target_arch = "wasm32"))] + { + // For non-WASM targets (testing) + #[cfg(feature = "std")] + std::process::abort(); + + #[cfg(not(feature = "std"))] + loop {} + } +} + +/// Assert condition +/// +/// # Arguments +/// +/// * `condition` - Condition to check +/// * `message` - Error message if condition is false +#[no_mangle] +pub extern "C" fn assert( + condition: bool, + message: *const u8, + message_len: usize, + file: *const u8, + file_len: usize, + line: u32, + column: u32, +) { + if !condition { + panic(message, message_len, file, file_len, line, column) + } +} + +/// Debug assertion (only in debug builds) +#[no_mangle] +pub extern "C" fn debug_assert( + condition: bool, + message: *const u8, + message_len: usize, + file: *const u8, + file_len: usize, + line: u32, + column: u32, +) { + #[cfg(debug_assertions)] + if !condition { + panic(message, message_len, file, file_len, line, column) + } +} + +/// Report use-after-move error +#[no_mangle] +pub extern "C" fn use_after_move( + var_name: *const u8, + var_name_len: usize, + file: *const u8, + file_len: usize, + line: u32, + column: u32, +) -> ! { + // TODO: Phase 6 implementation + // - [ ] Format error message + // - [ ] Include variable name + // - [ ] Call panic + + panic_code(ErrorCode::UseAfterMove) +} + +/// Report borrow violation error +#[no_mangle] +pub extern "C" fn borrow_violation( + message: *const u8, + message_len: usize, + file: *const u8, + file_len: usize, + line: u32, + column: u32, +) -> ! { + panic(message, message_len, file, file_len, line, column) +} + +// TODO: Phase 6 implementation +// - [ ] Implement WASI error output +// - [ ] Add stack trace collection (if debug info available) +// - [ ] Add error code documentation +// - [ ] Implement exception handling proposal support (optional) +// - [ ] Add runtime assertions for invariants diff --git a/tools/affine-doc/Cargo.toml b/tools/affine-doc/Cargo.toml new file mode 100644 index 0000000..6952613 --- /dev/null +++ b/tools/affine-doc/Cargo.toml @@ -0,0 +1,55 @@ +# SPDX-License-Identifier: Apache-2.0 OR MIT +# Copyright 2024 AffineScript Contributors + +[package] +name = "affine-doc" +version = "0.1.0" +edition = "2021" +description = "Documentation generator for AffineScript" +license = "Apache-2.0 OR MIT" +repository = "https://github.com/affinescript/affinescript" +keywords = ["documentation", "affinescript"] +categories = ["development-tools", "command-line-utilities"] + +[dependencies] +# CLI +clap = { version = "4", features = ["derive"] } + +# HTML generation +askama = "0.12" +pulldown-cmark = "0.9" + +# Syntax highlighting +syntect = "5" + +# Serialization +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +# Path handling +camino = "1" +walkdir = "2" + +# Logging +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +# Error handling +thiserror = "1" +anyhow = "1" + +# Search index +tantivy = "0.21" + +# HTTP server (for preview) +axum = { version = "0.7", optional = true } +tokio = { version = "1", features = ["full"], optional = true } +tower-http = { version = "0.5", features = ["fs"], optional = true } + +[features] +default = [] +serve = ["dep:axum", "dep:tokio", "dep:tower-http"] + +[[bin]] +name = "affine-doc" +path = "src/main.rs" diff --git a/tools/affine-doc/assets/search.js b/tools/affine-doc/assets/search.js new file mode 100644 index 0000000..0601e10 --- /dev/null +++ b/tools/affine-doc/assets/search.js @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +// AffineScript Documentation Search + +(function() { + 'use strict'; + + // Wait for DOM and search index to load + document.addEventListener('DOMContentLoaded', function() { + const searchInput = document.getElementById('search'); + if (!searchInput) return; + + let searchIndex = []; + let searchResults = null; + + // Load search index + if (window.searchIndex) { + searchIndex = window.searchIndex; + } + + // Create results container + searchResults = document.createElement('div'); + searchResults.id = 'search-results'; + searchResults.className = 'search-results'; + searchInput.parentNode.appendChild(searchResults); + + // Search function + function search(query) { + if (!query || query.length < 2) { + searchResults.innerHTML = ''; + searchResults.style.display = 'none'; + return; + } + + const queryLower = query.toLowerCase(); + const results = []; + + for (const entry of searchIndex) { + const score = computeScore(entry, queryLower); + if (score > 0) { + results.push({ entry, score }); + } + } + + // Sort by score + results.sort((a, b) => b.score - a.score); + + // Limit results + const topResults = results.slice(0, 20); + + // Render results + renderResults(topResults); + } + + function computeScore(entry, query) { + const nameLower = entry.name.toLowerCase(); + const pathLower = entry.path.toLowerCase(); + + if (nameLower === query) return 100; + if (nameLower.startsWith(query)) return 50; + if (nameLower.includes(query)) return 25; + if (pathLower.includes(query)) return 10; + + return 0; + } + + function renderResults(results) { + if (results.length === 0) { + searchResults.innerHTML = '
No results found
'; + searchResults.style.display = 'block'; + return; + } + + const html = results.map(function(r) { + return ` + + ${r.entry.kind} + ${r.entry.name} + ${r.entry.path} + ${r.entry.description ? `${r.entry.description}` : ''} + + `; + }).join(''); + + searchResults.innerHTML = html; + searchResults.style.display = 'block'; + } + + // Debounce search input + let debounceTimer; + searchInput.addEventListener('input', function() { + clearTimeout(debounceTimer); + debounceTimer = setTimeout(function() { + search(searchInput.value); + }, 150); + }); + + // Close results on outside click + document.addEventListener('click', function(e) { + if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) { + searchResults.style.display = 'none'; + } + }); + + // Keyboard navigation + searchInput.addEventListener('keydown', function(e) { + const results = searchResults.querySelectorAll('.search-result'); + const active = searchResults.querySelector('.search-result.active'); + let index = Array.from(results).indexOf(active); + + if (e.key === 'ArrowDown') { + e.preventDefault(); + if (active) active.classList.remove('active'); + index = (index + 1) % results.length; + if (results[index]) results[index].classList.add('active'); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + if (active) active.classList.remove('active'); + index = (index - 1 + results.length) % results.length; + if (results[index]) results[index].classList.add('active'); + } else if (e.key === 'Enter') { + if (active) { + window.location.href = active.href; + } + } else if (e.key === 'Escape') { + searchResults.style.display = 'none'; + searchInput.blur(); + } + }); + + // Focus search with / + document.addEventListener('keydown', function(e) { + if (e.key === '/' && document.activeElement !== searchInput) { + e.preventDefault(); + searchInput.focus(); + } + }); + }); +})(); diff --git a/tools/affine-doc/assets/style.css b/tools/affine-doc/assets/style.css new file mode 100644 index 0000000..6d80d77 --- /dev/null +++ b/tools/affine-doc/assets/style.css @@ -0,0 +1,228 @@ +/* SPDX-License-Identifier: Apache-2.0 OR MIT */ +/* AffineScript Documentation Styles */ + +:root { + --bg-primary: #ffffff; + --bg-secondary: #f5f5f5; + --text-primary: #1a1a1a; + --text-secondary: #666666; + --accent: #0066cc; + --accent-hover: #0052a3; + --border: #e0e0e0; + --code-bg: #f0f0f0; + --sidebar-width: 280px; +} + +.theme-dark { + --bg-primary: #1a1a1a; + --bg-secondary: #2d2d2d; + --text-primary: #e0e0e0; + --text-secondary: #a0a0a0; + --accent: #4da6ff; + --accent-hover: #80bfff; + --border: #404040; + --code-bg: #2d2d2d; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + background-color: var(--bg-primary); + color: var(--text-primary); + line-height: 1.6; +} + +/* Sidebar */ +.sidebar { + position: fixed; + left: 0; + top: 0; + width: var(--sidebar-width); + height: 100vh; + background-color: var(--bg-secondary); + border-right: 1px solid var(--border); + overflow-y: auto; + padding: 1rem; +} + +.search input { + width: 100%; + padding: 0.5rem; + border: 1px solid var(--border); + border-radius: 4px; + background-color: var(--bg-primary); + color: var(--text-primary); +} + +/* Main content */ +main { + margin-left: var(--sidebar-width); + padding: 2rem; + max-width: 900px; +} + +/* Typography */ +h1, h2, h3, h4, h5, h6 { + margin-top: 1.5rem; + margin-bottom: 0.75rem; + font-weight: 600; +} + +h1 { font-size: 2rem; } +h2 { font-size: 1.5rem; border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; } +h3 { font-size: 1.25rem; } +h4 { font-size: 1.1rem; } + +a { + color: var(--accent); + text-decoration: none; +} + +a:hover { + color: var(--accent-hover); + text-decoration: underline; +} + +/* Code */ +code { + font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, monospace; + font-size: 0.9em; + background-color: var(--code-bg); + padding: 0.2em 0.4em; + border-radius: 3px; +} + +pre { + background-color: var(--code-bg); + padding: 1rem; + border-radius: 6px; + overflow-x: auto; + margin: 1rem 0; +} + +pre code { + background: none; + padding: 0; +} + +.signature { + background-color: var(--code-bg); + padding: 0.75rem 1rem; + border-radius: 6px; + border-left: 3px solid var(--accent); + margin: 0.5rem 0 1rem 0; +} + +/* Items */ +.item { + margin: 2rem 0; + padding: 1rem; + border: 1px solid var(--border); + border-radius: 6px; +} + +.item-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.kind { + font-size: 0.75rem; + text-transform: uppercase; + color: var(--text-secondary); + background-color: var(--bg-secondary); + padding: 0.2em 0.5em; + border-radius: 3px; +} + +.name { + font-size: 1.25rem; + font-weight: 600; +} + +.item-doc { + margin-top: 1rem; +} + +/* Quantities */ +.quantity-erased { color: #888; } +.quantity-linear { color: #d32f2f; font-weight: bold; } +.quantity-unrestricted { color: #388e3c; } + +/* Effects */ +.effects { + color: #7b1fa2; +} + +.effect { + color: #7b1fa2; +} + +/* Badges */ +.badge { + display: inline-block; + font-size: 0.75rem; + padding: 0.2em 0.5em; + border-radius: 3px; + margin-left: 0.5rem; +} + +.stability-stable { background-color: #c8e6c9; color: #2e7d32; } +.stability-unstable { background-color: #fff9c4; color: #f9a825; } +.stability-experimental { background-color: #ffccbc; color: #e64a19; } +.stability-deprecated { background-color: #ffcdd2; color: #c62828; } + +/* Deprecated */ +.deprecated { + background-color: #fff3e0; + border: 1px solid #ff9800; + border-radius: 4px; + padding: 0.75rem; + margin: 0.5rem 0; +} + +/* Module list */ +.module-list { + list-style: none; +} + +.module-list li { + padding: 0.5rem 0; + border-bottom: 1px solid var(--border); +} + +/* Examples */ +.example { + margin: 1rem 0; + padding: 1rem; + background-color: var(--bg-secondary); + border-radius: 6px; +} + +.example h4 { + margin-top: 0; + font-size: 0.9rem; + color: var(--text-secondary); +} + +/* Responsive */ +@media (max-width: 768px) { + .sidebar { + position: static; + width: 100%; + height: auto; + border-right: none; + border-bottom: 1px solid var(--border); + } + + main { + margin-left: 0; + } +} diff --git a/tools/affine-doc/src/extract.rs b/tools/affine-doc/src/extract.rs new file mode 100644 index 0000000..0e91293 --- /dev/null +++ b/tools/affine-doc/src/extract.rs @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright 2024 AffineScript Contributors + +//! Documentation Extraction +//! +//! Extracts documentation from AffineScript source code. + +use std::path::Path; + +/// Extracted documentation for a module +#[derive(Debug, Clone)] +pub struct ModuleDoc { + /// Module path (e.g., "std::collections::vec") + pub path: Vec, + + /// Module-level documentation + pub doc: Option, + + /// Items in this module + pub items: Vec, + + /// Submodules + pub submodules: Vec, +} + +/// Documentation for an item +#[derive(Debug, Clone)] +pub struct ItemDoc { + /// Item name + pub name: String, + + /// Item kind + pub kind: ItemKind, + + /// Documentation comment + pub doc: Option, + + /// Type signature + pub signature: String, + + /// Source location + pub location: SourceLocation, + + /// Visibility + pub visibility: Visibility, + + /// Type parameters + pub type_params: Vec, + + /// Effects (for functions) + pub effects: Vec, + + /// Examples from doc comments + pub examples: Vec, +} + +/// Item kind +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ItemKind { + /// Function + Function, + /// Type alias + TypeAlias, + /// Struct + Struct, + /// Enum + Enum, + /// Trait + Trait, + /// Effect + Effect, + /// Constant + Const, + /// Static + Static, + /// Impl block + Impl, + /// Module + Module, +} + +impl ItemKind { + /// Display name for the item kind + pub fn display_name(&self) -> &'static str { + match self { + ItemKind::Function => "Function", + ItemKind::TypeAlias => "Type Alias", + ItemKind::Struct => "Struct", + ItemKind::Enum => "Enum", + ItemKind::Trait => "Trait", + ItemKind::Effect => "Effect", + ItemKind::Const => "Constant", + ItemKind::Static => "Static", + ItemKind::Impl => "Implementation", + ItemKind::Module => "Module", + } + } +} + +/// Visibility level +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Visibility { + /// Public + Public, + /// Public within crate + Crate, + /// Private + Private, +} + +/// Source location +#[derive(Debug, Clone)] +pub struct SourceLocation { + /// File path + pub file: String, + /// Line number + pub line: u32, + /// Column number + pub column: u32, +} + +/// Type parameter documentation +#[derive(Debug, Clone)] +pub struct TypeParamDoc { + /// Parameter name + pub name: String, + /// Bounds + pub bounds: Vec, + /// Default value + pub default: Option, +} + +/// Documentation extractor +pub struct Extractor { + /// Include private items + include_private: bool, +} + +impl Extractor { + /// Create a new extractor + pub fn new(include_private: bool) -> Self { + Extractor { include_private } + } + + /// Extract documentation from a source file + pub fn extract_file(&self, path: impl AsRef) -> anyhow::Result { + let _path = path.as_ref(); + + // TODO: Phase 8 implementation + // - [ ] Parse source file + // - [ ] Walk AST + // - [ ] Extract doc comments + // - [ ] Build ModuleDoc + + Ok(ModuleDoc { + path: vec![], + doc: None, + items: vec![], + submodules: vec![], + }) + } + + /// Extract documentation from a directory + pub fn extract_dir(&self, path: impl AsRef) -> anyhow::Result> { + let _path = path.as_ref(); + + // TODO: Phase 8 implementation + // - [ ] Find all .afs files + // - [ ] Extract each file + // - [ ] Build module hierarchy + + Ok(vec![]) + } + + /// Parse doc comment into structured sections + pub fn parse_doc_comment(&self, comment: &str) -> DocComment { + let mut description = String::new(); + let mut params = vec![]; + let mut returns = None; + let mut examples = vec![]; + let mut panics = None; + let mut safety = None; + + let mut current_section = "description"; + let mut current_content = String::new(); + + for line in comment.lines() { + let line = line.trim(); + + // Check for section headers + if line.starts_with("# ") { + // Save previous section + self.save_section( + current_section, + ¤t_content, + &mut description, + &mut params, + &mut returns, + &mut examples, + &mut panics, + &mut safety, + ); + + // Start new section + current_section = match line.to_lowercase().as_str() { + "# parameters" | "# arguments" => "params", + "# returns" => "returns", + "# examples" | "# example" => "examples", + "# panics" => "panics", + "# safety" => "safety", + _ => "description", + }; + current_content.clear(); + } else { + current_content.push_str(line); + current_content.push('\n'); + } + } + + // Save last section + self.save_section( + current_section, + ¤t_content, + &mut description, + &mut params, + &mut returns, + &mut examples, + &mut panics, + &mut safety, + ); + + DocComment { + description, + params, + returns, + examples, + panics, + safety, + } + } + + fn save_section( + &self, + section: &str, + content: &str, + description: &mut String, + _params: &mut Vec<(String, String)>, + returns: &mut Option, + examples: &mut Vec, + panics: &mut Option, + safety: &mut Option, + ) { + let content = content.trim(); + if content.is_empty() { + return; + } + + match section { + "description" => *description = content.to_string(), + "params" => { + // TODO: Parse parameter docs + } + "returns" => *returns = Some(content.to_string()), + "examples" => examples.push(content.to_string()), + "panics" => *panics = Some(content.to_string()), + "safety" => *safety = Some(content.to_string()), + _ => {} + } + } +} + +/// Parsed doc comment +#[derive(Debug, Clone)] +pub struct DocComment { + /// Main description + pub description: String, + /// Parameter documentation + pub params: Vec<(String, String)>, + /// Return value documentation + pub returns: Option, + /// Example code blocks + pub examples: Vec, + /// Panic conditions + pub panics: Option, + /// Safety requirements (for unsafe functions) + pub safety: Option, +} + +// TODO: Phase 8 implementation +// - [ ] Connect to AffineScript parser +// - [ ] Handle attribute macros (#[doc], #[deprecated], etc.) +// - [ ] Extract impl blocks +// - [ ] Handle re-exports +// - [ ] Support module-level docs (//! comments) diff --git a/tools/affine-doc/src/html.rs b/tools/affine-doc/src/html.rs new file mode 100644 index 0000000..b3302e3 --- /dev/null +++ b/tools/affine-doc/src/html.rs @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright 2024 AffineScript Contributors + +//! HTML Generation +//! +//! Generates HTML documentation pages. + +use crate::extract::{ItemDoc, ItemKind, ModuleDoc}; +use crate::render; +use std::path::Path; + +/// HTML generator configuration +#[derive(Debug, Clone)] +pub struct HtmlConfig { + /// Output directory + pub output_dir: std::path::PathBuf, + + /// Package name + pub package_name: String, + + /// Package version + pub package_version: String, + + /// Custom CSS + pub custom_css: Option, + + /// Theme (light, dark, auto) + pub theme: String, +} + +/// HTML generator +pub struct HtmlGenerator { + config: HtmlConfig, +} + +impl HtmlGenerator { + /// Create a new HTML generator + pub fn new(config: HtmlConfig) -> Self { + HtmlGenerator { config } + } + + /// Generate documentation for all modules + pub fn generate(&self, modules: &[ModuleDoc]) -> anyhow::Result<()> { + // Create output directory + std::fs::create_dir_all(&self.config.output_dir)?; + + // Generate static assets + self.generate_assets()?; + + // Generate index page + self.generate_index(modules)?; + + // Generate module pages + for module in modules { + self.generate_module(module)?; + } + + // Generate search index + self.generate_search_index(modules)?; + + Ok(()) + } + + /// Generate static assets (CSS, JS) + fn generate_assets(&self) -> anyhow::Result<()> { + let css = include_str!("../assets/style.css"); + let js = include_str!("../assets/search.js"); + + std::fs::write(self.config.output_dir.join("style.css"), css)?; + std::fs::write(self.config.output_dir.join("search.js"), js)?; + + Ok(()) + } + + /// Generate index page + fn generate_index(&self, modules: &[ModuleDoc]) -> anyhow::Result<()> { + let mut html = self.page_header("Index"); + + html.push_str("
"); + html.push_str(&format!("

{}

", self.config.package_name)); + html.push_str(&format!( + "

Version {}

", + self.config.package_version + )); + + html.push_str("

Modules

"); + html.push_str("
    "); + for module in modules { + let path = module.path.join("::"); + html.push_str(&format!( + "
  • {}
  • ", + path.replace("::", "/"), + path + )); + } + html.push_str("
"); + + html.push_str("
"); + html.push_str(&self.page_footer()); + + std::fs::write(self.config.output_dir.join("index.html"), html)?; + Ok(()) + } + + /// Generate module documentation + fn generate_module(&self, module: &ModuleDoc) -> anyhow::Result<()> { + let module_path = module.path.join("::"); + let mut html = self.page_header(&module_path); + + html.push_str("
"); + html.push_str(&format!("

Module {}

", module_path)); + + if let Some(doc) = &module.doc { + html.push_str("
"); + html.push_str(&render::render_markdown(doc)); + html.push_str("
"); + } + + // Group items by kind + let mut functions = vec![]; + let mut types = vec![]; + let mut traits = vec![]; + let mut effects = vec![]; + + for item in &module.items { + match item.kind { + ItemKind::Function => functions.push(item), + ItemKind::Struct | ItemKind::Enum | ItemKind::TypeAlias => types.push(item), + ItemKind::Trait => traits.push(item), + ItemKind::Effect => effects.push(item), + _ => {} + } + } + + // Render sections + if !types.is_empty() { + html.push_str(&self.render_section("Types", &types)); + } + if !traits.is_empty() { + html.push_str(&self.render_section("Traits", &traits)); + } + if !effects.is_empty() { + html.push_str(&self.render_section("Effects", &effects)); + } + if !functions.is_empty() { + html.push_str(&self.render_section("Functions", &functions)); + } + + html.push_str("
"); + html.push_str(&self.page_footer()); + + // Write file + let file_path = self + .config + .output_dir + .join(format!("{}.html", module_path.replace("::", "/"))); + + if let Some(parent) = file_path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::write(file_path, html)?; + + Ok(()) + } + + /// Render a section of items + fn render_section(&self, title: &str, items: &[&ItemDoc]) -> String { + let mut html = format!("
"); + html.push_str(&format!("

{}

", title)); + + for item in items { + html.push_str(&self.render_item(item)); + } + + html.push_str("
"); + html + } + + /// Render a single item + fn render_item(&self, item: &ItemDoc) -> String { + let mut html = format!( + "
", + item.kind.display_name().to_lowercase(), + item.name + ); + + // Header + html.push_str("
"); + html.push_str(&format!( + "{}", + item.kind.display_name() + )); + html.push_str(&format!("{}", item.name)); + html.push_str("
"); + + // Signature + html.push_str("
");
+        html.push_str(&render::html_escape(&item.signature));
+        html.push_str("
"); + + // Documentation + if let Some(doc) = &item.doc { + html.push_str("
"); + html.push_str(&render::render_markdown(doc)); + html.push_str("
"); + } + + // Examples + for example in &item.examples { + html.push_str("
"); + html.push_str("

Example

"); + html.push_str(&render::render_code(example, Some("affinescript"))); + html.push_str("
"); + } + + html.push_str("
"); + html + } + + /// Generate page header + fn page_header(&self, title: &str) -> String { + format!( + r#" + + + + + {} - {} Documentation + + + + +"#, + title, self.config.package_name, self.config.theme + ) + } + + /// Generate page footer + fn page_footer(&self) -> String { + r#" + + + +"# + .to_string() + } + + /// Generate search index + fn generate_search_index(&self, _modules: &[ModuleDoc]) -> anyhow::Result<()> { + // TODO: Phase 8 implementation + // - [ ] Extract searchable content + // - [ ] Build JSON index + // - [ ] Or build Tantivy index + + Ok(()) + } +} + +// TODO: Phase 8 implementation +// - [ ] Add navigation sidebar +// - [ ] Add breadcrumbs +// - [ ] Add source links +// - [ ] Add copy permalink +// - [ ] Add implementor lists for traits +// - [ ] Add method lists for types +// - [ ] Support custom templates diff --git a/tools/affine-doc/src/index.rs b/tools/affine-doc/src/index.rs new file mode 100644 index 0000000..79028f9 --- /dev/null +++ b/tools/affine-doc/src/index.rs @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright 2024 AffineScript Contributors + +//! Search Index +//! +//! Builds and queries the documentation search index. + +use crate::extract::{ItemDoc, ItemKind, ModuleDoc}; +use serde::{Deserialize, Serialize}; +use std::path::Path; + +/// Search index entry +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SearchEntry { + /// Entry name + pub name: String, + + /// Full path (e.g., "std::vec::Vec::push") + pub path: String, + + /// Item kind + pub kind: String, + + /// Brief description (first line of doc) + pub description: String, + + /// URL to documentation page + pub url: String, +} + +/// Search index builder +pub struct IndexBuilder { + entries: Vec, +} + +impl IndexBuilder { + /// Create a new index builder + pub fn new() -> Self { + IndexBuilder { + entries: Vec::new(), + } + } + + /// Add a module to the index + pub fn add_module(&mut self, module: &ModuleDoc) { + let module_path = module.path.join("::"); + + // Add module itself + self.entries.push(SearchEntry { + name: module.path.last().cloned().unwrap_or_default(), + path: module_path.clone(), + kind: "Module".to_string(), + description: first_line(module.doc.as_deref().unwrap_or("")), + url: format!("{}.html", module_path.replace("::", "/")), + }); + + // Add items + for item in &module.items { + self.add_item(&module_path, item); + } + } + + /// Add an item to the index + fn add_item(&mut self, module_path: &str, item: &ItemDoc) { + let full_path = if module_path.is_empty() { + item.name.clone() + } else { + format!("{}::{}", module_path, item.name) + }; + + self.entries.push(SearchEntry { + name: item.name.clone(), + path: full_path, + kind: item.kind.display_name().to_string(), + description: first_line(item.doc.as_deref().unwrap_or("")), + url: format!( + "{}.html#{}", + module_path.replace("::", "/"), + item.name + ), + }); + } + + /// Build the JSON search index + pub fn build_json(&self) -> String { + serde_json::to_string(&self.entries).unwrap_or_else(|_| "[]".to_string()) + } + + /// Write the search index to disk + pub fn write(&self, output_dir: impl AsRef) -> anyhow::Result<()> { + let json = self.build_json(); + std::fs::write(output_dir.as_ref().join("search-index.js"), format!( + "window.searchIndex = {};", + json + ))?; + Ok(()) + } +} + +impl Default for IndexBuilder { + fn default() -> Self { + Self::new() + } +} + +/// Search query +pub struct SearchQuery { + /// Query text + pub query: String, + + /// Filter by kind + pub kind_filter: Option, + + /// Maximum results + pub limit: usize, +} + +impl Default for SearchQuery { + fn default() -> Self { + SearchQuery { + query: String::new(), + kind_filter: None, + limit: 50, + } + } +} + +/// Search result +#[derive(Debug, Clone)] +pub struct SearchResult { + /// Matching entry + pub entry: SearchEntry, + + /// Relevance score + pub score: f32, +} + +/// Simple in-memory search (for client-side) +pub fn search(entries: &[SearchEntry], query: &SearchQuery) -> Vec { + let query_lower = query.query.to_lowercase(); + + let mut results: Vec = entries + .iter() + .filter_map(|entry| { + // Apply kind filter + if let Some(kind) = &query.kind_filter { + if entry.kind != kind.display_name() { + return None; + } + } + + // Score matching + let score = compute_score(&entry.name, &entry.path, &query_lower); + if score > 0.0 { + Some(SearchResult { + entry: entry.clone(), + score, + }) + } else { + None + } + }) + .collect(); + + // Sort by score (descending) + results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap()); + + // Apply limit + results.truncate(query.limit); + + results +} + +/// Compute search relevance score +fn compute_score(name: &str, path: &str, query: &str) -> f32 { + let name_lower = name.to_lowercase(); + let path_lower = path.to_lowercase(); + + let mut score = 0.0; + + // Exact name match + if name_lower == query { + score += 100.0; + } + // Name starts with query + else if name_lower.starts_with(query) { + score += 50.0; + } + // Name contains query + else if name_lower.contains(query) { + score += 25.0; + } + // Path contains query + else if path_lower.contains(query) { + score += 10.0; + } + + // Bonus for shorter names (more specific) + if score > 0.0 { + score += 10.0 / (name.len() as f32); + } + + score +} + +/// Get first line of text +fn first_line(text: &str) -> String { + text.lines() + .next() + .unwrap_or("") + .trim() + .to_string() +} + +// TODO: Phase 8 implementation +// - [ ] Use Tantivy for full-text search +// - [ ] Add fuzzy matching +// - [ ] Add type signature search +// - [ ] Add effect search +// - [ ] Cache search index diff --git a/tools/affine-doc/src/main.rs b/tools/affine-doc/src/main.rs new file mode 100644 index 0000000..66e14e2 --- /dev/null +++ b/tools/affine-doc/src/main.rs @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright 2024 AffineScript Contributors + +//! AffineScript Documentation Generator +//! +//! Generates API documentation from AffineScript source code. +//! +//! # Features +//! +//! - Extracts doc comments from source +//! - Generates HTML documentation +//! - Creates search index +//! - Supports cross-linking +//! - Renders Markdown in comments + +use clap::{Parser, Subcommand}; +use std::path::PathBuf; + +mod extract; +mod html; +mod index; +mod render; + +#[derive(Parser)] +#[command(name = "affine-doc")] +#[command(about = "AffineScript documentation generator", long_about = None)] +#[command(version)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Generate documentation + Build { + /// Source directory + #[arg(default_value = ".")] + source: PathBuf, + + /// Output directory + #[arg(short, long, default_value = "target/doc")] + output: PathBuf, + + /// Open in browser after building + #[arg(long)] + open: bool, + + /// Include private items + #[arg(long)] + document_private: bool, + + /// Include dependencies + #[arg(long)] + include_deps: bool, + }, + + /// Start documentation server + #[cfg(feature = "serve")] + Serve { + /// Documentation directory + #[arg(default_value = "target/doc")] + dir: PathBuf, + + /// Port to listen on + #[arg(short, long, default_value = "8080")] + port: u16, + }, + + /// Generate search index only + Index { + /// Documentation directory + #[arg(default_value = "target/doc")] + dir: PathBuf, + }, +} + +fn main() -> anyhow::Result<()> { + // Initialize logging + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::from_default_env() + .add_directive(tracing::Level::INFO.into()), + ) + .init(); + + let cli = Cli::parse(); + + match cli.command { + Commands::Build { + source, + output, + open, + document_private, + include_deps, + } => { + // TODO: Phase 8 implementation + // - [ ] Parse source files + // - [ ] Extract documentation + // - [ ] Generate HTML + // - [ ] Build search index + // - [ ] Open browser if requested + + println!("Building documentation from {:?} to {:?}", source, output); + let _ = (document_private, include_deps, open); + } + + #[cfg(feature = "serve")] + Commands::Serve { dir, port } => { + // TODO: Phase 8 implementation + // - [ ] Start HTTP server + // - [ ] Serve static files + // - [ ] Handle search API + + println!("Serving documentation from {:?} on port {}", dir, port); + } + + Commands::Index { dir } => { + // TODO: Phase 8 implementation + // - [ ] Scan HTML files + // - [ ] Extract content + // - [ ] Build Tantivy index + + println!("Building search index for {:?}", dir); + } + } + + Ok(()) +} + +// TODO: Phase 8 implementation +// - [ ] Parse AffineScript source and extract types/functions +// - [ ] Process doc comments (Markdown) +// - [ ] Generate HTML with templates +// - [ ] Create type/effect cross-references +// - [ ] Build searchable index +// - [ ] Support theme customization +// - [ ] Add source view with syntax highlighting diff --git a/tools/affine-doc/src/render.rs b/tools/affine-doc/src/render.rs new file mode 100644 index 0000000..f0b058c --- /dev/null +++ b/tools/affine-doc/src/render.rs @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright 2024 AffineScript Contributors + +//! Markdown Rendering +//! +//! Renders Markdown doc comments to HTML. + +use pulldown_cmark::{html, Options, Parser}; + +/// Render Markdown to HTML +pub fn render_markdown(markdown: &str) -> String { + let options = Options::all(); + let parser = Parser::new_ext(markdown, options); + + let mut html_output = String::new(); + html::push_html(&mut html_output, parser); + + html_output +} + +/// Render code with syntax highlighting +pub fn render_code(code: &str, language: Option<&str>) -> String { + // TODO: Phase 8 implementation + // - [ ] Use syntect for highlighting + // - [ ] Support AffineScript syntax + // - [ ] Add line numbers option + + let lang = language.unwrap_or("affinescript"); + format!( + r#"
{}
"#, + lang, + html_escape(code) + ) +} + +/// Escape HTML entities +pub fn html_escape(s: &str) -> String { + s.replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +} + +/// Render a type signature with links +pub fn render_signature(signature: &str, _link_resolver: &dyn Fn(&str) -> Option) -> String { + // TODO: Phase 8 implementation + // - [ ] Parse signature + // - [ ] Identify type names + // - [ ] Create links to type documentation + // - [ ] Apply syntax highlighting + + format!( + r#"{}"#, + html_escape(signature) + ) +} + +/// Render effect row +pub fn render_effects(effects: &[String]) -> String { + if effects.is_empty() { + return String::new(); + } + + let effect_links: Vec = effects + .iter() + .map(|e| format!(r#"{}"#, e, e)) + .collect(); + + format!(r#"/ {}"#, effect_links.join(" | ")) +} + +/// Render quantity annotation +pub fn render_quantity(quantity: &str) -> String { + let (class, display) = match quantity { + "0" => ("quantity-erased", "0"), + "1" => ("quantity-linear", "1"), + "ω" | "w" | "omega" => ("quantity-unrestricted", "ω"), + _ => ("quantity-unknown", quantity), + }; + + format!(r#"{}"#, class, display) +} + +/// Render deprecation notice +pub fn render_deprecated(message: Option<&str>) -> String { + match message { + Some(msg) => format!( + r#"
Deprecated: {}
"#, + html_escape(msg) + ), + None => r#"
Deprecated
"#.to_string(), + } +} + +/// Render stability badge +pub fn render_stability(stability: Stability) -> String { + let (class, label) = match stability { + Stability::Stable => ("stability-stable", "Stable"), + Stability::Unstable => ("stability-unstable", "Unstable"), + Stability::Experimental => ("stability-experimental", "Experimental"), + Stability::Deprecated => ("stability-deprecated", "Deprecated"), + }; + + format!(r#"{}"#, class, label) +} + +/// Stability level +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Stability { + Stable, + Unstable, + Experimental, + Deprecated, +} + +// TODO: Phase 8 implementation +// - [ ] Add syntax highlighting for AffineScript +// - [ ] Implement cross-reference resolution +// - [ ] Add heading anchor links +// - [ ] Support custom markdown extensions +// - [ ] Add copy button for code blocks diff --git a/tools/affine-pkg/Cargo.toml b/tools/affine-pkg/Cargo.toml new file mode 100644 index 0000000..04d250d --- /dev/null +++ b/tools/affine-pkg/Cargo.toml @@ -0,0 +1,65 @@ +# SPDX-License-Identifier: Apache-2.0 OR MIT +# Copyright 2024 AffineScript Contributors + +[package] +name = "affine-pkg" +version = "0.1.0" +edition = "2021" +description = "Package manager for AffineScript" +license = "Apache-2.0 OR MIT" +repository = "https://github.com/affinescript/affinescript" +keywords = ["package-manager", "affinescript", "build"] +categories = ["development-tools", "command-line-utilities"] + +[dependencies] +# CLI +clap = { version = "4", features = ["derive", "env"] } + +# Async runtime +tokio = { version = "1", features = ["full"] } + +# HTTP client for registry +reqwest = { version = "0.11", features = ["json", "rustls-tls"], default-features = false } + +# Serialization +serde = { version = "1", features = ["derive"] } +toml = "0.8" +serde_json = "1" + +# Hashing (content-addressed storage) +sha2 = "0.10" +hex = "0.4" + +# Archive handling +tar = "0.4" +flate2 = "1" +zip = "0.6" + +# Path handling +camino = "1" +dirs = "5" + +# Logging +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +# Error handling +thiserror = "1" +anyhow = "1" + +# Semver +semver = { version = "1", features = ["serde"] } + +# Lockfile +toml_edit = "0.21" + +# Progress bars +indicatif = "0.17" + +[dev-dependencies] +tempfile = "3" +tokio-test = "0.4" + +[[bin]] +name = "affine" +path = "src/main.rs" diff --git a/tools/affine-pkg/src/build.rs b/tools/affine-pkg/src/build.rs new file mode 100644 index 0000000..d8da1a5 --- /dev/null +++ b/tools/affine-pkg/src/build.rs @@ -0,0 +1,235 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright 2024 AffineScript Contributors + +//! Build System +//! +//! Orchestrates compilation of AffineScript packages. + +use std::path::{Path, PathBuf}; +use std::process::Command; + +/// Build configuration +#[derive(Debug, Clone)] +pub struct BuildConfig { + /// Release or debug mode + pub release: bool, + + /// Target directory + pub target_dir: PathBuf, + + /// Number of parallel jobs + pub jobs: Option, + + /// Extra compiler flags + pub flags: Vec, + + /// Features to enable + pub features: Vec, + + /// Disable default features + pub no_default_features: bool, +} + +impl Default for BuildConfig { + fn default() -> Self { + BuildConfig { + release: false, + target_dir: PathBuf::from("target"), + jobs: None, + flags: Vec::new(), + features: Vec::new(), + no_default_features: false, + } + } +} + +/// Build result +#[derive(Debug)] +pub struct BuildResult { + /// Output artifacts + pub artifacts: Vec, + + /// Compilation time in milliseconds + pub duration_ms: u64, + + /// Compiler warnings + pub warnings: Vec, +} + +/// Build artifact +#[derive(Debug)] +pub struct Artifact { + /// Artifact kind + pub kind: ArtifactKind, + + /// Output path + pub path: PathBuf, + + /// Package name + pub package: String, + + /// Size in bytes + pub size: u64, +} + +/// Artifact kind +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ArtifactKind { + /// WebAssembly binary + Wasm, + /// WebAssembly + JavaScript wrapper + WasmJs, + /// Library + Library, + /// Documentation + Doc, +} + +/// Build orchestrator +pub struct Builder { + config: BuildConfig, +} + +impl Builder { + /// Create a new builder + pub fn new(config: BuildConfig) -> Self { + Builder { config } + } + + /// Build a package + pub fn build(&self, package_dir: impl AsRef) -> anyhow::Result { + let _package_dir = package_dir.as_ref(); + let start = std::time::Instant::now(); + + // TODO: Phase 8 implementation + // - [ ] Load manifest + // - [ ] Resolve dependencies + // - [ ] Build dependencies first + // - [ ] Compile source files + // - [ ] Link into output + + let duration_ms = start.elapsed().as_millis() as u64; + + Ok(BuildResult { + artifacts: vec![], + duration_ms, + warnings: vec![], + }) + } + + /// Check (type check without code generation) + pub fn check(&self, package_dir: impl AsRef) -> anyhow::Result> { + let _package_dir = package_dir.as_ref(); + + // TODO: Phase 8 implementation + // - [ ] Load manifest + // - [ ] Parse all source files + // - [ ] Type check + // - [ ] Borrow check + // - [ ] Effect check + // - [ ] Return diagnostics + + Ok(vec![]) + } + + /// Run tests + pub fn test( + &self, + package_dir: impl AsRef, + filter: Option<&str>, + ) -> anyhow::Result { + let _package_dir = package_dir.as_ref(); + let _filter = filter; + + // TODO: Phase 8 implementation + // - [ ] Find test functions (annotated with #[test]) + // - [ ] Build test binary + // - [ ] Run tests + // - [ ] Collect results + + Ok(TestResult { + passed: 0, + failed: 0, + skipped: 0, + duration_ms: 0, + failures: vec![], + }) + } + + /// Generate documentation + pub fn doc(&self, package_dir: impl AsRef) -> anyhow::Result { + let package_dir = package_dir.as_ref(); + let output_dir = self.config.target_dir.join("doc"); + + // TODO: Phase 8 implementation + // - [ ] Parse source files + // - [ ] Extract doc comments + // - [ ] Generate HTML + + let _ = package_dir; + Ok(output_dir) + } + + /// Clean build artifacts + pub fn clean(&self, package_dir: impl AsRef) -> anyhow::Result<()> { + let target_dir = package_dir.as_ref().join(&self.config.target_dir); + if target_dir.exists() { + std::fs::remove_dir_all(target_dir)?; + } + Ok(()) + } + + /// Run the AffineScript compiler + fn run_compiler(&self, args: &[&str]) -> anyhow::Result { + let output = Command::new("affinescript") + .args(args) + .output()?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + anyhow::bail!("Compiler failed: {}", stderr); + } + + Ok(output) + } +} + +/// Test result +#[derive(Debug)] +pub struct TestResult { + /// Number of passing tests + pub passed: usize, + + /// Number of failing tests + pub failed: usize, + + /// Number of skipped tests + pub skipped: usize, + + /// Total duration in milliseconds + pub duration_ms: u64, + + /// Details of failures + pub failures: Vec, +} + +/// Test failure details +#[derive(Debug)] +pub struct TestFailure { + /// Test name + pub name: String, + + /// Failure message + pub message: String, + + /// Source location + pub location: Option, +} + +// TODO: Phase 8 implementation +// - [ ] Implement incremental compilation +// - [ ] Add dependency tracking +// - [ ] Add parallel compilation +// - [ ] Add build caching +// - [ ] Add build scripts support +// - [ ] Add custom compiler invocation diff --git a/tools/affine-pkg/src/config.rs b/tools/affine-pkg/src/config.rs new file mode 100644 index 0000000..95cf19c --- /dev/null +++ b/tools/affine-pkg/src/config.rs @@ -0,0 +1,235 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright 2024 AffineScript Contributors + +//! Configuration +//! +//! User and project configuration for the package manager. + +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; + +/// Global configuration file location +pub fn global_config_path() -> Option { + dirs::config_dir().map(|d| d.join("affine").join("config.toml")) +} + +/// Credentials file location +pub fn credentials_path() -> Option { + dirs::config_dir().map(|d| d.join("affine").join("credentials.toml")) +} + +/// Global configuration +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Config { + /// Registry configuration + #[serde(default)] + pub registries: Vec, + + /// Network configuration + #[serde(default)] + pub net: NetworkConfig, + + /// Build configuration + #[serde(default)] + pub build: BuildConfig, + + /// Environment variables + #[serde(default)] + pub env: std::collections::HashMap, +} + +/// Registry configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RegistryConfig { + /// Registry name + pub name: String, + + /// Registry URL + pub url: String, + + /// Whether this is the default registry + #[serde(default)] + pub default: bool, +} + +/// Network configuration +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct NetworkConfig { + /// HTTP proxy + pub proxy: Option, + + /// HTTPS proxy + pub https_proxy: Option, + + /// No proxy hosts + #[serde(default)] + pub no_proxy: Vec, + + /// Connection timeout in seconds + #[serde(default = "default_timeout")] + pub timeout: u64, + + /// Number of retries + #[serde(default = "default_retries")] + pub retries: u32, + + /// Whether to verify SSL certificates + #[serde(default = "default_true")] + pub verify_ssl: bool, +} + +fn default_timeout() -> u64 { + 30 +} + +fn default_retries() -> u32 { + 3 +} + +fn default_true() -> bool { + true +} + +/// Build configuration +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct BuildConfig { + /// Number of parallel jobs + pub jobs: Option, + + /// Target directory + pub target_dir: Option, + + /// Whether to use incremental compilation + #[serde(default = "default_true")] + pub incremental: bool, + + /// Compiler flags + #[serde(default)] + pub flags: Vec, +} + +/// Credentials +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Credentials { + /// Token by registry name + pub tokens: std::collections::HashMap, +} + +impl Config { + /// Load global configuration + pub fn load_global() -> anyhow::Result { + match global_config_path() { + Some(path) if path.exists() => { + let content = std::fs::read_to_string(&path)?; + Ok(toml::from_str(&content)?) + } + _ => Ok(Self::default()), + } + } + + /// Save global configuration + pub fn save_global(&self) -> anyhow::Result<()> { + if let Some(path) = global_config_path() { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + let content = toml::to_string_pretty(self)?; + std::fs::write(path, content)?; + } + Ok(()) + } + + /// Load project-local configuration + pub fn load_local(project_root: impl AsRef) -> anyhow::Result> { + let path = project_root.as_ref().join(".affine").join("config.toml"); + if path.exists() { + let content = std::fs::read_to_string(&path)?; + Ok(Some(toml::from_str(&content)?)) + } else { + Ok(None) + } + } + + /// Merge with another config (other takes precedence) + pub fn merge(&mut self, other: Self) { + if !other.registries.is_empty() { + self.registries = other.registries; + } + // Merge network config + if other.net.proxy.is_some() { + self.net.proxy = other.net.proxy; + } + if other.net.https_proxy.is_some() { + self.net.https_proxy = other.net.https_proxy; + } + // Merge build config + if other.build.jobs.is_some() { + self.build.jobs = other.build.jobs; + } + if other.build.target_dir.is_some() { + self.build.target_dir = other.build.target_dir; + } + // Merge env + self.env.extend(other.env); + } + + /// Get the default registry URL + pub fn default_registry(&self) -> String { + self.registries + .iter() + .find(|r| r.default) + .map(|r| r.url.clone()) + .unwrap_or_else(|| crate::registry::DEFAULT_REGISTRY.to_string()) + } +} + +impl Credentials { + /// Load credentials + pub fn load() -> anyhow::Result { + match credentials_path() { + Some(path) if path.exists() => { + let content = std::fs::read_to_string(&path)?; + Ok(toml::from_str(&content)?) + } + _ => Ok(Self::default()), + } + } + + /// Save credentials + pub fn save(&self) -> anyhow::Result<()> { + if let Some(path) = credentials_path() { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + // Set restrictive permissions on credentials file + let content = toml::to_string_pretty(self)?; + std::fs::write(&path, content)?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = std::fs::metadata(&path)?.permissions(); + perms.set_mode(0o600); + std::fs::set_permissions(&path, perms)?; + } + } + Ok(()) + } + + /// Get token for a registry + pub fn get_token(&self, registry: &str) -> Option<&str> { + self.tokens.get(registry).map(|s| s.as_str()) + } + + /// Set token for a registry + pub fn set_token(&mut self, registry: String, token: String) { + self.tokens.insert(registry, token); + } +} + +// TODO: Phase 8 implementation +// - [ ] Add environment variable overrides +// - [ ] Add configuration validation +// - [ ] Add shell completion config +// - [ ] Add alias support +// - [ ] Add profiles (dev, release, etc.) diff --git a/tools/affine-pkg/src/lockfile.rs b/tools/affine-pkg/src/lockfile.rs new file mode 100644 index 0000000..b962993 --- /dev/null +++ b/tools/affine-pkg/src/lockfile.rs @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright 2024 AffineScript Contributors + +//! Lock File (affine.lock) +//! +//! Records the exact versions of all dependencies for reproducibility. + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::Path; + +/// Lock file format +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Lockfile { + /// Lock file version + pub version: u32, + + /// Locked packages + pub packages: Vec, + + /// Metadata (checksums, etc.) + #[serde(default)] + pub metadata: HashMap, +} + +/// A locked package +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LockedPackage { + /// Package name + pub name: String, + + /// Exact version + pub version: String, + + /// Source (registry, git, path) + pub source: PackageSource, + + /// Content hash (SHA-256) + pub checksum: Option, + + /// Dependencies of this package + #[serde(default)] + pub dependencies: Vec, +} + +/// Package source +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum PackageSource { + /// From registry + #[serde(rename = "registry")] + Registry { + /// Registry URL + url: String, + }, + + /// From git + #[serde(rename = "git")] + Git { + /// Repository URL + url: String, + /// Commit hash + commit: String, + }, + + /// Local path + #[serde(rename = "path")] + Path { + /// Relative path + path: String, + }, +} + +/// A locked dependency reference +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LockedDependency { + /// Package name + pub name: String, + + /// Version + pub version: String, + + /// Source identifier + pub source: Option, +} + +impl Lockfile { + /// Current lock file version + pub const VERSION: u32 = 1; + + /// Create empty lockfile + pub fn new() -> Self { + Lockfile { + version: Self::VERSION, + packages: Vec::new(), + metadata: HashMap::new(), + } + } + + /// Load lockfile from path + pub fn load(path: impl AsRef) -> anyhow::Result { + let content = std::fs::read_to_string(path)?; + let lockfile: Lockfile = toml::from_str(&content)?; + Ok(lockfile) + } + + /// Save lockfile to path + pub fn save(&self, path: impl AsRef) -> anyhow::Result<()> { + let content = toml::to_string_pretty(self)?; + std::fs::write(path, content)?; + Ok(()) + } + + /// Find a locked package by name and version + pub fn find(&self, name: &str, version: &str) -> Option<&LockedPackage> { + self.packages + .iter() + .find(|p| p.name == name && p.version == version) + } + + /// Add or update a package + pub fn insert(&mut self, package: LockedPackage) { + // Remove existing entry if present + self.packages + .retain(|p| !(p.name == package.name && p.version == package.version)); + + self.packages.push(package); + } + + /// Sort packages for deterministic output + pub fn sort(&mut self) { + self.packages.sort_by(|a, b| { + a.name.cmp(&b.name).then_with(|| a.version.cmp(&b.version)) + }); + } +} + +impl Default for Lockfile { + fn default() -> Self { + Self::new() + } +} + +// TODO: Phase 8 implementation +// - [ ] Add checksum verification +// - [ ] Add lockfile merging (for conflicts) +// - [ ] Add lockfile diffing +// - [ ] Add source canonicalization diff --git a/tools/affine-pkg/src/main.rs b/tools/affine-pkg/src/main.rs new file mode 100644 index 0000000..a0a038d --- /dev/null +++ b/tools/affine-pkg/src/main.rs @@ -0,0 +1,323 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright 2024 AffineScript Contributors + +//! AffineScript Package Manager +//! +//! A workspace-aware, Cargo-inspired package manager for AffineScript. +//! +//! # Features +//! +//! - Dependency resolution with version constraints +//! - Workspace support for monorepos +//! - Content-addressed storage (like pnpm) +//! - Lock file for reproducibility +//! - Build script support + +use clap::{Parser, Subcommand}; + +mod build; +mod config; +mod lockfile; +mod manifest; +mod registry; +mod resolve; +mod storage; +mod workspace; + +#[derive(Parser)] +#[command(name = "affine")] +#[command(about = "AffineScript package manager", long_about = None)] +#[command(version)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Create a new package + New { + /// Package name + name: String, + /// Create a library instead of a binary + #[arg(long)] + lib: bool, + }, + + /// Initialize a package in the current directory + Init { + /// Package name (defaults to directory name) + #[arg(long)] + name: Option, + /// Create a library instead of a binary + #[arg(long)] + lib: bool, + }, + + /// Build the current package + Build { + /// Build in release mode + #[arg(long)] + release: bool, + /// Build only the specified package + #[arg(short, long)] + package: Option, + }, + + /// Check the current package for errors + Check { + /// Check only the specified package + #[arg(short, long)] + package: Option, + }, + + /// Run the current package + Run { + /// Run in release mode + #[arg(long)] + release: bool, + /// Arguments to pass to the program + #[arg(trailing_var_arg = true)] + args: Vec, + }, + + /// Run tests + Test { + /// Test name filter + filter: Option, + /// Run in release mode + #[arg(long)] + release: bool, + }, + + /// Add a dependency + Add { + /// Dependency to add (name or name@version) + dependency: String, + /// Add as dev dependency + #[arg(long)] + dev: bool, + /// Add as build dependency + #[arg(long)] + build: bool, + }, + + /// Remove a dependency + Remove { + /// Dependency to remove + dependency: String, + }, + + /// Update dependencies + Update { + /// Package to update (updates all if not specified) + package: Option, + }, + + /// Install dependencies + Install, + + /// Publish package to registry + Publish { + /// Don't actually publish, just verify + #[arg(long)] + dry_run: bool, + }, + + /// Search for packages + Search { + /// Search query + query: String, + }, + + /// Show package information + Info { + /// Package name + package: String, + }, + + /// Clean build artifacts + Clean, + + /// Format source code + Fmt { + /// Check formatting without changing files + #[arg(long)] + check: bool, + }, + + /// Run linter + Lint { + /// Auto-fix issues where possible + #[arg(long)] + fix: bool, + }, + + /// Generate documentation + Doc { + /// Open documentation in browser + #[arg(long)] + open: bool, + }, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Initialize logging + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::from_default_env() + .add_directive(tracing::Level::INFO.into()), + ) + .init(); + + let cli = Cli::parse(); + + match cli.command { + Commands::New { name, lib } => { + // TODO: Phase 8 implementation + // - [ ] Create directory structure + // - [ ] Generate affine.toml + // - [ ] Create src/main.afs or src/lib.afs + println!("Creating new {} package: {}", if lib { "library" } else { "binary" }, name); + } + + Commands::Init { name, lib } => { + // TODO: Phase 8 implementation + // - [ ] Generate affine.toml in current directory + // - [ ] Create src/ directory + let _ = (name, lib); + println!("Initializing package in current directory"); + } + + Commands::Build { release, package } => { + // TODO: Phase 8 implementation + // - [ ] Load manifest + // - [ ] Resolve dependencies + // - [ ] Compile all packages + let _ = (release, package); + println!("Building..."); + } + + Commands::Check { package } => { + // TODO: Phase 8 implementation + // - [ ] Type check without codegen + let _ = package; + println!("Checking..."); + } + + Commands::Run { release, args } => { + // TODO: Phase 8 implementation + // - [ ] Build if needed + // - [ ] Run the binary + let _ = (release, args); + println!("Running..."); + } + + Commands::Test { filter, release } => { + // TODO: Phase 8 implementation + // - [ ] Find test functions + // - [ ] Build test binary + // - [ ] Run tests + let _ = (filter, release); + println!("Testing..."); + } + + Commands::Add { dependency, dev, build } => { + // TODO: Phase 8 implementation + // - [ ] Parse dependency spec + // - [ ] Resolve version + // - [ ] Update manifest + // - [ ] Update lockfile + let _ = (dev, build); + println!("Adding dependency: {}", dependency); + } + + Commands::Remove { dependency } => { + // TODO: Phase 8 implementation + // - [ ] Remove from manifest + // - [ ] Update lockfile + println!("Removing dependency: {}", dependency); + } + + Commands::Update { package } => { + // TODO: Phase 8 implementation + // - [ ] Resolve latest compatible versions + // - [ ] Update lockfile + let _ = package; + println!("Updating dependencies..."); + } + + Commands::Install => { + // TODO: Phase 8 implementation + // - [ ] Read lockfile + // - [ ] Download missing packages + // - [ ] Link to content store + println!("Installing dependencies..."); + } + + Commands::Publish { dry_run } => { + // TODO: Phase 8 implementation + // - [ ] Verify package + // - [ ] Build tarball + // - [ ] Upload to registry + let _ = dry_run; + println!("Publishing..."); + } + + Commands::Search { query } => { + // TODO: Phase 8 implementation + // - [ ] Query registry API + // - [ ] Display results + println!("Searching for: {}", query); + } + + Commands::Info { package } => { + // TODO: Phase 8 implementation + // - [ ] Fetch package info + // - [ ] Display metadata + println!("Package info: {}", package); + } + + Commands::Clean => { + // TODO: Phase 8 implementation + // - [ ] Remove target/ directory + println!("Cleaning..."); + } + + Commands::Fmt { check } => { + // TODO: Phase 8 implementation + // - [ ] Run formatter + let _ = check; + println!("Formatting..."); + } + + Commands::Lint { fix } => { + // TODO: Phase 8 implementation + // - [ ] Run linter + let _ = fix; + println!("Linting..."); + } + + Commands::Doc { open } => { + // TODO: Phase 8 implementation + // - [ ] Generate docs + // - [ ] Open in browser if requested + let _ = open; + println!("Generating documentation..."); + } + } + + Ok(()) +} + +// TODO: Phase 8 implementation +// - [ ] Implement manifest parsing (affine.toml) +// - [ ] Implement dependency resolution (SAT solver or PubGrub) +// - [ ] Implement content-addressed storage +// - [ ] Implement lockfile format +// - [ ] Implement registry client +// - [ ] Implement build orchestration +// - [ ] Add workspace support +// - [ ] Add feature flags +// - [ ] Add build scripts diff --git a/tools/affine-pkg/src/manifest.rs b/tools/affine-pkg/src/manifest.rs new file mode 100644 index 0000000..ee2b9c8 --- /dev/null +++ b/tools/affine-pkg/src/manifest.rs @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright 2024 AffineScript Contributors + +//! Package Manifest (affine.toml) +//! +//! Defines the package metadata and dependencies. + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::Path; + +/// Package manifest +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Manifest { + /// Package information + pub package: Package, + + /// Dependencies + #[serde(default)] + pub dependencies: HashMap, + + /// Dev dependencies + #[serde(default, rename = "dev-dependencies")] + pub dev_dependencies: HashMap, + + /// Build dependencies + #[serde(default, rename = "build-dependencies")] + pub build_dependencies: HashMap, + + /// Feature flags + #[serde(default)] + pub features: HashMap>, + + /// Binary targets + #[serde(default, rename = "bin")] + pub binaries: Vec, + + /// Library target + pub lib: Option, + + /// Workspace configuration + pub workspace: Option, +} + +/// Package metadata +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Package { + /// Package name + pub name: String, + + /// Package version + pub version: String, + + /// AffineScript edition + #[serde(default = "default_edition")] + pub edition: String, + + /// Package authors + #[serde(default)] + pub authors: Vec, + + /// Package description + pub description: Option, + + /// Documentation URL + pub documentation: Option, + + /// Homepage URL + pub homepage: Option, + + /// Repository URL + pub repository: Option, + + /// License identifier (SPDX) + pub license: Option, + + /// License file path + #[serde(rename = "license-file")] + pub license_file: Option, + + /// Keywords for search + #[serde(default)] + pub keywords: Vec, + + /// Categories for classification + #[serde(default)] + pub categories: Vec, + + /// Build script path + pub build: Option, +} + +fn default_edition() -> String { + "2024".to_string() +} + +/// Dependency specification +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum Dependency { + /// Simple version string + Version(String), + + /// Detailed dependency specification + Detailed(DependencyDetail), +} + +/// Detailed dependency specification +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DependencyDetail { + /// Version requirement + pub version: Option, + + /// Git repository URL + pub git: Option, + + /// Git branch + pub branch: Option, + + /// Git tag + pub tag: Option, + + /// Git commit + pub rev: Option, + + /// Local path + pub path: Option, + + /// Package name in registry (if different) + pub package: Option, + + /// Required features + #[serde(default)] + pub features: Vec, + + /// Whether this is optional + #[serde(default)] + pub optional: bool, + + /// Default features + #[serde(default = "default_true", rename = "default-features")] + pub default_features: bool, + + /// Registry URL + pub registry: Option, +} + +fn default_true() -> bool { + true +} + +/// Binary target +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BinaryTarget { + /// Binary name + pub name: String, + + /// Source file path + pub path: Option, + + /// Required features + #[serde(default)] + #[serde(rename = "required-features")] + pub required_features: Vec, +} + +/// Library target +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LibraryTarget { + /// Library name + pub name: Option, + + /// Source file path + pub path: Option, + + /// Crate types to generate + #[serde(default, rename = "crate-type")] + pub crate_type: Vec, +} + +/// Workspace configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Workspace { + /// Member packages + pub members: Vec, + + /// Excluded paths + #[serde(default)] + pub exclude: Vec, + + /// Shared dependencies + pub dependencies: Option>, +} + +impl Manifest { + /// Load manifest from file + pub fn load(path: impl AsRef) -> anyhow::Result { + let content = std::fs::read_to_string(path)?; + let manifest: Manifest = toml::from_str(&content)?; + Ok(manifest) + } + + /// Save manifest to file + pub fn save(&self, path: impl AsRef) -> anyhow::Result<()> { + let content = toml::to_string_pretty(self)?; + std::fs::write(path, content)?; + Ok(()) + } + + /// Get all dependencies (including dev and build) + pub fn all_dependencies(&self) -> impl Iterator { + self.dependencies + .iter() + .chain(self.dev_dependencies.iter()) + .chain(self.build_dependencies.iter()) + } +} + +// TODO: Phase 8 implementation +// - [ ] Add manifest validation +// - [ ] Add version validation (semver) +// - [ ] Add license validation (SPDX) +// - [ ] Add workspace resolution +// - [ ] Add feature resolution diff --git a/tools/affine-pkg/src/registry.rs b/tools/affine-pkg/src/registry.rs new file mode 100644 index 0000000..06462c3 --- /dev/null +++ b/tools/affine-pkg/src/registry.rs @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright 2024 AffineScript Contributors + +//! Package Registry Client +//! +//! Communicates with the AffineScript package registry. + +use semver::Version; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Default registry URL +pub const DEFAULT_REGISTRY: &str = "https://packages.affinescript.dev"; + +/// Registry client +pub struct RegistryClient { + /// HTTP client + client: reqwest::Client, + + /// Registry base URL + base_url: String, + + /// Authentication token + token: Option, +} + +/// Package search result +#[derive(Debug, Clone, Deserialize)] +pub struct SearchResult { + /// Total number of matches + pub total: usize, + + /// Matching packages + pub packages: Vec, +} + +/// Package summary (from search) +#[derive(Debug, Clone, Deserialize)] +pub struct PackageSummary { + /// Package name + pub name: String, + + /// Latest version + pub version: String, + + /// Description + pub description: Option, + + /// Download count + pub downloads: u64, +} + +/// Full package info +#[derive(Debug, Clone, Deserialize)] +pub struct PackageInfo { + /// Package name + pub name: String, + + /// All published versions + pub versions: Vec, + + /// Keywords + pub keywords: Vec, + + /// Categories + pub categories: Vec, + + /// Repository URL + pub repository: Option, + + /// Documentation URL + pub documentation: Option, + + /// Homepage URL + pub homepage: Option, +} + +/// Version info +#[derive(Debug, Clone, Deserialize)] +pub struct VersionInfo { + /// Version number + pub version: String, + + /// Publish date + pub published_at: String, + + /// Download URL + pub download_url: String, + + /// SHA-256 checksum + pub checksum: String, + + /// Dependencies + pub dependencies: HashMap, + + /// Features + pub features: HashMap>, + + /// Whether this version is yanked + pub yanked: bool, +} + +/// Publish request +#[derive(Debug, Clone, Serialize)] +pub struct PublishRequest { + /// Package name + pub name: String, + + /// Version + pub version: String, + + /// Tarball contents (base64) + pub tarball: String, + + /// SHA-256 of tarball + pub checksum: String, +} + +/// Registry error +#[derive(Debug, thiserror::Error)] +pub enum RegistryError { + #[error("HTTP error: {0}")] + Http(#[from] reqwest::Error), + + #[error("Package not found: {0}")] + NotFound(String), + + #[error("Authentication required")] + Unauthorized, + + #[error("Rate limited, retry after {0} seconds")] + RateLimited(u64), + + #[error("Registry error: {0}")] + RegistryError(String), +} + +impl RegistryClient { + /// Create a new registry client + pub fn new(base_url: Option) -> Self { + RegistryClient { + client: reqwest::Client::new(), + base_url: base_url.unwrap_or_else(|| DEFAULT_REGISTRY.to_string()), + token: None, + } + } + + /// Set authentication token + pub fn with_token(mut self, token: String) -> Self { + self.token = Some(token); + self + } + + /// Search for packages + pub async fn search(&self, query: &str, page: u32) -> Result { + // TODO: Phase 8 implementation + // GET /api/v1/search?q={query}&page={page} + let _ = (query, page); + Ok(SearchResult { + total: 0, + packages: vec![], + }) + } + + /// Get package info + pub async fn get_package(&self, name: &str) -> Result { + // TODO: Phase 8 implementation + // GET /api/v1/packages/{name} + let _ = name; + Err(RegistryError::NotFound(name.to_string())) + } + + /// Get specific version info + pub async fn get_version( + &self, + name: &str, + version: &Version, + ) -> Result { + // TODO: Phase 8 implementation + // GET /api/v1/packages/{name}/{version} + let _ = (name, version); + Err(RegistryError::NotFound(format!("{}@{}", name, version))) + } + + /// Download package tarball + pub async fn download(&self, url: &str) -> Result { + // TODO: Phase 8 implementation + // GET {download_url} + let _ = url; + Ok(bytes::Bytes::new()) + } + + /// Publish a package + pub async fn publish(&self, request: PublishRequest) -> Result<(), RegistryError> { + // TODO: Phase 8 implementation + // PUT /api/v1/packages/{name} + // Authorization: Bearer {token} + let _ = request; + + if self.token.is_none() { + return Err(RegistryError::Unauthorized); + } + + Ok(()) + } + + /// Yank a version + pub async fn yank(&self, name: &str, version: &Version) -> Result<(), RegistryError> { + // TODO: Phase 8 implementation + // DELETE /api/v1/packages/{name}/{version} + let _ = (name, version); + + if self.token.is_none() { + return Err(RegistryError::Unauthorized); + } + + Ok(()) + } + + /// Unyank a version + pub async fn unyank(&self, name: &str, version: &Version) -> Result<(), RegistryError> { + // TODO: Phase 8 implementation + // PUT /api/v1/packages/{name}/{version}/unyank + let _ = (name, version); + + if self.token.is_none() { + return Err(RegistryError::Unauthorized); + } + + Ok(()) + } + + /// Get owners of a package + pub async fn get_owners(&self, name: &str) -> Result, RegistryError> { + // TODO: Phase 8 implementation + // GET /api/v1/packages/{name}/owners + let _ = name; + Ok(vec![]) + } + + /// Add an owner + pub async fn add_owner(&self, name: &str, user: &str) -> Result<(), RegistryError> { + // TODO: Phase 8 implementation + // PUT /api/v1/packages/{name}/owners + let _ = (name, user); + + if self.token.is_none() { + return Err(RegistryError::Unauthorized); + } + + Ok(()) + } +} + +// TODO: Phase 8 implementation +// - [ ] Implement actual HTTP requests +// - [ ] Add retry logic with backoff +// - [ ] Add caching with ETags +// - [ ] Add offline mode with cached index +// - [ ] Add sparse index support +// - [ ] Add parallel downloads diff --git a/tools/affine-pkg/src/resolve.rs b/tools/affine-pkg/src/resolve.rs new file mode 100644 index 0000000..ed24393 --- /dev/null +++ b/tools/affine-pkg/src/resolve.rs @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright 2024 AffineScript Contributors + +//! Dependency Resolution +//! +//! Resolves version constraints to concrete versions. +//! Uses PubGrub algorithm for efficient conflict detection. + +use crate::manifest::Dependency; +use semver::{Version, VersionReq}; +use std::collections::{HashMap, HashSet}; + +/// A resolved dependency graph +#[derive(Debug, Clone)] +pub struct ResolvedGraph { + /// Packages in topological order + pub packages: Vec, + + /// Dependency edges + pub edges: HashMap>, +} + +/// A resolved package +#[derive(Debug, Clone)] +pub struct ResolvedPackage { + /// Package identifier + pub id: PackageId, + + /// Features enabled + pub features: HashSet, + + /// Source URL/path + pub source: String, +} + +/// Package identifier (name + version) +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PackageId { + /// Package name + pub name: String, + + /// Exact version + pub version: Version, +} + +impl std::fmt::Display for PackageId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}@{}", self.name, self.version) + } +} + +/// Resolution error +#[derive(Debug, thiserror::Error)] +pub enum ResolveError { + /// Version conflict + #[error("version conflict for {package}: {required} conflicts with {available}")] + Conflict { + package: String, + required: String, + available: String, + }, + + /// Package not found + #[error("package not found: {0}")] + NotFound(String), + + /// Cyclic dependency + #[error("cyclic dependency detected: {0}")] + Cycle(String), + + /// Feature not found + #[error("feature {feature} not found in package {package}")] + FeatureNotFound { package: String, feature: String }, +} + +/// Resolver state +pub struct Resolver { + /// Registry client + // registry: RegistryClient, + + /// Cache of available versions + version_cache: HashMap>, + + /// Cache of package metadata + metadata_cache: HashMap, +} + +/// Package metadata from registry +#[derive(Debug, Clone)] +pub struct PackageMetadata { + /// Package name + pub name: String, + + /// Version + pub version: Version, + + /// Dependencies + pub dependencies: HashMap, + + /// Features + pub features: HashMap>, + + /// Default features + pub default_features: Vec, +} + +impl Resolver { + /// Create a new resolver + pub fn new() -> Self { + Resolver { + version_cache: HashMap::new(), + metadata_cache: HashMap::new(), + } + } + + /// Resolve dependencies + pub fn resolve( + &mut self, + _root_deps: &HashMap, + ) -> Result { + // TODO: Phase 8 implementation using PubGrub algorithm + // Reference: https://nex3.medium.com/pubgrub-2fb6470504f + // + // 1. Start with root package requirements + // 2. For each unsatisfied requirement: + // a. Find compatible versions + // b. Pick best version (highest satisfying) + // c. Add package's dependencies to requirements + // 3. If conflict detected: + // a. Analyze conflict + // b. Derive resolution (incompatibility) + // c. Backtrack and try alternative + // 4. Continue until all satisfied or proven unsatisfiable + + Ok(ResolvedGraph { + packages: Vec::new(), + edges: HashMap::new(), + }) + } + + /// Check if a version satisfies a requirement + fn satisfies(&self, version: &Version, req: &VersionReq) -> bool { + req.matches(version) + } + + /// Get available versions for a package + fn get_versions(&mut self, _name: &str) -> Result<&[Version], ResolveError> { + // TODO: Phase 8 implementation + // - [ ] Check cache + // - [ ] Query registry + // - [ ] Parse and cache versions + + Ok(&[]) + } + + /// Get package metadata + fn get_metadata(&mut self, _id: &PackageId) -> Result<&PackageMetadata, ResolveError> { + // TODO: Phase 8 implementation + // - [ ] Check cache + // - [ ] Query registry + // - [ ] Parse and cache metadata + + Err(ResolveError::NotFound("not implemented".into())) + } +} + +impl Default for Resolver { + fn default() -> Self { + Self::new() + } +} + +/// Parse a version requirement string +pub fn parse_requirement(s: &str) -> Result { + // Handle common shortcuts + let normalized = match s { + // Exact version + s if !s.contains(['>', '<', '=', '^', '~', '*']) => format!("^{}", s), + // Already has operator + s => s.to_string(), + }; + + VersionReq::parse(&normalized) +} + +// TODO: Phase 8 implementation +// - [ ] Implement full PubGrub algorithm +// - [ ] Add version preference (prefer newer, prefer locked) +// - [ ] Add feature unification +// - [ ] Add optional dependency handling +// - [ ] Add workspace dependency resolution +// - [ ] Add parallel version fetching diff --git a/tools/affine-pkg/src/storage.rs b/tools/affine-pkg/src/storage.rs new file mode 100644 index 0000000..a9dcea9 --- /dev/null +++ b/tools/affine-pkg/src/storage.rs @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright 2024 AffineScript Contributors + +//! Content-Addressed Storage +//! +//! Stores packages by content hash for deduplication (like pnpm). + +use sha2::{Digest, Sha256}; +use std::path::{Path, PathBuf}; + +/// Content store directory +pub const STORE_DIR: &str = ".affine/store"; + +/// Package store +pub struct PackageStore { + /// Store root directory + root: PathBuf, +} + +impl PackageStore { + /// Create or open a package store + pub fn new(root: impl AsRef) -> std::io::Result { + let root = root.as_ref().to_path_buf(); + std::fs::create_dir_all(&root)?; + Ok(PackageStore { root }) + } + + /// Get the default store location + pub fn default_store() -> std::io::Result { + let home = dirs::home_dir() + .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "no home directory"))?; + + Self::new(home.join(STORE_DIR)) + } + + /// Store content and return its hash + pub fn store(&self, content: &[u8]) -> std::io::Result { + let hash = ContentHash::compute(content); + let path = self.path_for(&hash); + + if !path.exists() { + // Create parent directories + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + + // Write atomically + let temp_path = path.with_extension("tmp"); + std::fs::write(&temp_path, content)?; + std::fs::rename(&temp_path, &path)?; + } + + Ok(hash) + } + + /// Retrieve content by hash + pub fn retrieve(&self, hash: &ContentHash) -> std::io::Result> { + let path = self.path_for(hash); + std::fs::read(path) + } + + /// Check if content exists + pub fn contains(&self, hash: &ContentHash) -> bool { + self.path_for(hash).exists() + } + + /// Get path for a content hash + pub fn path_for(&self, hash: &ContentHash) -> PathBuf { + // Use first 2 chars as directory for sharding + let hex = hash.to_hex(); + self.root.join(&hex[..2]).join(&hex[2..]) + } + + /// Store a package and create a link + pub fn store_package( + &self, + name: &str, + version: &str, + content: &[u8], + ) -> std::io::Result { + let hash = self.store(content)?; + + // Create package directory + let pkg_dir = self.root.join("packages").join(name).join(version); + std::fs::create_dir_all(&pkg_dir)?; + + // Extract tarball + // TODO: Phase 8 implementation + // - [ ] Extract tar.gz to pkg_dir + // - [ ] Create integrity file + + Ok(pkg_dir) + } + + /// Link package to node_modules equivalent + pub fn link_package( + &self, + _name: &str, + _version: &str, + _target: impl AsRef, + ) -> std::io::Result<()> { + // TODO: Phase 8 implementation + // - [ ] Create symlink or copy on Windows + // - [ ] Handle nested dependencies + + Ok(()) + } + + /// Garbage collect unused content + pub fn gc(&self) -> std::io::Result { + // TODO: Phase 8 implementation + // - [ ] Scan all projects for used packages + // - [ ] Remove unreferenced content + // - [ ] Return statistics + + Ok(GcStats { + removed_count: 0, + removed_bytes: 0, + }) + } + + /// Verify store integrity + pub fn verify(&self) -> std::io::Result> { + // TODO: Phase 8 implementation + // - [ ] Scan all content + // - [ ] Verify hashes match + // - [ ] Report errors + + Ok(vec![]) + } +} + +/// Content hash (SHA-256) +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ContentHash([u8; 32]); + +impl ContentHash { + /// Compute hash of content + pub fn compute(content: &[u8]) -> Self { + let mut hasher = Sha256::new(); + hasher.update(content); + let result = hasher.finalize(); + ContentHash(result.into()) + } + + /// Parse from hex string + pub fn from_hex(hex: &str) -> Result { + let bytes = hex::decode(hex)?; + if bytes.len() != 32 { + return Err(hex::FromHexError::InvalidStringLength); + } + let mut arr = [0u8; 32]; + arr.copy_from_slice(&bytes); + Ok(ContentHash(arr)) + } + + /// Convert to hex string + pub fn to_hex(&self) -> String { + hex::encode(self.0) + } +} + +impl std::fmt::Display for ContentHash { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "sha256:{}", self.to_hex()) + } +} + +/// GC statistics +#[derive(Debug, Default)] +pub struct GcStats { + /// Number of items removed + pub removed_count: usize, + + /// Bytes freed + pub removed_bytes: u64, +} + +/// Verification error +#[derive(Debug)] +pub struct VerifyError { + /// Path to corrupted content + pub path: PathBuf, + + /// Expected hash + pub expected: ContentHash, + + /// Actual hash + pub actual: ContentHash, +} + +// TODO: Phase 8 implementation +// - [ ] Add parallel extraction +// - [ ] Add hardlink support for same-OS +// - [ ] Add copy-on-write support (reflinks) +// - [ ] Add lockfile for concurrent access +// - [ ] Add prune for specific packages diff --git a/tools/affine-pkg/src/workspace.rs b/tools/affine-pkg/src/workspace.rs new file mode 100644 index 0000000..59d9b6c --- /dev/null +++ b/tools/affine-pkg/src/workspace.rs @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright 2024 AffineScript Contributors + +//! Workspace Support +//! +//! Manages monorepos with multiple packages. + +use crate::manifest::Manifest; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +/// A workspace containing multiple packages +#[derive(Debug)] +pub struct Workspace { + /// Root directory + pub root: PathBuf, + + /// Root manifest + pub manifest: Manifest, + + /// Member packages (path -> manifest) + pub members: HashMap, +} + +impl Workspace { + /// Find and load a workspace + pub fn find(start_dir: impl AsRef) -> anyhow::Result> { + let start = start_dir.as_ref().canonicalize()?; + + // Walk up looking for workspace root + let mut current = start.as_path(); + loop { + let manifest_path = current.join("affine.toml"); + if manifest_path.exists() { + let manifest = Manifest::load(&manifest_path)?; + if manifest.workspace.is_some() { + return Ok(Some(Self::load(current)?)); + } + } + + match current.parent() { + Some(parent) => current = parent, + None => break, + } + } + + Ok(None) + } + + /// Load a workspace from its root + pub fn load(root: impl AsRef) -> anyhow::Result { + let root = root.as_ref().to_path_buf(); + let manifest = Manifest::load(root.join("affine.toml"))?; + + let mut members = HashMap::new(); + + if let Some(ws) = &manifest.workspace { + for pattern in &ws.members { + // Expand glob patterns + for entry in glob::glob(&root.join(pattern).to_string_lossy())? { + let path = entry?; + if path.is_dir() { + let member_manifest_path = path.join("affine.toml"); + if member_manifest_path.exists() { + let member_manifest = Manifest::load(&member_manifest_path)?; + members.insert(path, member_manifest); + } + } + } + } + } + + Ok(Workspace { + root, + manifest, + members, + }) + } + + /// Get all packages in the workspace (including root if it's a package) + pub fn packages(&self) -> impl Iterator { + let root_iter = if self.manifest.package.name.is_empty() { + None + } else { + Some((&self.root, &self.manifest)) + }; + + root_iter.into_iter().chain(self.members.iter()) + } + + /// Find a package by name + pub fn find_package(&self, name: &str) -> Option<(&PathBuf, &Manifest)> { + self.packages().find(|(_, m)| m.package.name == name) + } + + /// Get dependency graph between workspace members + pub fn dependency_graph(&self) -> HashMap> { + let mut graph = HashMap::new(); + + for (_, manifest) in self.packages() { + let mut deps = Vec::new(); + + for dep_name in manifest.dependencies.keys() { + // Check if this is a workspace member + if self.find_package(dep_name).is_some() { + deps.push(dep_name.clone()); + } + } + + graph.insert(manifest.package.name.clone(), deps); + } + + graph + } + + /// Topological sort of packages (dependencies first) + pub fn sorted_packages(&self) -> anyhow::Result> { + let graph = self.dependency_graph(); + let mut result = Vec::new(); + let mut visited = std::collections::HashSet::new(); + let mut visiting = std::collections::HashSet::new(); + + fn visit( + name: &str, + graph: &HashMap>, + visited: &mut std::collections::HashSet, + visiting: &mut std::collections::HashSet, + result: &mut Vec, + ) -> anyhow::Result<()> { + if visited.contains(name) { + return Ok(()); + } + if visiting.contains(name) { + anyhow::bail!("Cyclic dependency detected involving: {}", name); + } + + visiting.insert(name.to_string()); + + if let Some(deps) = graph.get(name) { + for dep in deps { + visit(dep, graph, visited, visiting, result)?; + } + } + + visiting.remove(name); + visited.insert(name.to_string()); + result.push(name.to_string()); + + Ok(()) + } + + for name in graph.keys() { + visit(name, &graph, &mut visited, &mut visiting, &mut result)?; + } + + Ok(result) + } +} + +// TODO: Phase 8 implementation +// - [ ] Add workspace inheritance for dependencies +// - [ ] Add workspace-level features +// - [ ] Add parallel builds across workspace +// - [ ] Add affected package detection for CI +// - [ ] Add workspace-wide linting/formatting diff --git a/tools/affinescript-lsp/Cargo.toml b/tools/affinescript-lsp/Cargo.toml new file mode 100644 index 0000000..400cb68 --- /dev/null +++ b/tools/affinescript-lsp/Cargo.toml @@ -0,0 +1,45 @@ +# SPDX-License-Identifier: Apache-2.0 OR MIT +# Copyright 2024 AffineScript Contributors + +[package] +name = "affinescript-lsp" +version = "0.1.0" +edition = "2021" +description = "Language Server Protocol implementation for AffineScript" +license = "Apache-2.0 OR MIT" +repository = "https://github.com/affinescript/affinescript" +keywords = ["lsp", "language-server", "affinescript", "ide"] +categories = ["development-tools", "text-editors"] + +[dependencies] +# LSP protocol types +tower-lsp = "0.20" +lsp-types = "0.94" + +# Async runtime +tokio = { version = "1", features = ["full"] } + +# Serialization +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +# Logging +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +# Path handling +camino = "1" + +# Error handling +thiserror = "1" +anyhow = "1" + +# TODO: Add affinescript compiler as dependency once published +# affinescript-compiler = { path = "../../" } + +[dev-dependencies] +tokio-test = "0.4" + +[[bin]] +name = "affinescript-lsp" +path = "src/main.rs" diff --git a/tools/affinescript-lsp/src/capabilities.rs b/tools/affinescript-lsp/src/capabilities.rs new file mode 100644 index 0000000..eb31fe6 --- /dev/null +++ b/tools/affinescript-lsp/src/capabilities.rs @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright 2024 AffineScript Contributors + +//! Server Capabilities +//! +//! Defines what features the language server supports. + +use tower_lsp::lsp_types::*; + +/// Build the server capabilities to advertise to the client +pub fn server_capabilities() -> ServerCapabilities { + ServerCapabilities { + // Text document sync + text_document_sync: Some(TextDocumentSyncCapability::Kind( + TextDocumentSyncKind::INCREMENTAL, + )), + + // Hover + hover_provider: Some(HoverProviderCapability::Simple(true)), + + // Completion + completion_provider: Some(CompletionOptions { + trigger_characters: Some(vec![".".to_string(), ":".to_string()]), + resolve_provider: Some(true), + ..Default::default() + }), + + // Definition + definition_provider: Some(OneOf::Left(true)), + + // References + references_provider: Some(OneOf::Left(true)), + + // Document highlight (same-symbol highlighting) + document_highlight_provider: Some(OneOf::Left(true)), + + // Document symbols (outline) + document_symbol_provider: Some(OneOf::Left(true)), + + // Workspace symbol search + workspace_symbol_provider: Some(OneOf::Left(true)), + + // Code actions (quick fixes) + code_action_provider: Some(CodeActionProviderCapability::Simple(true)), + + // Formatting + document_formatting_provider: Some(OneOf::Left(true)), + + // Range formatting + document_range_formatting_provider: Some(OneOf::Left(true)), + + // Rename + rename_provider: Some(OneOf::Right(RenameOptions { + prepare_provider: Some(true), + work_done_progress_options: WorkDoneProgressOptions::default(), + })), + + // Folding + folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)), + + // Signature help + signature_help_provider: Some(SignatureHelpOptions { + trigger_characters: Some(vec!["(".to_string(), ",".to_string()]), + retrigger_characters: Some(vec![",".to_string()]), + work_done_progress_options: WorkDoneProgressOptions::default(), + }), + + // Semantic tokens (syntax highlighting) + semantic_tokens_provider: Some( + SemanticTokensServerCapabilities::SemanticTokensOptions(SemanticTokensOptions { + legend: SemanticTokensLegend { + token_types: semantic_token_types(), + token_modifiers: semantic_token_modifiers(), + }, + full: Some(SemanticTokensFullOptions::Bool(true)), + range: Some(true), + ..Default::default() + }), + ), + + // Inlay hints (type annotations) + inlay_hint_provider: Some(OneOf::Left(true)), + + // Workspace capabilities + workspace: Some(WorkspaceServerCapabilities { + workspace_folders: Some(WorkspaceFoldersServerCapabilities { + supported: Some(true), + change_notifications: Some(OneOf::Left(true)), + }), + file_operations: None, + }), + + ..Default::default() + } +} + +/// Semantic token types for syntax highlighting +fn semantic_token_types() -> Vec { + vec![ + SemanticTokenType::NAMESPACE, + SemanticTokenType::TYPE, + SemanticTokenType::CLASS, + SemanticTokenType::ENUM, + SemanticTokenType::INTERFACE, + SemanticTokenType::STRUCT, + SemanticTokenType::TYPE_PARAMETER, + SemanticTokenType::PARAMETER, + SemanticTokenType::VARIABLE, + SemanticTokenType::PROPERTY, + SemanticTokenType::ENUM_MEMBER, + SemanticTokenType::FUNCTION, + SemanticTokenType::METHOD, + SemanticTokenType::MACRO, + SemanticTokenType::KEYWORD, + SemanticTokenType::MODIFIER, + SemanticTokenType::COMMENT, + SemanticTokenType::STRING, + SemanticTokenType::NUMBER, + SemanticTokenType::OPERATOR, + // AffineScript-specific + SemanticTokenType::new("effect"), + SemanticTokenType::new("handler"), + SemanticTokenType::new("quantity"), + SemanticTokenType::new("lifetime"), + ] +} + +/// Semantic token modifiers +fn semantic_token_modifiers() -> Vec { + vec![ + SemanticTokenModifier::DECLARATION, + SemanticTokenModifier::DEFINITION, + SemanticTokenModifier::READONLY, + SemanticTokenModifier::STATIC, + SemanticTokenModifier::DEPRECATED, + SemanticTokenModifier::ABSTRACT, + SemanticTokenModifier::ASYNC, + SemanticTokenModifier::MODIFICATION, + SemanticTokenModifier::DOCUMENTATION, + SemanticTokenModifier::DEFAULT_LIBRARY, + // AffineScript-specific + SemanticTokenModifier::new("linear"), + SemanticTokenModifier::new("affine"), + SemanticTokenModifier::new("unrestricted"), + SemanticTokenModifier::new("erased"), + SemanticTokenModifier::new("mutable"), + SemanticTokenModifier::new("borrowed"), + ] +} diff --git a/tools/affinescript-lsp/src/diagnostics.rs b/tools/affinescript-lsp/src/diagnostics.rs new file mode 100644 index 0000000..27b1619 --- /dev/null +++ b/tools/affinescript-lsp/src/diagnostics.rs @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright 2024 AffineScript Contributors + +//! Diagnostics +//! +//! Converts AffineScript compiler diagnostics to LSP diagnostics. + +use tower_lsp::lsp_types::*; + +/// Diagnostic severity mapping +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Severity { + Error, + Warning, + Info, + Hint, +} + +impl From for DiagnosticSeverity { + fn from(severity: Severity) -> Self { + match severity { + Severity::Error => DiagnosticSeverity::ERROR, + Severity::Warning => DiagnosticSeverity::WARNING, + Severity::Info => DiagnosticSeverity::INFORMATION, + Severity::Hint => DiagnosticSeverity::HINT, + } + } +} + +/// AffineScript diagnostic category +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DiagnosticCategory { + /// Parse error + Parse, + /// Type error + Type, + /// Borrow checker error + Borrow, + /// Effect error + Effect, + /// Quantity error + Quantity, + /// Name resolution error + Name, + /// Refinement type error + Refinement, + /// Warning + Warning, + /// Lint + Lint, +} + +impl DiagnosticCategory { + /// Get the error code prefix + pub fn code_prefix(&self) -> &'static str { + match self { + DiagnosticCategory::Parse => "E0", + DiagnosticCategory::Type => "E1", + DiagnosticCategory::Borrow => "E2", + DiagnosticCategory::Effect => "E3", + DiagnosticCategory::Quantity => "E4", + DiagnosticCategory::Name => "E5", + DiagnosticCategory::Refinement => "E6", + DiagnosticCategory::Warning => "W", + DiagnosticCategory::Lint => "L", + } + } +} + +/// AffineScript diagnostic +#[derive(Debug, Clone)] +pub struct AfsDiagnostic { + /// Error category + pub category: DiagnosticCategory, + /// Error code (within category) + pub code: u32, + /// Error message + pub message: String, + /// Primary span + pub span: AfsSpan, + /// Additional labeled spans + pub labels: Vec<(AfsSpan, String)>, + /// Help text + pub help: Option, + /// Note text + pub note: Option, +} + +/// Source span +#[derive(Debug, Clone)] +pub struct AfsSpan { + pub file: String, + pub start_line: u32, + pub start_col: u32, + pub end_line: u32, + pub end_col: u32, +} + +impl AfsSpan { + /// Convert to LSP range + pub fn to_range(&self) -> Range { + Range { + start: Position { + line: self.start_line.saturating_sub(1), + character: self.start_col.saturating_sub(1), + }, + end: Position { + line: self.end_line.saturating_sub(1), + character: self.end_col.saturating_sub(1), + }, + } + } +} + +impl AfsDiagnostic { + /// Convert to LSP diagnostic + pub fn to_lsp(&self) -> Diagnostic { + let severity = match self.category { + DiagnosticCategory::Warning | DiagnosticCategory::Lint => Severity::Warning, + _ => Severity::Error, + }; + + let mut related = Vec::new(); + for (span, label) in &self.labels { + related.push(DiagnosticRelatedInformation { + location: Location { + uri: Url::parse(&format!("file://{}", span.file)).unwrap(), + range: span.to_range(), + }, + message: label.clone(), + }); + } + + let mut message = self.message.clone(); + if let Some(help) = &self.help { + message.push_str(&format!("\n\nhelp: {}", help)); + } + if let Some(note) = &self.note { + message.push_str(&format!("\n\nnote: {}", note)); + } + + Diagnostic { + range: self.span.to_range(), + severity: Some(severity.into()), + code: Some(NumberOrString::String(format!( + "{}{}", + self.category.code_prefix(), + self.code + ))), + code_description: Some(CodeDescription { + href: Url::parse(&format!( + "https://affinescript.dev/errors/{}{}", + self.category.code_prefix(), + self.code + )) + .unwrap(), + }), + source: Some("affinescript".to_string()), + message, + related_information: if related.is_empty() { + None + } else { + Some(related) + }, + tags: None, + data: None, + } + } +} + +/// Convert compiler diagnostics to LSP diagnostics +pub fn convert_diagnostics(_diagnostics: Vec) -> Vec { + // TODO: Phase 8 implementation + // - [ ] Connect to compiler diagnostic output + // - [ ] Convert spans correctly + // - [ ] Handle multi-file diagnostics + + vec![] +} + +// TODO: Phase 8 implementation +// - [ ] Import actual diagnostic types from compiler +// - [ ] Implement diagnostic code links +// - [ ] Add diagnostic tags (deprecated, unnecessary) +// - [ ] Support diagnostic quickfixes diff --git a/tools/affinescript-lsp/src/document.rs b/tools/affinescript-lsp/src/document.rs new file mode 100644 index 0000000..7bc34b3 --- /dev/null +++ b/tools/affinescript-lsp/src/document.rs @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright 2024 AffineScript Contributors + +//! Document Management +//! +//! Tracks open documents and their state. + +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; +use tower_lsp::lsp_types::*; + +/// Manages open documents +#[derive(Debug)] +pub struct DocumentManager { + /// Open documents by URI + documents: Arc>>, +} + +impl DocumentManager { + /// Create a new document manager + pub fn new() -> Self { + DocumentManager { + documents: Arc::new(RwLock::new(HashMap::new())), + } + } + + /// Open a document + pub fn open(&self, uri: Url, text: String, version: i32) { + let doc = Document::new(text, version); + self.documents.write().unwrap().insert(uri, doc); + } + + /// Close a document + pub fn close(&self, uri: &Url) { + self.documents.write().unwrap().remove(uri); + } + + /// Apply changes to a document + pub fn apply_changes(&self, uri: &Url, version: i32, changes: Vec) { + let mut docs = self.documents.write().unwrap(); + if let Some(doc) = docs.get_mut(uri) { + doc.apply_changes(version, changes); + } + } + + /// Get document text + pub fn get_text(&self, uri: &Url) -> Option { + self.documents.read().unwrap().get(uri).map(|d| d.text.clone()) + } + + /// Get document version + pub fn get_version(&self, uri: &Url) -> Option { + self.documents.read().unwrap().get(uri).map(|d| d.version) + } +} + +impl Default for DocumentManager { + fn default() -> Self { + Self::new() + } +} + +/// A single document +#[derive(Debug)] +pub struct Document { + /// Document text + pub text: String, + /// Document version + pub version: i32, + /// Line offsets (byte offset of each line start) + line_offsets: Vec, + // TODO: Add parsed AST cache + // TODO: Add type-checked state cache +} + +impl Document { + /// Create a new document + pub fn new(text: String, version: i32) -> Self { + let line_offsets = compute_line_offsets(&text); + Document { + text, + version, + line_offsets, + } + } + + /// Apply content changes + pub fn apply_changes(&mut self, version: i32, changes: Vec) { + self.version = version; + + for change in changes { + match change.range { + Some(range) => { + // Incremental change + let start_offset = self.offset_at(range.start); + let end_offset = self.offset_at(range.end); + self.text.replace_range(start_offset..end_offset, &change.text); + } + None => { + // Full document change + self.text = change.text; + } + } + } + + // Recompute line offsets + self.line_offsets = compute_line_offsets(&self.text); + } + + /// Get byte offset from position + pub fn offset_at(&self, pos: Position) -> usize { + let line = pos.line as usize; + if line >= self.line_offsets.len() { + return self.text.len(); + } + + let line_start = self.line_offsets[line]; + let line_end = if line + 1 < self.line_offsets.len() { + self.line_offsets[line + 1] + } else { + self.text.len() + }; + + let col = pos.character as usize; + let line_text = &self.text[line_start..line_end]; + + // Handle UTF-16 code units + let mut char_offset = 0; + let mut utf16_offset = 0; + + for c in line_text.chars() { + if utf16_offset >= col { + break; + } + char_offset += c.len_utf8(); + utf16_offset += c.len_utf16(); + } + + line_start + char_offset + } + + /// Get position from byte offset + pub fn position_at(&self, offset: usize) -> Position { + let offset = offset.min(self.text.len()); + + // Find line + let line = self + .line_offsets + .iter() + .position(|&o| o > offset) + .map(|l| l - 1) + .unwrap_or(self.line_offsets.len() - 1); + + let line_start = self.line_offsets[line]; + let line_text = &self.text[line_start..offset]; + + // Count UTF-16 code units + let character = line_text.chars().map(|c| c.len_utf16()).sum::(); + + Position { + line: line as u32, + character: character as u32, + } + } +} + +/// Compute byte offsets of line starts +fn compute_line_offsets(text: &str) -> Vec { + let mut offsets = vec![0]; + for (i, c) in text.char_indices() { + if c == '\n' { + offsets.push(i + 1); + } + } + offsets +} + +// TODO: Phase 8 implementation +// - [ ] Add AST caching +// - [ ] Add incremental parsing +// - [ ] Add type information caching +// - [ ] Track diagnostics per document +// - [ ] Implement dependency tracking diff --git a/tools/affinescript-lsp/src/handlers.rs b/tools/affinescript-lsp/src/handlers.rs new file mode 100644 index 0000000..35108a5 --- /dev/null +++ b/tools/affinescript-lsp/src/handlers.rs @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright 2024 AffineScript Contributors + +//! Request Handlers +//! +//! Implementation of LSP request handlers. + +use tower_lsp::lsp_types::*; + +/// Handle hover request +pub fn hover(_uri: &Url, _position: Position, _text: &str) -> Option { + // TODO: Phase 8 implementation + // - [ ] Parse document to AST + // - [ ] Find node at position + // - [ ] Get type information + // - [ ] Format hover content + + // Example response structure: + // Some(Hover { + // contents: HoverContents::Markup(MarkupContent { + // kind: MarkupKind::Markdown, + // value: format!("```affinescript\n{}\n```\n{}", type_sig, docs), + // }), + // range: Some(Range { ... }), + // }) + + None +} + +/// Handle goto definition +pub fn goto_definition(_uri: &Url, _position: Position, _text: &str) -> Option { + // TODO: Phase 8 implementation + // - [ ] Parse document to AST + // - [ ] Find symbol at position + // - [ ] Look up definition in symbol table + // - [ ] Return location + + None +} + +/// Handle find references +pub fn find_references( + _uri: &Url, + _position: Position, + _text: &str, + _include_declaration: bool, +) -> Vec { + // TODO: Phase 8 implementation + // - [ ] Find symbol at position + // - [ ] Search for all references in workspace + // - [ ] Optionally include declaration + + vec![] +} + +/// Handle completion +pub fn completion(_uri: &Url, _position: Position, _text: &str) -> Vec { + // TODO: Phase 8 implementation + // - [ ] Determine completion context (after dot, in type position, etc.) + // - [ ] Generate candidates based on context + // - [ ] Include: + // - [ ] Local variables in scope + // - [ ] Module members + // - [ ] Type names + // - [ ] Effect names + // - [ ] Keywords + // - [ ] Snippets + + // Example: + // vec![ + // CompletionItem { + // label: "map".to_string(), + // kind: Some(CompletionItemKind::FUNCTION), + // detail: Some("fn map[A, B](f: A -> B, xs: List[A]) -> List[B]".to_string()), + // documentation: Some(Documentation::String("Apply f to each element".to_string())), + // ..Default::default() + // }, + // ] + + vec![] +} + +/// Handle rename +pub fn prepare_rename(_uri: &Url, _position: Position, _text: &str) -> Option { + // TODO: Phase 8 implementation + // - [ ] Check if position is on renameable symbol + // - [ ] Return symbol range + + None +} + +/// Handle rename +pub fn rename( + _uri: &Url, + _position: Position, + _new_name: &str, + _text: &str, +) -> Option { + // TODO: Phase 8 implementation + // - [ ] Find all references + // - [ ] Generate text edits for each + // - [ ] Handle cross-file renames + + None +} + +/// Handle document formatting +pub fn format(_uri: &Url, _text: &str, _options: &FormattingOptions) -> Vec { + // TODO: Phase 8 implementation + // - [ ] Parse document + // - [ ] Format AST + // - [ ] Compute minimal diff + + vec![] +} + +/// Handle code actions +pub fn code_actions(_uri: &Url, _range: Range, _diagnostics: &[Diagnostic]) -> Vec { + // TODO: Phase 8 implementation + // - [ ] Generate quick fixes for diagnostics + // - [ ] Add refactoring actions + // - [ ] Extract function + // - [ ] Extract variable + // - [ ] Inline variable + // - [ ] Add type annotation + // - [ ] Convert to/from effect style + + vec![] +} + +/// Handle document symbols +pub fn document_symbols(_uri: &Url, _text: &str) -> Vec { + // TODO: Phase 8 implementation + // - [ ] Parse document + // - [ ] Extract top-level items + // - [ ] Build hierarchical symbol tree + + vec![] +} + +/// Handle signature help +pub fn signature_help(_uri: &Url, _position: Position, _text: &str) -> Option { + // TODO: Phase 8 implementation + // - [ ] Determine if inside function call + // - [ ] Get function signature + // - [ ] Highlight active parameter + + None +} + +/// Handle inlay hints +pub fn inlay_hints(_uri: &Url, _range: Range, _text: &str) -> Vec { + // TODO: Phase 8 implementation + // - [ ] Find bindings without explicit types + // - [ ] Add type hints + // - [ ] Add parameter name hints at call sites + // - [ ] Add lifetime hints (if not obvious) + + vec![] +} + +// TODO: Phase 8 implementation +// - [ ] Connect to AffineScript compiler +// - [ ] Implement caching for performance +// - [ ] Add semantic tokens for syntax highlighting +// - [ ] Add call hierarchy +// - [ ] Add type hierarchy diff --git a/tools/affinescript-lsp/src/main.rs b/tools/affinescript-lsp/src/main.rs new file mode 100644 index 0000000..ff82f18 --- /dev/null +++ b/tools/affinescript-lsp/src/main.rs @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: Apache-2.0 OR MIT +// Copyright 2024 AffineScript Contributors + +//! AffineScript Language Server +//! +//! Provides IDE features via the Language Server Protocol: +//! - Diagnostics (errors, warnings) +//! - Hover information +//! - Go to definition +//! - Find references +//! - Code completion +//! - Rename +//! - Formatting +//! - Code actions + +use tower_lsp::jsonrpc::Result; +use tower_lsp::lsp_types::*; +use tower_lsp::{Client, LanguageServer, LspService, Server}; + +mod capabilities; +mod diagnostics; +mod document; +mod handlers; + +/// The AffineScript language server backend +#[derive(Debug)] +struct Backend { + /// LSP client for sending notifications + client: Client, + /// Document manager + documents: document::DocumentManager, +} + +impl Backend { + fn new(client: Client) -> Self { + Backend { + client, + documents: document::DocumentManager::new(), + } + } +} + +#[tower_lsp::async_trait] +impl LanguageServer for Backend { + async fn initialize(&self, _: InitializeParams) -> Result { + Ok(InitializeResult { + capabilities: capabilities::server_capabilities(), + server_info: Some(ServerInfo { + name: "affinescript-lsp".to_string(), + version: Some(env!("CARGO_PKG_VERSION").to_string()), + }), + }) + } + + async fn initialized(&self, _: InitializedParams) { + self.client + .log_message(MessageType::INFO, "AffineScript LSP initialized") + .await; + } + + async fn shutdown(&self) -> Result<()> { + Ok(()) + } + + async fn did_open(&self, params: DidOpenTextDocumentParams) { + // TODO: Phase 8 implementation + // - [ ] Add document to manager + // - [ ] Parse and type check + // - [ ] Publish diagnostics + let _ = params; + } + + async fn did_change(&self, params: DidChangeTextDocumentParams) { + // TODO: Phase 8 implementation + // - [ ] Update document in manager + // - [ ] Incremental re-check + // - [ ] Publish diagnostics + let _ = params; + } + + async fn did_close(&self, params: DidCloseTextDocumentParams) { + // TODO: Phase 8 implementation + // - [ ] Remove document from manager + // - [ ] Clear diagnostics + let _ = params; + } + + async fn hover(&self, params: HoverParams) -> Result> { + // TODO: Phase 8 implementation + // - [ ] Find symbol at position + // - [ ] Get type information + // - [ ] Format hover content + let _ = params; + Ok(None) + } + + async fn goto_definition( + &self, + params: GotoDefinitionParams, + ) -> Result> { + // TODO: Phase 8 implementation + // - [ ] Find symbol at position + // - [ ] Look up definition location + // - [ ] Return location + let _ = params; + Ok(None) + } + + async fn references(&self, params: ReferenceParams) -> Result>> { + // TODO: Phase 8 implementation + // - [ ] Find symbol at position + // - [ ] Search for all references + // - [ ] Return locations + let _ = params; + Ok(None) + } + + async fn completion(&self, params: CompletionParams) -> Result> { + // TODO: Phase 8 implementation + // - [ ] Determine completion context + // - [ ] Generate candidates + // - [ ] Filter and rank + let _ = params; + Ok(None) + } + + async fn rename(&self, params: RenameParams) -> Result> { + // TODO: Phase 8 implementation + // - [ ] Find symbol at position + // - [ ] Find all references + // - [ ] Generate workspace edit + let _ = params; + Ok(None) + } + + async fn formatting(&self, params: DocumentFormattingParams) -> Result>> { + // TODO: Phase 8 implementation + // - [ ] Parse document + // - [ ] Format AST + // - [ ] Compute diff + let _ = params; + Ok(None) + } + + async fn code_action(&self, params: CodeActionParams) -> Result> { + // TODO: Phase 8 implementation + // - [ ] Get diagnostics for range + // - [ ] Generate fix suggestions + let _ = params; + Ok(None) + } +} + +#[tokio::main] +async fn main() { + // Initialize logging + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::from_default_env() + .add_directive(tracing::Level::INFO.into()), + ) + .init(); + + tracing::info!("Starting AffineScript Language Server"); + + let stdin = tokio::io::stdin(); + let stdout = tokio::io::stdout(); + + let (service, socket) = LspService::new(Backend::new); + Server::new(stdin, stdout, socket).serve(service).await; +} + +// TODO: Phase 8 implementation +// - [ ] Implement document manager with incremental updates +// - [ ] Integrate with AffineScript compiler for parsing/checking +// - [ ] Implement semantic tokens for syntax highlighting +// - [ ] Add inlay hints for types +// - [ ] Implement signature help +// - [ ] Add document symbols (outline) +// - [ ] Implement workspace symbol search +// - [ ] Add folding ranges