Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .cursor/rules/specify-rules.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Auto-generated from all feature plans. Last updated: 2025-12-19
- N/A - In-memory data structures only (005-keywords-maps-sets)
- Haskell (GHC 9.10.3, base >=4.18 && <5) + gram-hs library (pattern, subject, gram packages), Cabal build system (006-gram-hs-migration)
- N/A (in-memory pattern structures) (006-gram-hs-migration)
- Haskell (GHC 9.10.3, base >=4.18 && <5) + gram-hs library (gram, pattern, subject packages), Megaparsec (parsing), Data.Map.Strict (record representation), Cabal build system (007-inline-record-notation)
- N/A (in-memory record structures) (007-inline-record-notation)

- Haskell with GHC 9.6.3 (see research.md for version selection rationale) + gram-hs (source repository from GitHub), text, containers, megaparsec, mtl, hspec, QuickCheck (001-pattern-lisp-init)

Expand All @@ -31,9 +33,9 @@ tests/
Haskell with GHC 9.6.3 (see research.md for version selection rationale): Follow standard conventions

## Recent Changes
- 007-inline-record-notation: Added Haskell (GHC 9.10.3, base >=4.18 && <5) + gram-hs library (gram, pattern, subject packages), Megaparsec (parsing), Data.Map.Strict (record representation), Cabal build system
- 006-gram-hs-migration: Added Haskell (GHC 9.10.3, base >=4.18 && <5) + gram-hs library (pattern, subject, gram packages), Cabal build system
- 005-keywords-maps-sets: Added Haskell (GHC 9.10.3)
- 004-implementation-consistency-review: Added Haskell (GHC 9.10.3), Cabal build system + gram-hs (source repository), text, containers, megaparsec, mtl, hspec, QuickCheck


<!-- MANUAL ADDITIONS START -->
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This project provides a tiny, well-specified Lisp evaluator that serves as a ref
**Minimal Core Language**
- Small expression set: `lambda`, `if`, `let`, `define`, `quote`, `begin`
- Basic primitives: arithmetic, comparisons, string operations
- Data structures: records (key-value), arrays, sets
- No I/O in core - host capabilities exposed through explicit boundary
- Tail-recursive, expression-only evaluation (Scheme-ish)
- Purely functional semantics with immutable environments
Expand Down
35 changes: 26 additions & 9 deletions app/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -9,36 +9,53 @@
import System.IO
import System.Environment
import System.Exit
import qualified Data.Text as T

Check warning on line 12 in app/Main.hs

View workflow job for this annotation

GitHub Actions / build-and-test (9.10.3, 3.10)

The qualified import of ‘Data.Text’ is redundant
import qualified Data.Map as Map
import Data.List (isPrefixOf, isSuffixOf, partition, elemIndex, sortOn)
import qualified Data.Set as Set
import Data.List (isPrefixOf, isSuffixOf, partition, elemIndex, sortOn, intercalate)

Check warning on line 15 in app/Main.hs

View workflow job for this annotation

GitHub Actions / build-and-test (9.10.3, 3.10)

The import of ‘isPrefixOf, partition’
import Data.Maybe (maybe)

Check warning on line 16 in app/Main.hs

View workflow job for this annotation

GitHub Actions / build-and-test (9.10.3, 3.10)

The import of ‘Data.Maybe’ is redundant
import Control.Applicative ((<|>))

-- | Format a Value for display
formatValue :: Value -> String
formatValue (VNumber n) = show n
formatValue (VString s) = T.unpack s
formatValue (VBool True) = "#t"
formatValue (VBool False) = "#f"
formatValue (VList vals) = "(" ++ unwords (map formatValue vals) ++ ")"
formatValue (VInteger n) = show n
formatValue (VString s) = "\"" ++ s ++ "\""
formatValue (VBoolean True) = "true"
formatValue (VBoolean False) = "false"
formatValue (VArray vals) = "(" ++ unwords (map formatValue vals) ++ ")"
formatValue (VMap m) = "{" ++ intercalate ", " (map (\(k, v) -> k ++ ": " ++ formatValue v) (Map.toList m)) ++ "}"
formatValue (VPattern _) = "<pattern>"
formatValue (VClosure _) = "<closure>"
formatValue (VPrimitive _) = "<primitive>"
formatValue (VKeyword k) = k ++ ":"
formatValue (VSet s) = "#{" ++ unwords (map formatValue (Set.toList s)) ++ "}"
formatValue (VDecimal d) = show d
formatValue (VSymbol s) = s
formatValue (VTaggedString tag val) = tag ++ ":" ++ val
formatValue (VRange _) = "<range>"
formatValue (VMeasurement unit val) = show val ++ " " ++ unit

