summaryrefslogtreecommitdiff
path: root/lambda-calcul/haskell/plan.md
diff options
context:
space:
mode:
Diffstat (limited to 'lambda-calcul/haskell/plan.md')
-rw-r--r--lambda-calcul/haskell/plan.md212
1 files changed, 143 insertions, 69 deletions
diff --git a/lambda-calcul/haskell/plan.md b/lambda-calcul/haskell/plan.md
index 2d8a21b..724b719 100644
--- a/lambda-calcul/haskell/plan.md
+++ b/lambda-calcul/haskell/plan.md
@@ -7,10 +7,10 @@ participants to actually *change* the parser, evaluator, or type system.
The codebase currently consists of:
-- **`Minilang.Lambda.Parser`** — Megaparsec parser producing an `AST` (`Sym`, `Abs`, `App`,
+- **`Minilang.Lambda.Parser`** — Megaparsec parser producing an `AST` (`Sym`, `Lit`, `Abs`, `App`,
`Def`), with `desugar`/`resugar`/`pretty` for round-tripping to `Term`.
- **`Minilang.Lambda.Eval`** — Normalisation-by-Evaluation (NbE) evaluator over `Term`
- (`Var`, `Lam`, `App`), producing `Value` (`Clos`, `VNeut`).
+ (`Var`, `Lam`, `App`, `Num`), producing `Value` (`Clos`, `VNeut`, `VNum`).
- **`Minilang.Lambda.DeBruijn`** — A separate CBV head-reduction evaluator used by
`evalInEnv` at the top level, operating on De Bruijn-indexed terms.
- **`Minilang.Lambda.Infer` / `Unify`** — A complete Hindley-Milner type inferencer
@@ -22,38 +22,7 @@ The codebase currently consists of:
---
-## Extension 1 — Typed mode challenges
-
-**Difficulty: 1 / 5**
-
-### What it is
-
-The Hindley-Milner type inferencer in `Minilang.Lambda.Infer` and `Minilang.Lambda.Unify`
-is fully implemented and reachable via `:lang Typed` in the interactive REPL, but is
-completely invisible to the challenge system. `evaluateOutputs` in `App.hs` pattern-matches
-on `[Bye, Evaluated ast _ _]` and discards the `Type` field entirely.
-
-### What changes
-
-- Switch `initialREPL` from `Untyped` to `Typed` for these challenges (one field change).
-- Add a parallel `evaluateType` path in `App.hs` that pattern-matches on the `Type` in the
- `Evaluated` constructor and compares it against the submitted string using `Unify.pretty`.
-- Optionally introduce a second contest endpoint or a `kind: "type"` field in `Challenge`
- to distinguish "what does this reduce to?" from "what type does this have?".
-
-### Example challenges
-
-| Expression | Expected type |
-|---|---|
-| `(lam (x) x)` | `a -> a` |
-| `(lam (x y) x)` | `a -> b -> a` |
-| `(lam (f x) (f (f x)))` | `(a -> a) -> a -> a` |
-| `(lam (x) (x x))` | circularity error |
-| `(lam (f g x) (f (g x)))` | `(b -> c) -> (a -> b) -> a -> c` |
-
----
-
-## Extension 2 — Integer literals
+## Extension 1 — Integer literals ✅ implemented
**Difficulty: 2 / 5**
@@ -63,40 +32,58 @@ Add native integer values to the language. An integer literal like `42` or `-7`
self-evaluating value — it needs no reduction. This is the smallest possible extension to
`Term` and the foundation for all arithmetic extensions that follow.
-### What changes
+### What was changed
**`Minilang.Lambda.Eval`**
-- Add `Num Int` to the `Term` and `Value` types.
-- `eval`: one new case — a `Num` literal evaluates to `VNum`.
-- `readBack`: one new case — `VNum n` reads back as `Num n`.
+- Added `Num Int` to `Term` and `VNum Int` to `Value`.
+- Added `NNum Int` to `Neutral` so that a number in function position produces a stuck
+ neutral application rather than a pattern-match failure.
+- `eval`: `Num n → VNum n`.
+- `apply`: `VNum n` in function position wraps in `VNeut (NApp (NNum n) a)`.
+- `readBack`: `VNum n → Num n` and `VNeut (NNum n) → Num n`.
**`Minilang.Lambda.DeBruijn`**
-- Add `FNum Int` to `DeBruijn`, propagated through `toDeBruijn`, `fromDeBruijn`,
- `substitute`, `eval`/`eval'`, and `pretty`.
+- Added `FNum Int` to `DeBruijn`, propagated through `fromDeBruijn`, `toDeBruijn`,
+ `substitute` (numbers are closed values — `substitute` is a no-op), `eval`/`eval'`
+ (numbers are already values), and `pretty` (rendered via `showNum`).
+- `showNum` renders negative integers with `-` immediately preceding the digits and no
+ intervening space, matching the parser's requirement.
**`Minilang.Lambda.Parser`**
-- Add `Lit Int` to `AST`.
-- Add a `lit` parser rule using `L.signed spaceConsumer L.decimal`, tried before `sym` so
- that `-7` is not mistaken for an identifier starting with `-`.
-- `desugar`: `Lit n → Num n`. `resugar`: `Num n → Lit n`. `pretty`: `Lit n → pack (show n)`.
+- Added `Lit Int` to `AST`.
+- Added a `lit` parser rule: optional `char '-'` with **no intervening whitespace** before
+ `some digitChar`, wrapped in `lexeme`. Tried before `sym` in the `ast` alternation so
+ that `-7` is consumed as a number rather than as an identifier starting with `-`.
+- `desugar`: `Lit n → Num n`. `resugar`: `Num n → Lit n`.
+- `pretty`/`prettyInt`: negative integers printed as `-N` (no space), positives as `N`.
+- `Arbitrary AST` updated: `genAst` now includes `Lit <$> arbitrary`; `shrink` has a
+ `Lit _ → []` base case; `Identifier` generator gained a `suchThat` guard excluding
+ strings whose prefix would be consumed as an integer literal by the parser.
+
+**Tests**
+- `EvalSpec`: 8 new unit tests covering `normalise` and `eval` on `Num` values, identity
+ through a lambda, constant ignoring an integer, and a number in function position.
+- `ParserSpec`: 2 new property tests replacing all unit tests:
+ - *valid*: `∀ n :: Int. parse (pretty (Lit n)) == Right (Lit n)`
+ - *invalid*: `∀ n > 0. parse ("- " <> show n) ≠ Right (Lit (-n))`
### Example challenges
| Expression | Result |
|---|---|
| `42` | `42` |
-| `(lam (x) x) 99` | `99` |
-| `(lam (n) n) -3` | `-3` |
+| `((lam (x) x) 99)` | `99` |
+| `((lam (n) n) -3)` | `-3` |
---
-## Extension 3 — Built-in arithmetic operators
+## Extension 2 — Built-in arithmetic operators
**Difficulty: 2 / 5**
### What it is
-Depends on Extension 2. Add `+`, `-`, `*`, `/`, `mod`, `=`, `<` as first-class built-in
+Depends on Extension 1. Add `+`, `-`, `*`, `/`, `mod`, `=`, `<` as first-class built-in
functions. The key interpreter challenge beyond the trivial "add a case" is *partial
application*: `(+ 1)` must be a valid value — a closure waiting for its second argument —
rather than an error.
@@ -123,12 +110,12 @@ rather than an error.
| `(+ 3 4)` | `7` |
| `(* 2 (+ 1 4))` | `10` |
| `(- 10 3)` | `7` |
-| `(= 3 3)` | `#t` (requires Extension 5) or `True` Church-bool |
+| `(= 3 3)` | `#t` (requires Extension 6) or `True` Church-bool |
| `(+ 1)` | a partial application — prints as a neutral `(+ 1)` |
---
-## Extension 4 — String literals
+## Extension 3 — String literals
**Difficulty: 2 / 5**
@@ -153,7 +140,7 @@ on the escape convention.
- `desugar`: `StrLit s → Str s`. `resugar`: `Str s → StrLit s`.
- `pretty`: `StrLit s → "\"" <> escapeString s <> "\""`.
-**String builtins** (same pattern as Extension 3): `concat`, `length`, `slice`, `str=`.
+**String builtins** (same pattern as Extension 2): `concat`, `length`, `slice`, `str=`.
### Example challenges
@@ -166,7 +153,94 @@ on the escape convention.
---
-## Extension 5 — Boolean literals and `if`
+## Extension 4 — Floating-point number literals
+
+**Difficulty: 2 / 5**
+
+### What it is
+
+Add native double-precision floating-point values to the language. A float literal like
+`3.14` or `-0.5` is self-evaluating. This is similar to the integer literal extension but
+requires careful handling in the parser to distinguish floats from integers (floats always
+contain a `.`), and in `pretty` to ensure the printed form is always re-parseable as a
+float (e.g. `1.0` rather than `1`).
+
+### What changes
+
+**`Minilang.Lambda.Eval`**
+- Add `Flt Double` to `Term` and `VFlt Double` to `Value`.
+- Add `NFlt Double` to `Neutral` so a float in function position produces a stuck neutral
+ application rather than a pattern-match failure.
+- `eval`: `Flt d → VFlt d`.
+- `apply`: `VFlt d` in function position wraps in `VNeut (NApp (NFlt d) a)`.
+- `readBack`: `VFlt d → Flt d` and `VNeut (NFlt d) → Flt d`.
+
+**`Minilang.Lambda.DeBruijn`**
+- Add `FFlt Double`, propagated through `fromDeBruijn`, `toDeBruijn`, `substitute`
+ (floats are closed values), `eval`/`eval'`, and `pretty` (via `showFlt`).
+- `showFlt` renders the value with enough decimal places that the printed form round-trips
+ through the parser (always includes a `.`).
+
+**`Minilang.Lambda.Parser`**
+- Add `FltLit Double` to `AST`.
+- Parser rule: try the float rule (digits, `.`, digits, optional exponent) *before* the
+ integer rule so that `3.14` is not partially consumed as the integer `3`.
+- `desugar`: `FltLit d → Flt d`. `resugar`: `Flt d → FltLit d`.
+- `pretty`/`prettyFlt`: always emit a decimal point; negative floats printed as `-D.D`
+ (no space), matching the parser's requirement.
+- `Arbitrary AST` updated: `genAst` includes `FltLit <$> arbitrary`; `shrink` has a
+ `FltLit _ → []` base case; the `Identifier` guard is extended to exclude strings that
+ would be consumed as float literals.
+
+**Tests**
+- `EvalSpec`: unit tests covering `normalise` and `eval` on `Flt` values, identity through
+ a lambda, constant ignoring a float, and a float in function position.
+- `ParserSpec`: property tests mirroring the integer ones:
+ - *valid*: `∀ d :: Double. parse (pretty (FltLit d)) == Right (FltLit d)`
+ - *invalid*: `∀ d > 0. parse ("- " <> show d) ≠ Right (FltLit (-d))`
+
+### Example challenges
+
+| Expression | Result |
+|---|---|
+| `3.14` | `3.14` |
+| `((lam (x) x) 2.718)` | `2.718` |
+| `((lam (n) n) -0.5)` | `-0.5` |
+
+---
+
+## Extension 5 — Typed mode challenges
+
+**Difficulty: 3 / 5**
+
+### What it is
+
+The Hindley-Milner type inferencer in `Minilang.Lambda.Infer` and `Minilang.Lambda.Unify`
+is fully implemented and reachable via `:lang Typed` in the interactive REPL, but is
+completely invisible to the challenge system. `evaluateOutputs` in `App.hs` pattern-matches
+on `[Bye, Evaluated ast _ _]` and discards the `Type` field entirely.
+
+### What changes
+
+- Switch `initialREPL` from `Untyped` to `Typed` for these challenges (one field change).
+- Add a parallel `evaluateType` path in `App.hs` that pattern-matches on the `Type` in the
+ `Evaluated` constructor and compares it against the submitted string using `Unify.pretty`.
+- Optionally introduce a second contest endpoint or a `kind: "type"` field in `Challenge`
+ to distinguish "what does this reduce to?" from "what type does this have?".
+
+### Example challenges
+
+| Expression | Expected type |
+|---|---|
+| `(lam (x) x)` | `a -> a` |
+| `(lam (x y) x)` | `a -> b -> a` |
+| `(lam (f x) (f (f x)))` | `(a -> a) -> a -> a` |
+| `(lam (x) (x x))` | circularity error |
+| `(lam (f g x) (f (g x)))` | `(b -> c) -> (a -> b) -> a -> c` |
+
+---
+
+## Extension 6 — Boolean literals and `if`
**Difficulty: 2 / 5**
@@ -201,12 +275,12 @@ A divergent or error-raising expression in the dead branch must not be evaluated
| `#t` | `#t` |
| `(if #t a b)` | `a` |
| `(if #f a b)` | `b` |
-| `(if (= 1 1) "yes" "no")` | `"yes"` (requires Extensions 2–4) |
+| `(if (= 1 1) "yes" "no")` | `"yes"` (requires Extensions 2, 3) |
| `(if #f (omega omega) 42)` | `42` — the divergent branch is not evaluated |
---
-## Extension 6 — Array literals
+## Extension 7 — Array literals
**Difficulty: 3 / 5**
@@ -215,7 +289,7 @@ A divergent or error-raising expression in the dead branch must not be evaluated
Add `[e1 e2 … en]` syntax for constructing array values. The square-bracket delimiters
are currently unused by the parser. Arrays are *eager*: all elements are evaluated at
construction time, consistent with CBV. Structural operations (`head`, `tail`, `cons`,
-`nth`, `length`, `map`) follow the same partial-application builtin pattern as Extension 3.
+`nth`, `length`, `map`) follow the same partial-application builtin pattern as Extension 2.
### What changes
@@ -246,11 +320,11 @@ new test here).
| `(tail [10 20 30])` | `[20 30]` |
| `(cons 0 [1 2])` | `[0 1 2]` |
| `(nth 1 [a b c])` | `b` |
-| `(map (lam (x) (+ x 1)) [1 2 3])` | `[2 3 4]` (requires Extension 3) |
+| `(map (lam (x) (+ x 1)) [1 2 3])` | `[2 3 4]` (requires Extension 2) |
---
-## Extension 7 — `quote` and `eval`
+## Extension 8 — `quote` and `eval`
**Difficulty: 3 / 5**
@@ -265,7 +339,7 @@ distinction between *object language* and *meta-language* becomes observable in
The hard part is `(eval e)`: the `eval` builtin must receive the current *environment* as a
hidden argument. This means the builtin dispatch mechanism must be extended to thread the
environment through, which is a non-trivial change to the builtin application architecture
-introduced in Extension 3.
+introduced in Extension 2.
### What changes
@@ -291,11 +365,11 @@ introduced in Extension 3.
| `(quote (lam (x) x))` | `(lam (x) x)` (syntax as data) |
| `(eval (quote (lam (x) x)))` | `(lam (x) x)` (re-evaluated) |
| `(eval (quote ((lam (x) x) y)))` | `y` |
-| `(let ((e (quote (+ 1 2)))) (eval e))` | `3` (requires Extension 3) |
+| `(let ((e (quote (+ 1 2)))) (eval e))` | `3` (requires Extension 2) |
---
-## Extension 8 — `letrec` / recursive definitions
+## Extension 9 — `letrec` / recursive definitions
**Difficulty: 3 / 5**
@@ -303,7 +377,7 @@ introduced in Extension 3.
The current `let` desugars to an immediate lambda application, which means bindings cannot
refer to themselves or each other. `letrec` allows mutually recursive definitions. Under
-CBV this requires either mutable thunks (see Extension 9) or lazy tying-of-the-knot in the
+CBV this requires either mutable thunks (see Extension 10) or lazy tying-of-the-knot in the
Haskell host. The standard interpreter-level approach is to build a *circular environment*:
the closure created for each binding has the entire `letrec` group already in its
environment, which in a lazy host language like Haskell can be expressed directly using
@@ -328,12 +402,12 @@ environment, which in a lazy host language like Haskell can be expressed directl
| Expression | Result |
|---|---|
-| `(letrec ((f (lam (x) (if (= x 0) 1 (* x (f (- x 1))))))) (f 5))` | `120` (requires Extensions 3, 5) |
+| `(letrec ((f (lam (x) (if (= x 0) 1 (* x (f (- x 1))))))) (f 5))` | `120` (requires Extensions 2, 6) |
| `(letrec ((even? (lam (n) (if (= n 0) #t (odd? (- n 1))))) (odd? (lam (n) (if (= n 0) #f (even? (- n 1)))))) (even? 4))` | `#t` |
---
-## Extension 9 — Mutable references
+## Extension 10 — Mutable references
**Difficulty: 3 / 5**
@@ -366,12 +440,12 @@ updated to thread the store.
|---|---|
| `(let ((r (ref 0))) (deref r))` | `0` |
| `(let ((r (ref 0))) (set! r 42) (deref r))` | `42` |
-| `(let ((r (ref 0))) (set! r (+ (deref r) 1)) (deref r))` | `1` (requires Extension 3) |
+| `(let ((r (ref 0))) (set! r (+ (deref r) 1)) (deref r))` | `1` (requires Extension 2) |
| Show that argument evaluation order is observable when both arguments mutate shared state | order-dependent result |
---
-## Extension 10 — Type annotations
+## Extension 11 — Type annotations
**Difficulty: 3 / 5**
@@ -413,7 +487,7 @@ the inferred type with the declared type and reporting mismatches.
---
-## Extension 11 — Algebraic data types
+## Extension 12 — Algebraic data types
**Difficulty: 4 / 5**
@@ -460,7 +534,7 @@ of the type system (sum types, product types, type constructors, arity checking)
---
-## Extension 12 — `call/cc`
+## Extension 13 — `call/cc`
**Difficulty: 4 / 5**
@@ -505,7 +579,7 @@ to supply an initial continuation (typically `id`).
---
-## Extension 13 — Delimited continuations (`reset` / `shift`)
+## Extension 14 — Delimited continuations (`reset` / `shift`)
**Difficulty: 5 / 5**
@@ -528,7 +602,7 @@ maintained as a mutable data structure threaded through `eval`.
### What changes
**`Minilang.Lambda.Eval`**
-- Prerequisite: Extension 12 (CPS'd evaluator).
+- Prerequisite: Extension 13 (CPS'd evaluator).
- Change `eval` to carry *two* continuations: the current meta-continuation and the
delimited one, or use an explicit stack of `Frame | Reset` markers.
- `reset expr`: evaluate `expr` with a fresh inner continuation and the current outer