-- | Format a Value type for display in variable listing
formatValueType :: Value -> String
formatValueType (VNumber _) = "Number"
formatValueType (VInteger _) = "Number"
formatValueType (VString _) = "String"
formatValueType (VBool _) = "Bool"
formatValueType (VList _) = "List"
formatValueType (VBoolean _) = "Bool"
formatValueType (VArray _) = "List"
formatValueType (VMap _) = "Record"
formatValueType (VPattern _) = "Pattern"
formatValueType (VClosure (Closure params _ _)) = "Closure(" ++ unwords params ++ ")"

Check warning on line 46 in app/Main.hs

View workflow job for this annotation

GitHub Actions / build-and-test (9.10.3, 3.10)

This binding for ‘params’ shadows the existing binding
formatValueType (VPrimitive _) = "Primitive"
formatValueType (VKeyword _) = "Keyword"
formatValueType (VSet _) = "Set"
formatValueType (VDecimal _) = "Decimal"
formatValueType (VSymbol _) = "Symbol"
formatValueType (VTaggedString _ _) = "TaggedString"
formatValueType (VRange _) = "Range"
formatValueType (VMeasurement _ _) = "Measurement"

-- | Format environment variables for display
formatEnv :: Env -> String
formatEnv env =

Check warning on line 58 in app/Main.hs

View workflow job for this annotation

GitHub Actions / build-and-test (9.10.3, 3.10)

This binding for ‘env’ shadows the existing binding
if Map.null env
then "No variables in scope"
else unlines $ map formatBinding $ sortByVarName $ Map.toList env
Expand Down Expand Up @@ -111,7 +128,7 @@

-- | Process a single REPL line
processLine :: String -> Env -> IO (Env, Bool)
processLine input env

Check warning on line 131 in app/Main.hs

View workflow job for this annotation

GitHub Actions / build-and-test (9.10.3, 3.10)

This binding for ‘env’ shadows the existing binding
| input == ":quit" || input == ":q" = return (env, False)
| input == ":help" || input == ":h" = do
putStrLn "Pattern Lisp REPL"
Expand Down Expand Up @@ -141,7 +158,7 @@

-- | Main REPL loop
repl :: Env -> IO ()
repl env = do

Check warning on line 161 in app/Main.hs

View workflow job for this annotation

GitHub Actions / build-and-test (9.10.3, 3.10)

This binding for ‘env’ shadows the existing binding
putStr "> "
hFlush stdout
eof <- isEOF
Expand All @@ -158,7 +175,7 @@
executeWithEval :: [String] -> String -> IO ()
executeWithEval allArgs exprStr = do
-- Separate files from flags
let (files, flags) = parseArgs allArgs

Check warning on line 178 in app/Main.hs

View workflow job for this annotation

GitHub Actions / build-and-test (9.10.3, 3.10)

Defined but not used: ‘flags’

-- Load files
envResult <- processFiles files initialEnv
Expand All @@ -166,7 +183,7 @@
Left err -> do
hPutStrLn stderr (formatError err)
exitFailure
Right env -> do

Check warning on line 186 in app/Main.hs

View workflow job for this annotation

GitHub Actions / build-and-test (9.10.3, 3.10)

This binding for ‘env’ shadows the existing binding
-- Parse and evaluate expression
case parseExpr exprStr of
Left parseErr -> do
Expand All @@ -191,7 +208,7 @@
Left err -> do
hPutStrLn stderr (formatError err)
exitFailure
Right env -> do

Check warning on line 211 in app/Main.hs

View workflow job for this annotation

GitHub Actions / build-and-test (9.10.3, 3.10)

This binding for ‘env’ shadows the existing binding
-- Get plisp files in order
let plispFiles = filter isPlisp files
case plispFiles of
Expand Down
294 changes: 294 additions & 0 deletions docs/pattern-lisp-records.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
# Pattern Lisp Records

**Last Updated**: 2025-01-30

Records are immutable key-value structures in Pattern Lisp, using gram-compatible notation. Records replace the previous map syntax and provide a complete set of operations for data manipulation.

## Syntax

Records use curly braces with comma-separated key-value pairs:

```lisp
{name: "Alice", age: 30} ;; Basic record
{user: {name: "Bob", email: "bob@example.com"}} ;; Nested record
{} ;; Empty record
```

### Key Syntax

Keys can be:
- **Identifiers**: `{name: "Alice"}` - automatically converted to strings
- **Quoted strings**: `{"user-id": 123}` - useful for keys with special characters
- **Mixed**: `{name: "Alice", "user-id": 123}` - both styles in one record

Both single (`:`) and double (`::`) colons are supported for gram compatibility:
- `{name: "Alice"}` - single colon
- `{name:: "Alice"}` - double colon (gram-compatible)

### Value Types

Record values can be any Pattern Lisp value:
- Numbers: `{count: 42}`
- Strings: `{name: "Alice"}`
- Booleans: `{active: true}`
- Arrays: `{tags: ["admin", "user"]}`
- Nested records: `{user: {name: "Bob"}}`
- Any other Pattern Lisp value

## Basic Operations

### Type Checking

```lisp
(record? {name: "Alice"}) ;; => true
(record? "not a record") ;; => false
```

### Accessing Values

```lisp
(get {name: "Alice", age: 30} "name") ;; => "Alice"
(get {name: "Alice"} "email" "unknown") ;; => "unknown" (default value)
(get {name: "Alice"} "email") ;; => () (nil if no default)
```

### Checking for Keys

```lisp
(has? {name: "Alice", age: 30} "name") ;; => true
(has? {name: "Alice", age: 30} "email") ;; => false
```

### Getting Keys and Values

```lisp
(keys {name: "Alice", age: 30}) ;; => ("name" "age")
(values {name: "Alice", age: 30}) ;; => ("Alice" 30)
```

## Modification Operations

All modification operations return **new records** - original records are immutable.

### Adding/Updating Keys

```lisp
(assoc {name: "Alice"} "age" 30) ;; => {name: "Alice", age: 30}
(assoc {name: "Alice", age: 30} "age" 31) ;; => {name: "Alice", age: 31}
```

### Removing Keys

```lisp
(dissoc {name: "Alice", age: 30} "age") ;; => {name: "Alice"}
```

### Merging Records

```lisp
(merge {name: "Alice", age: 30} {age: 31, city: "NYC"})
;; => {name: "Alice", age: 31, city: "NYC"}
;; Note: Right record takes precedence
```

## Transformation Operations

### Mapping Over Records

```lisp
(map (lambda (k v) (* v 2)) {a: 1, b: 2, c: 3})
;; => {a: 2, b: 4, c: 6}
```

The mapping function receives two arguments: the key (as a string) and the value.

### Filtering Records

```lisp
(filter (lambda (k v) (> v 2)) {a: 1, b: 2, c: 3, d: 4})
;; => {c: 3, d: 4}
```

The filter function receives two arguments (key, value) and must return a boolean.

## Conversion Operations

### Record to Association List

```lisp
(record->alist {name: "Alice", age: 30})
;; => (("name" "Alice") ("age" 30))
```

### Association List to Record

```lisp
(alist->record (("name" "Alice") ("age" 30)))
;; => {name: "Alice", age: 30}
```

## Programmatic Creation

### Using the `record` Constructor

```lisp
(record "name" "Alice" "age" 30)
;; => {name: "Alice", age: 30}
```

Takes alternating key-value pairs. Requires an even number of arguments.

## Quasiquotation

Records support quasiquotation for dynamic construction:

### Unquoting Values

```lisp
(let ((name "Alice")
(age 30))
`{name: ,name, age: ,age})
;; => {name: "Alice", age: 30}
```

### Splicing Records

```lisp
(let ((base {role: "Engineer"})
(extra {name: "Alice", age: 30}))
`{,@base, ,@extra})
;; => {role: "Engineer", name: "Alice", age: 30}
```

### Combined Unquoting and Splicing

```lisp
(let ((name "Alice")
(metadata {age: 30, city: "NYC"}))
`{name: ,name, ,@metadata})
;; => {name: "Alice", age: 30, city: "NYC"}
```

## Nested Records

Records can be nested to any depth:

```lisp
{user: {name: "Bob", email: "bob@example.com"},
config: {debug: true, verbose: false}}
```

Access nested values:

```lisp
(get (get {user: {name: "Bob"}} "user") "name") ;; => "Bob"
```

## Real-World Examples

### User Profile

```lisp
{id: 1,
name: "Alice",
email: "alice@example.com",
roles: ["admin", "user"],
settings: {theme: "dark", notifications: true}}
```

### Configuration Object

```lisp
{server: {host: "localhost", port: 8080},
database: {host: "db.example.com", port: 5432},
features: {logging: true, caching: false}}
```

### Pattern Subject Representation

```lisp
{identity: "person-123",
labels: ["Person", "Employee"],
properties: {name: "Bob", department: "Engineering"}}
```

## Gram Syntax Compatibility

Records in Pattern Lisp are **100% gram-compatible**. You can copy records from `.gram` files into `.plisp` files with no changes:

```gram
{name: "Alice", age: 30, tags: ["admin", "user"]}
```

This exact syntax works in both gram and Pattern Lisp.

### Supported Gram Features

- ✅ Comma-separated key-value pairs
- ✅ Both single (`:`) and double (`::`) colons
- ✅ String keys (quoted)
- ✅ Identifier keys (unquoted)
- ✅ Nested records
- ✅ Arrays as values: `[1, 2, 3]`
- ✅ All gram value types (integers, decimals, strings, booleans, arrays, nested records)

### Pattern-Lisp Specific Features

These features work in Pattern Lisp but are not part of gram notation:
- **Unquotes**: `,expr` - evaluate expression dynamically
- **Splices**: `,@expr` - merge records dynamically

When records with these features are serialized to gram, the unquotes and splices are evaluated first, so the resulting gram contains only pure values.

## Complete Operation Reference

### Type Checking

| Function | Signature | Description |
|---------|-----------|-------------|
| `record?` | `(record? value)` | Returns `true` if value is a record |

### Access Operations

| Function | Signature | Description |
|---------|-----------|-------------|
| `get` | `(get record key [default])` | Get value by key, with optional default |
| `has?` | `(has? record key)` | Check if key exists |
| `keys` | `(keys record)` | Get all keys as array |
| `values` | `(values record)` | Get all values as array |

### Modification Operations

| Function | Signature | Description |
|---------|-----------|-------------|
| `assoc` | `(assoc record key value)` | Add/update key (returns new record) |
| `dissoc` | `(dissoc record key)` | Remove key (returns new record) |
| `merge` | `(merge record1 record2)` | Merge two records (right takes precedence) |

### Transformation Operations

| Function | Signature | Description |
|---------|-----------|-------------|
| `map` | `(map fn record)` | Apply function to each (key, value) pair |
| `filter` | `(filter pred record)` | Keep entries where predicate returns `true` |

### Conversion Operations

| Function | Signature | Description |
|---------|-----------|-------------|
| `record->alist` | `(record->alist record)` | Convert to association list |
| `alist->record` | `(alist->record pairs)` | Convert from association list |

### Constructor

| Function | Signature | Description |
|---------|-----------|-------------|
| `record` | `(record key1 val1 key2 val2 ...)` | Create record from key-value pairs |

## Notes

- **Immutability**: All operations return new records. Original records are never modified.
- **Key Order**: Keys maintain their insertion order (first to last).
- **Duplicate Keys**: If a record literal has duplicate keys, the last value wins.
- **Empty Records**: `{}` is a valid record with no entries.
- **Type**: Records are represented as `VMap (Map String Value)` internally, using gram's `Subject.Value.VMap` type directly.
Loading
Loading