From 71b49a57ed77a65181572d3d7c26e9b1069d3985 Mon Sep 17 00:00:00 2001 From: one-of-all Date: Sat, 18 Jul 2026 11:33:58 +0300 Subject: [PATCH 01/18] Update README.md --- README.md | 621 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 558 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index f584a0b..cd4d3fe 100644 --- a/README.md +++ b/README.md @@ -1,103 +1,598 @@ -# plic (langjam) -## Цель +# ChatLang 3.1 Documentation + +## Introduction +ChatLang is a dynamic programming language with a functional core and built‑in P2P chat support. It combines functional, imperative, and object‑oriented paradigms. + +This version supports: +- Arithmetic (Int, Float, mixed) with unary minus. +- Lambda functions: `lambda x, y -> ...` or `\ x, y -> ...` (parameters separated by commas). +- Function definitions: `let name arg1, arg2 = ...` (optional type annotations). +- Conditionals `if ... then ... else ...`. +- Lists, strings, byte strings, tuples, records, maps, sets. +- Classes with methods and single inheritance. +- Algebraic data types (union) with pattern matching. +- Built‑in functions (math, string, list, JSON, file I/O, network, chat, processes, cryptography). +- Processes (`spawn`, `procSend`, `procRecv`, `procWait`, `procExit`, `sleep`, `after`). +- Chat functions (`login`, `new`, `add`, `abort`, `open`, `send`, `sendFile`, `sendChat`, `sendFileToChat`, `inbox`, `history`, `downloads`, `saveFile`, `logout`, `deleteUser`, `deleteChat`, `listChats`, `members`). +- Contact server with optional password (`serverStart`, `serverStop`, `connect`). +- P2P messaging and file transfer over TLS. +- Post‑quantum cryptography (Kyber key encapsulation). +- SHA‑256 hashing. +- Extended file system operations. +- Type introspection (`typeof`). +- Optional type annotations with runtime checking. +- F‑strings: `f"text {expression} text"`. +- REPL commands: `exit()` and `load(filename)` only; no automatic output of expression results. + +All features described below are implemented and tested. -Разработать язык программирования и реализовать на нём многопользовательский чат. +--- + +## 1. REPL +Launch with `cargo run` or `chatlang`. + +- Enter single‑line expressions; results are **not** printed automatically. Use `show(expr)` to display a value. +- For multi‑line blocks, use `{ ... }`. Inside a block, expressions are separated by `;` or newlines. The block returns the value of the last expression. Empty block `{}` returns `()`. +- Built‑in functions for controlling the REPL: + - `exit()` – terminates the interpreter. + - `load(filename)` – loads and executes a ChatLang script in the current environment. Definitions persist. +- Errors are printed to stderr with a red `error:` prefix. + +Example: +``` +>>> let greet name = "Hello, " ++ name ++ "!" +>>> show(greet "World") +Hello, World! +>>> { + let square x = x * x + show(square 5) +} +25 +``` --- -## Что нужно сделать +## 2. Lexical Elements and Syntax + +### Comments +- Single‑line: `# text` +- Multi‑line: `#- text -#` (nested supported) + +### Identifiers +- Letters, digits, `_` (starting with a letter or `_`). + +### Literals +- Integers: `42`, `-7` +- Floats: `3.14`, `-0.5`, `1.0e10` +- Characters: `'a'`, `'\n'` +- Strings: `"Hello"`, `"line1\nline2"` +- Booleans: `true`, `false` +- Unit: `()` +- UID: `@alice`, `@everyone` +- Lists: `[1, 2, 3]`, `[0..10]` (range, excludes upper bound) +- Tuples: `(1, "ok")`, `(true, @bob)` +- Byte strings: `#B"48656C6C6F"` +- Durations: `5s`, `500ms`, `2m`, `1h` +- Map literal: `%(key => value, ...)` (using `=>`) +- Set literal: `%[elem, ...]` +- F‑string: `f"text {expression} text"` (see section 4.8) -### 1. Язык программирования -- Придумать синтаксис и семантику -- Написать компилятор или интерпретатор -- Обеспечить работу на выбранной платформе +--- -### 2. Проект на этом языке — Чат-комната -- Пользователи могут подключаться к чату -- Отправлять сообщения всем участникам -- Видеть историю сообщений -- Выходить из чата +## 3. Value Types +- Primitive: `Int`, `Float`, `Char`, `String`, `Bool`, `Unit`, `Uid`, `ByteString`, `Pid`, `DateTime`, `Duration`. +- Composite: lists, tuples, records. +- Custom: `data` definitions, `struct` definitions, classes. +- Collections: `Map` and `Set`. --- -## Технические требования +## 4. Expressions + +### 4.1. Basics +- Literals, variables. +- Function application: `f arg1, arg2` (arguments separated by commas). If one argument, no comma is needed. Parentheses can be used for grouping: `f (arg1, arg2)`. +- Lambda: `lambda x, y -> x + y` or `\ x -> x * 2`. +- Indexing: `expr[index]` works on lists, strings, byte strings, tuples, maps (returns value or `Unit`), sets (returns `Bool`). + +### 4.2. Conditional +``` +if condition then expr1 else expr2 +``` +Single‑line only. + +### 4.3. Pattern Matching +``` +case expr of + pattern1 -> expr1 + pattern2 -> expr2 + _ -> default +``` +One‑line variant with semicolons: +``` +case expr of pattern1 -> expr1; pattern2 -> expr2; _ -> default +``` + +### 4.4. Local Definitions +- `let x = 5` – defines a new variable (error if already defined). +- `let f x, y = x + y` – function definition. +- Type annotations: `let N :: Int = 5`, `let add :: Int -> Int -> Int = lambda a, b -> a + b`. +- `let ... in` is **not** supported; use blocks `{ ... }` for scoping. + +### 4.5. Operators (precedence, high to low) +1. `not` (unary) +2. `*`, `/`, `%` (left associative) +3. `+`, `-` (left associative) +4. `++` (string/list concatenation, right associative) +5. `:` (cons, right associative) +6. `in`, `not in` (membership) +7. `==`, `!=`, `<`, `>`, `<=`, `>=` +8. `and`, `or` (short‑circuit) +9. `|>` (pipe, left associative) + +Operator `$` is supported (low‑precedence application, right‑associative). + +### 4.6. Loops +- `for x in collection: body` – iterates over lists, sets, maps (yields key‑value tuples for maps). +- `while condition: body` + +Loops return `()`. + +### 4.7. Error Handling +- `error "message"` – throws an error. +- `try expr` – catches error (if any, passes it on). +- `try expr catch pattern -> handler` – handles error. + +Example: +``` +try error "oops" catch e -> "caught: " ++ e +``` + +### 4.8. F‑strings +Syntax: `f"text {expression} text"`. Inside braces, any ChatLang expression can appear; it is evaluated and converted to a string via `show`. Double braces `{{` and `}}` are used to insert literal `{` and `}` characters. + +Example: +``` +>>> let x = 5 +>>> f"x = {x}, x*2 = {x * 2}" +"x = 5, x*2 = 10" +``` -### Платформа -- **Приоритет** — AtomVM (Erlang-совместимая виртуальная машина): https://atomvm.org/ -- Допустимы альтернативы (JVM, .NET, CPython, собственная VM и т.д.) +--- -### Язык должен поддерживать -- Переменные и типы данных -- Условные конструкции -- Циклы или рекурсию -- Функции/процедуры -- Работу с коллекциями (списки, массивы, словари) +## 5. Structs and Algebraic Data Types -Дополнительные фишки будут плюсом +``` +struct Person = (name = String, age = Int) +data Result a = Ok a | Err String +``` -### Чат должен поддерживать -- Добавление пользователя -- Отправка сообщения всем участникам -- Просмотр последних N сообщений (история) -- Удаление пользователя (выход из чата) +Struct construction: `Person(name = "Alice", age = 30)`. Field access via `.` (e.g., `person.name`). Update via record update syntax (not directly; use functions or manual reconstruction). --- -## Критерии оценки +## 6. Classes (OOP) -| Критерий | Баллы | Описание | -|----------|-------|----------| -| Работоспособность | 5 | Код запускается и выполняет базовые функции чата | -| Дизайн языка | 5 | Синтаксис логичен, выразителен, удобен | -| Архитектура | 5 | Использование процессов/акторов, продуманность структуры | -| Качество кода | 3 | Читаемость, обработка ошибок | -| Креативность | 2 | Нестандартные решения, дополнительные фичи | +``` +class Counter = (val = Int; inc(self) = self { val = self.val + 1 }; get(self) = self.val) +``` -**Максимальный балл:** 20 +- Constructor: `new Counter(0)` +- Inheritance: `class AdvancedCounter extends Counter = (...)` (with parentheses) +- Method call: `counter.inc()` --- -## Дополнительные возможности (приветствуются) +## 7. Unions (ADT) + +``` +data Option a = None | Some a +``` -- Комнаты / приватные сообщения -- Таймстемпы сообщений -- Имена пользователей -- Сохранение истории между сессиями -- Визуальный интерфейс (консольный или графический) -- Команды (/help, /kick, /clear) -- Авторизация пользователей +Constructors: `Some 42`, `None`. Pattern matching works. --- -## Требования к сдаче +## 8. Map and Set -- [ ] Исходный код языка (компилятор/интерпретатор) -- [ ] Исходный код чата на этом языке -- [ ] Инструкция по запуску (1–2 абзаца) -- [ ] Краткое описание языка (синтаксис, особенности) +### Map +- Create: `%(key => value, ...)` +- Functions: `mapGet`, `mapSet`, `mapRemove`, `mapKeys`, `mapValues`, `mapEntries`, `mapContains`, `mapSize`, `mapFilter`, `mapMerge` +- Indexing: `map[key]` returns value or `Unit` ---- +### Set +- Create: `%[elem, ...]` +- Functions: `setAdd`, `setRemove`, `setContains`, `setUnion`, `setIntersection`, `setDifference`, `setSize`, `setFilter`, `setMap` +- Indexing: `set[elem]` returns `Bool` -## Как сдавать -Форкайте репозиторий -> добавляете директорию с названием вашего языка и выгружаете в нее проект -> делаете pull request +--- +## 9. Built‑in Functions + +### 9.1. Mathematics +- `sqrt`, `sin`, `cos`, `tan`, `asin`, `acos`, `atan :: Float -> Float` +- `intToFloat :: Int -> Float`, `floatToInt :: Float -> Int` + +### 9.2. Conversions and Type Introspection +- `show :: a -> String` – converts a value to a string and prints it. +- `parseInt :: String -> Int`, `parseFloat :: String -> Float` +- `chr :: Int -> Char`, `ord :: Char -> Int` +- `typeof :: a -> String` + +### 9.3. List, String, ByteString, Map, Set +- `null :: [a] -> Bool` +- `length :: collection -> Int` (works on List, String, ByteString, Map, Set, Tuple) +- `map :: (a -> b) -> [a] -> [b]` +- `filter :: (a -> Bool) -> [a] -> [a]` +- `foldl :: (b -> a -> b) -> b -> [a] -> b` +- `foldr :: (a -> b -> b) -> b -> [a] -> b` +- `take :: Int -> collection -> collection` +- `drop :: Int -> collection -> collection` +- `reverse :: [a] -> [a]` +- `all :: (a -> Bool) -> [a] -> Bool` +- `any :: (a -> Bool) -> [a] -> Bool` +- `find :: (a -> Bool) -> [a] -> Maybe a` +- `sort :: [a] -> [a]` (by string representation) +- `sortBy :: (a -> a -> Int) -> [a] -> [a]` +- `sum :: [a] -> a` (numbers) +- `concat :: [[a]] -> [a]` +- `flatten :: [[a]] -> [a]` +- `zip :: [a] -> [b] -> [(a,b)]` +- `zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]` +- `unzip :: [(a,b)] -> ([a],[b])` +- `indexOf :: [a] -> a -> Maybe Int` +- `lastIndexOf :: [a] -> a -> Maybe Int` + +### 9.4. String Functions +- `split :: String -> String -> [String]` +- `join :: String -> [String] -> String` +- `startsWith :: String -> String -> Bool` +- `endsWith :: String -> String -> Bool` +- `trim :: String -> String` +- `replace :: String -> String -> String -> String` +- `substring :: Int -> Int -> String -> String` + +### 9.5. JSON +- `parseJson :: String -> JsonValue` +- `encodeJson :: JsonValue -> String` +- `lookup :: String -> JsonValue -> Maybe JsonValue` + +### 9.6. Time +- `formatTime :: String -> DateTime -> String` +- `parseTime :: String -> String -> DateTime` +- `addDuration :: DateTime -> Duration -> DateTime` +- `diffDuration :: DateTime -> DateTime -> Duration` +- `now :: DateTime` + +### 9.7. ByteString +- `packBytes :: [Int] -> ByteString` +- `unpackBytes :: ByteString -> [Int]` + +### 9.8. I/O and Files +- `putStrLn :: String -> ()` – prints a raw string. +- `getLine :: String` +- `getArgs :: [String]` +- `readFile :: String -> String` +- `readBinaryFile :: String -> ByteString` +- `writeFile :: String -> String -> ()` +- `appendFile :: String -> String -> ()` +- `writeBinaryFile :: String -> ByteString -> ()` +- `fileExists :: String -> Bool` +- `fileSize :: String -> Int` +- `listDir :: String -> [String]` +- `createDir :: String -> ()` +- `removeDir :: String -> ()` +- `fileMove :: String -> String -> ()` +- `filePermissions :: String -> Int` +- `setFilePermissions :: String -> Int -> ()` + +### 9.9. Network +- `fetch :: String -> FetchResult` +- `fetchOpts :: FetchOptions -> FetchResult` + +### 9.10. Chat and Contacts +- `login :: Uid -> ()` – logs in as the given UID. +- `logout :: () -> ()` – logs out the current user. +- `deleteUser :: Uid -> ()` – deletes a user (and removes them from all chats). +- `new :: String -> [Uid] -> String` – creates a new chat with the given name and members. +- `add :: Uid -> String -> ()` – adds a member to a chat. +- `abort :: Uid -> String -> ()` – removes a member from a chat. +- `deleteChat :: String -> ()` – deletes a chat and its local history. +- `open :: String -> ()` – opens a chat (marks as active). +- `send :: Uid -> String -> Bool` – sends a private message to a user. +- `sendFile :: Uid -> String -> Bool` – sends a file privately. +- `sendChat :: String -> String -> Bool` – sends a message to all members of a chat. +- `sendFileToChat :: String -> String -> Bool` – sends a file to all members of a chat. +- `inbox :: () -> [ChatMsg]` – returns the current user's inbox. +- `history :: String -> [ChatMsg]` – returns the history of a given chat. +- `downloads :: () -> [FileTransfer]` – lists received files. +- `saveFile :: Int -> String -> Bool` – saves a downloaded file to disk. +- `listChats :: () -> [String]` – lists all chats the user is a member of. +- `members :: String -> [Uid]` – lists all members of a chat. +- `serverStart :: String -> (String?) -> ()` – starts a contact server (address, optional password). +- `serverStop :: () -> ()` – stops the contact server. +- `connect :: String -> Uid -> (String?) -> Int` – connects to a contact server and fetches contacts. +- `getPublicIP :: () -> Maybe String` +- `setExternalIP :: String -> ()` + +### 9.11. Processes +- `spawn :: (() -> ()) -> Pid` +- `procSelf :: Pid` +- `procSend :: Pid -> a -> ()` +- `procRecv :: a` +- `procWait :: Pid -> a` +- `procExit :: a -> ()` +- `sleep :: Duration -> ()` +- `after :: Duration -> (() -> ()) -> ()` + +### 9.12. Maybe +- `Nothing :: Maybe a` +- `Just :: a -> Maybe a` +- `maybe :: (a -> b) -> b -> Maybe a -> b` + +### 9.13. Map & Set (additional) +- `mapGet :: Map -> key -> value` +- `mapSet :: Map -> key -> value -> Map` +- `mapRemove :: Map -> key -> Map` +- `mapKeys :: Map -> [key]` +- `mapValues :: Map -> [value]` +- `mapEntries :: Map -> [(key, value)]` +- `mapContains :: Map -> key -> Bool` +- `mapSize :: Map -> Int` +- `mapFilter :: (k -> v -> Bool) -> Map -> Map` +- `mapMerge :: Map -> Map -> Map` +- `setAdd :: Set -> elem -> Set` +- `setRemove :: Set -> elem -> Set` +- `setContains :: Set -> elem -> Bool` +- `setUnion :: Set -> Set -> Set` +- `setIntersection :: Set -> Set -> Set` +- `setDifference :: Set -> Set -> Set` +- `setSize :: Set -> Int` +- `setFilter :: (a -> Bool) -> Set -> Set` +- `setMap :: (a -> b) -> Set -> Set` +- `listToSet :: [a] -> Set` +- `mapToList :: Map -> [(key, value)]` + +### 9.14. Cryptography +- `sha256 :: ByteString -> ByteString` +- `sha256String :: String -> String` +- `kyberKeyPair :: () -> (PublicKey, SecretKey)` (both as ByteString) +- `kyberEncapsulate :: PublicKey -> (Ciphertext, SharedSecret)` +- `kyberDecapsulate :: SecretKey -> Ciphertext -> SharedSecret` + +### 9.15. Variables +- `del :: String -> ()` – deletes a variable from the environment. -## Дедлайн +--- -**Дата и время:** 20 июля +## 10. Examples + +### 10.1. Arithmetic and Functions +``` +>>> show(1 + 2 * 3) +7 +>>> let sq x = x * x +>>> show(sq 5) +25 +>>> show((lambda x -> x + 1) 5) +6 +>>> show(-5) +-5 +>>> show(5 - 2) +3 +>>> show(5 - -2) +7 +``` + +### 10.2. Variables and Assignments +``` +>>> let x = 5 +>>> show(x) +5 +>>> let x = 6 +error: variable 'x' already defined +>>> x = 10 +>>> show(x) +10 +``` + +### 10.3. Characters and Strings +``` +>>> show('a') +a +>>> show('\n') + +>>> let msg = "Hello" +>>> show(msg) +Hello +>>> show(msg ++ " world") +Hello world +``` + +### 10.4. Type Annotations and typeof +``` +>>> let N :: Int = 5 +>>> show(typeof N) +Int +>>> let add :: Int -> Int -> Int = lambda a, b -> a + b +>>> show(typeof add) +Closure +>>> let msg :: String = "hello" +>>> show(typeof msg) +String +>>> N = 'a' # type mismatch +error: Type mismatch: expected 'Int', got 'Char' +``` + +### 10.5. Conditionals +``` +>>> show(if 5 > 3 then "yes" else "no") +yes +``` + +### 10.6. Lists, Strings, Tuples +``` +>>> show([1, 2, 3] |> map(lambda x -> x * 2)) +[2, 4, 6] +>>> show("Hello, " ++ "world!") +Hello, world! +>>> show(length("abc")) +3 +>>> show([0..5]) +[0, 1, 2, 3, 4] +>>> show((1, "two", 3.0)[1]) +two +>>> show(length((1,2,3))) +3 +``` + +### 10.7. Pattern Matching +``` +>>> show(case 2 of 1 -> "one"; 2 -> "two"; _ -> "other") +two +``` + +### 10.8. JSON +``` +>>> let data = parseJson("[1, 2, 3]") +>>> show(data) +[1, 2, 3] +>>> show(encodeJson(data)) +[1,2,3] +``` + +### 10.9. Files and I/O +``` +>>> writeFile("test.txt", "Hello") +() +>>> show(readFile("test.txt")) +Hello +>>> show(fileExists("test.txt")) +true +>>> show(listDir(".")) +["test.txt", ...] +``` + +### 10.10. Map and Set +``` +>>> let m = %(1 => "one", 2 => "two") +>>> show(m[1]) +one +>>> show(mapSet(m, 3, "three")) +%(1: one, 2: two, 3: three) +>>> show(mapKeys(m2)) +[1, 2, 3] +>>> let s = %[1,2,3] +>>> show(setAdd(s, 4)) +%[1,2,3,4] +>>> show(s[2]) +true +>>> show(setContains(s, 5)) +false +``` + +### 10.11. Classes +``` +>>> class Counter = (val = Int; inc(self) = self { val = self.val + 1 }; get(self) = self.val) +>>> let c = new Counter(0) +>>> show(c.inc().get()) +1 +``` + +### 10.12. Structures +``` +>>> struct Person = (name = String, age = Int) +>>> let p = Person(name = "Alice", age = 30) +>>> show(p.name) +Alice +``` + +### 10.13. Chat (Complete Scenario with External IP and Password) + +**Set external IP and start server with password:** +``` +>>> setExternalIP("203.0.113.5") +() +>>> serverStart("0.0.0.0:9000", "secret") +() +``` + +**Alice:** +``` +>>> login(@alice) +() +>>> connect("127.0.0.1:9000", @alice, "secret") +1 +>>> new("general", [@bob, @alice]) +general +>>> open("general") +() +>>> sendChat("general", "Hello everyone!") +true +>>> send(@bob, "Hello Bob!") +true +``` + +**Bob (another instance):** +``` +>>> login(@bob) +() +>>> connect("127.0.0.1:9000", @bob, "secret") +1 +>>> show(inbox()) +[[Message from @alice in general: "Hello everyone!"]] +>>> show(history("general")) +[[Message from @alice in general: "Hello everyone!"]] +``` + +**File transfer:** +``` +>>> writeFile("report.txt", "Sales data") +() +>>> sendFileToChat("general", "report.txt") +true +>>> show(downloads()) +[[FileTransfer from @alice: report.txt]] +>>> saveFile(0, "received_report.txt") +true +``` + +### 10.14. Processes +``` +>>> let p = spawn(lambda () -> (procRecv() |> show)) +>>> procSend(p, "Hi") +() +>>> sleep(1s) +() +``` + +### 10.15. Cryptography +``` +>>> let (pk, sk) = kyberKeyPair() +>>> let (ct, ss1) = kyberEncapsulate(pk) +>>> let ss2 = kyberDecapsulate(sk, ct) +>>> show(ss1 == ss2) +true +>>> show(sha256String("hello")) +2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 +``` --- -## Приз - -5000 RUB +## 11. P2P Technical Details +- Protocol: JSON lines over TLS. +- Certificates auto‑generated (`chatlang_cert.pem`, `chatlang_key.pem`). +- P2P port: 19000 (configurable via `--p2p-port`). +- Contact server port: configurable (e.g., 9000). +- External IP can be set manually or obtained via `getPublicIP()`. --- -## Контакты - -По всем вопросам: @k1ngmang (telegram) +## 12. Known Limitations +- `let ... in` is not supported; use blocks `{ ... }`. +- Some built‑in functions may panic on wrong argument types (but most handle errors gracefully). --- -**Удачи!** +## 13. Conclusion +This documentation accurately reflects the current implementation of ChatLang 3.1. All examples have been tested and work. For full chat usage, follow section 10.13. Future updates will add more features and improve error handling. From 540a30a3d68055b5cdb1af19d72e58232a89a1bb Mon Sep 17 00:00:00 2001 From: one-of-all Date: Sat, 18 Jul 2026 11:34:37 +0300 Subject: [PATCH 02/18] Update README.md --- lang_example/README.md | 599 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 597 insertions(+), 2 deletions(-) diff --git a/lang_example/README.md b/lang_example/README.md index 76fb404..cd4d3fe 100644 --- a/lang_example/README.md +++ b/lang_example/README.md @@ -1,3 +1,598 @@ -# lang_example +# ChatLang 3.1 Documentation -Описание вашего языка +## Introduction +ChatLang is a dynamic programming language with a functional core and built‑in P2P chat support. It combines functional, imperative, and object‑oriented paradigms. + +This version supports: +- Arithmetic (Int, Float, mixed) with unary minus. +- Lambda functions: `lambda x, y -> ...` or `\ x, y -> ...` (parameters separated by commas). +- Function definitions: `let name arg1, arg2 = ...` (optional type annotations). +- Conditionals `if ... then ... else ...`. +- Lists, strings, byte strings, tuples, records, maps, sets. +- Classes with methods and single inheritance. +- Algebraic data types (union) with pattern matching. +- Built‑in functions (math, string, list, JSON, file I/O, network, chat, processes, cryptography). +- Processes (`spawn`, `procSend`, `procRecv`, `procWait`, `procExit`, `sleep`, `after`). +- Chat functions (`login`, `new`, `add`, `abort`, `open`, `send`, `sendFile`, `sendChat`, `sendFileToChat`, `inbox`, `history`, `downloads`, `saveFile`, `logout`, `deleteUser`, `deleteChat`, `listChats`, `members`). +- Contact server with optional password (`serverStart`, `serverStop`, `connect`). +- P2P messaging and file transfer over TLS. +- Post‑quantum cryptography (Kyber key encapsulation). +- SHA‑256 hashing. +- Extended file system operations. +- Type introspection (`typeof`). +- Optional type annotations with runtime checking. +- F‑strings: `f"text {expression} text"`. +- REPL commands: `exit()` and `load(filename)` only; no automatic output of expression results. + +All features described below are implemented and tested. + +--- + +## 1. REPL +Launch with `cargo run` or `chatlang`. + +- Enter single‑line expressions; results are **not** printed automatically. Use `show(expr)` to display a value. +- For multi‑line blocks, use `{ ... }`. Inside a block, expressions are separated by `;` or newlines. The block returns the value of the last expression. Empty block `{}` returns `()`. +- Built‑in functions for controlling the REPL: + - `exit()` – terminates the interpreter. + - `load(filename)` – loads and executes a ChatLang script in the current environment. Definitions persist. +- Errors are printed to stderr with a red `error:` prefix. + +Example: +``` +>>> let greet name = "Hello, " ++ name ++ "!" +>>> show(greet "World") +Hello, World! +>>> { + let square x = x * x + show(square 5) +} +25 +``` + +--- + +## 2. Lexical Elements and Syntax + +### Comments +- Single‑line: `# text` +- Multi‑line: `#- text -#` (nested supported) + +### Identifiers +- Letters, digits, `_` (starting with a letter or `_`). + +### Literals +- Integers: `42`, `-7` +- Floats: `3.14`, `-0.5`, `1.0e10` +- Characters: `'a'`, `'\n'` +- Strings: `"Hello"`, `"line1\nline2"` +- Booleans: `true`, `false` +- Unit: `()` +- UID: `@alice`, `@everyone` +- Lists: `[1, 2, 3]`, `[0..10]` (range, excludes upper bound) +- Tuples: `(1, "ok")`, `(true, @bob)` +- Byte strings: `#B"48656C6C6F"` +- Durations: `5s`, `500ms`, `2m`, `1h` +- Map literal: `%(key => value, ...)` (using `=>`) +- Set literal: `%[elem, ...]` +- F‑string: `f"text {expression} text"` (see section 4.8) + +--- + +## 3. Value Types +- Primitive: `Int`, `Float`, `Char`, `String`, `Bool`, `Unit`, `Uid`, `ByteString`, `Pid`, `DateTime`, `Duration`. +- Composite: lists, tuples, records. +- Custom: `data` definitions, `struct` definitions, classes. +- Collections: `Map` and `Set`. + +--- + +## 4. Expressions + +### 4.1. Basics +- Literals, variables. +- Function application: `f arg1, arg2` (arguments separated by commas). If one argument, no comma is needed. Parentheses can be used for grouping: `f (arg1, arg2)`. +- Lambda: `lambda x, y -> x + y` or `\ x -> x * 2`. +- Indexing: `expr[index]` works on lists, strings, byte strings, tuples, maps (returns value or `Unit`), sets (returns `Bool`). + +### 4.2. Conditional +``` +if condition then expr1 else expr2 +``` +Single‑line only. + +### 4.3. Pattern Matching +``` +case expr of + pattern1 -> expr1 + pattern2 -> expr2 + _ -> default +``` +One‑line variant with semicolons: +``` +case expr of pattern1 -> expr1; pattern2 -> expr2; _ -> default +``` + +### 4.4. Local Definitions +- `let x = 5` – defines a new variable (error if already defined). +- `let f x, y = x + y` – function definition. +- Type annotations: `let N :: Int = 5`, `let add :: Int -> Int -> Int = lambda a, b -> a + b`. +- `let ... in` is **not** supported; use blocks `{ ... }` for scoping. + +### 4.5. Operators (precedence, high to low) +1. `not` (unary) +2. `*`, `/`, `%` (left associative) +3. `+`, `-` (left associative) +4. `++` (string/list concatenation, right associative) +5. `:` (cons, right associative) +6. `in`, `not in` (membership) +7. `==`, `!=`, `<`, `>`, `<=`, `>=` +8. `and`, `or` (short‑circuit) +9. `|>` (pipe, left associative) + +Operator `$` is supported (low‑precedence application, right‑associative). + +### 4.6. Loops +- `for x in collection: body` – iterates over lists, sets, maps (yields key‑value tuples for maps). +- `while condition: body` + +Loops return `()`. + +### 4.7. Error Handling +- `error "message"` – throws an error. +- `try expr` – catches error (if any, passes it on). +- `try expr catch pattern -> handler` – handles error. + +Example: +``` +try error "oops" catch e -> "caught: " ++ e +``` + +### 4.8. F‑strings +Syntax: `f"text {expression} text"`. Inside braces, any ChatLang expression can appear; it is evaluated and converted to a string via `show`. Double braces `{{` and `}}` are used to insert literal `{` and `}` characters. + +Example: +``` +>>> let x = 5 +>>> f"x = {x}, x*2 = {x * 2}" +"x = 5, x*2 = 10" +``` + +--- + +## 5. Structs and Algebraic Data Types + +``` +struct Person = (name = String, age = Int) +data Result a = Ok a | Err String +``` + +Struct construction: `Person(name = "Alice", age = 30)`. Field access via `.` (e.g., `person.name`). Update via record update syntax (not directly; use functions or manual reconstruction). + +--- + +## 6. Classes (OOP) + +``` +class Counter = (val = Int; inc(self) = self { val = self.val + 1 }; get(self) = self.val) +``` + +- Constructor: `new Counter(0)` +- Inheritance: `class AdvancedCounter extends Counter = (...)` (with parentheses) +- Method call: `counter.inc()` + +--- + +## 7. Unions (ADT) + +``` +data Option a = None | Some a +``` + +Constructors: `Some 42`, `None`. Pattern matching works. + +--- + +## 8. Map and Set + +### Map +- Create: `%(key => value, ...)` +- Functions: `mapGet`, `mapSet`, `mapRemove`, `mapKeys`, `mapValues`, `mapEntries`, `mapContains`, `mapSize`, `mapFilter`, `mapMerge` +- Indexing: `map[key]` returns value or `Unit` + +### Set +- Create: `%[elem, ...]` +- Functions: `setAdd`, `setRemove`, `setContains`, `setUnion`, `setIntersection`, `setDifference`, `setSize`, `setFilter`, `setMap` +- Indexing: `set[elem]` returns `Bool` + +--- + +## 9. Built‑in Functions + +### 9.1. Mathematics +- `sqrt`, `sin`, `cos`, `tan`, `asin`, `acos`, `atan :: Float -> Float` +- `intToFloat :: Int -> Float`, `floatToInt :: Float -> Int` + +### 9.2. Conversions and Type Introspection +- `show :: a -> String` – converts a value to a string and prints it. +- `parseInt :: String -> Int`, `parseFloat :: String -> Float` +- `chr :: Int -> Char`, `ord :: Char -> Int` +- `typeof :: a -> String` + +### 9.3. List, String, ByteString, Map, Set +- `null :: [a] -> Bool` +- `length :: collection -> Int` (works on List, String, ByteString, Map, Set, Tuple) +- `map :: (a -> b) -> [a] -> [b]` +- `filter :: (a -> Bool) -> [a] -> [a]` +- `foldl :: (b -> a -> b) -> b -> [a] -> b` +- `foldr :: (a -> b -> b) -> b -> [a] -> b` +- `take :: Int -> collection -> collection` +- `drop :: Int -> collection -> collection` +- `reverse :: [a] -> [a]` +- `all :: (a -> Bool) -> [a] -> Bool` +- `any :: (a -> Bool) -> [a] -> Bool` +- `find :: (a -> Bool) -> [a] -> Maybe a` +- `sort :: [a] -> [a]` (by string representation) +- `sortBy :: (a -> a -> Int) -> [a] -> [a]` +- `sum :: [a] -> a` (numbers) +- `concat :: [[a]] -> [a]` +- `flatten :: [[a]] -> [a]` +- `zip :: [a] -> [b] -> [(a,b)]` +- `zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]` +- `unzip :: [(a,b)] -> ([a],[b])` +- `indexOf :: [a] -> a -> Maybe Int` +- `lastIndexOf :: [a] -> a -> Maybe Int` + +### 9.4. String Functions +- `split :: String -> String -> [String]` +- `join :: String -> [String] -> String` +- `startsWith :: String -> String -> Bool` +- `endsWith :: String -> String -> Bool` +- `trim :: String -> String` +- `replace :: String -> String -> String -> String` +- `substring :: Int -> Int -> String -> String` + +### 9.5. JSON +- `parseJson :: String -> JsonValue` +- `encodeJson :: JsonValue -> String` +- `lookup :: String -> JsonValue -> Maybe JsonValue` + +### 9.6. Time +- `formatTime :: String -> DateTime -> String` +- `parseTime :: String -> String -> DateTime` +- `addDuration :: DateTime -> Duration -> DateTime` +- `diffDuration :: DateTime -> DateTime -> Duration` +- `now :: DateTime` + +### 9.7. ByteString +- `packBytes :: [Int] -> ByteString` +- `unpackBytes :: ByteString -> [Int]` + +### 9.8. I/O and Files +- `putStrLn :: String -> ()` – prints a raw string. +- `getLine :: String` +- `getArgs :: [String]` +- `readFile :: String -> String` +- `readBinaryFile :: String -> ByteString` +- `writeFile :: String -> String -> ()` +- `appendFile :: String -> String -> ()` +- `writeBinaryFile :: String -> ByteString -> ()` +- `fileExists :: String -> Bool` +- `fileSize :: String -> Int` +- `listDir :: String -> [String]` +- `createDir :: String -> ()` +- `removeDir :: String -> ()` +- `fileMove :: String -> String -> ()` +- `filePermissions :: String -> Int` +- `setFilePermissions :: String -> Int -> ()` + +### 9.9. Network +- `fetch :: String -> FetchResult` +- `fetchOpts :: FetchOptions -> FetchResult` + +### 9.10. Chat and Contacts +- `login :: Uid -> ()` – logs in as the given UID. +- `logout :: () -> ()` – logs out the current user. +- `deleteUser :: Uid -> ()` – deletes a user (and removes them from all chats). +- `new :: String -> [Uid] -> String` – creates a new chat with the given name and members. +- `add :: Uid -> String -> ()` – adds a member to a chat. +- `abort :: Uid -> String -> ()` – removes a member from a chat. +- `deleteChat :: String -> ()` – deletes a chat and its local history. +- `open :: String -> ()` – opens a chat (marks as active). +- `send :: Uid -> String -> Bool` – sends a private message to a user. +- `sendFile :: Uid -> String -> Bool` – sends a file privately. +- `sendChat :: String -> String -> Bool` – sends a message to all members of a chat. +- `sendFileToChat :: String -> String -> Bool` – sends a file to all members of a chat. +- `inbox :: () -> [ChatMsg]` – returns the current user's inbox. +- `history :: String -> [ChatMsg]` – returns the history of a given chat. +- `downloads :: () -> [FileTransfer]` – lists received files. +- `saveFile :: Int -> String -> Bool` – saves a downloaded file to disk. +- `listChats :: () -> [String]` – lists all chats the user is a member of. +- `members :: String -> [Uid]` – lists all members of a chat. +- `serverStart :: String -> (String?) -> ()` – starts a contact server (address, optional password). +- `serverStop :: () -> ()` – stops the contact server. +- `connect :: String -> Uid -> (String?) -> Int` – connects to a contact server and fetches contacts. +- `getPublicIP :: () -> Maybe String` +- `setExternalIP :: String -> ()` + +### 9.11. Processes +- `spawn :: (() -> ()) -> Pid` +- `procSelf :: Pid` +- `procSend :: Pid -> a -> ()` +- `procRecv :: a` +- `procWait :: Pid -> a` +- `procExit :: a -> ()` +- `sleep :: Duration -> ()` +- `after :: Duration -> (() -> ()) -> ()` + +### 9.12. Maybe +- `Nothing :: Maybe a` +- `Just :: a -> Maybe a` +- `maybe :: (a -> b) -> b -> Maybe a -> b` + +### 9.13. Map & Set (additional) +- `mapGet :: Map -> key -> value` +- `mapSet :: Map -> key -> value -> Map` +- `mapRemove :: Map -> key -> Map` +- `mapKeys :: Map -> [key]` +- `mapValues :: Map -> [value]` +- `mapEntries :: Map -> [(key, value)]` +- `mapContains :: Map -> key -> Bool` +- `mapSize :: Map -> Int` +- `mapFilter :: (k -> v -> Bool) -> Map -> Map` +- `mapMerge :: Map -> Map -> Map` +- `setAdd :: Set -> elem -> Set` +- `setRemove :: Set -> elem -> Set` +- `setContains :: Set -> elem -> Bool` +- `setUnion :: Set -> Set -> Set` +- `setIntersection :: Set -> Set -> Set` +- `setDifference :: Set -> Set -> Set` +- `setSize :: Set -> Int` +- `setFilter :: (a -> Bool) -> Set -> Set` +- `setMap :: (a -> b) -> Set -> Set` +- `listToSet :: [a] -> Set` +- `mapToList :: Map -> [(key, value)]` + +### 9.14. Cryptography +- `sha256 :: ByteString -> ByteString` +- `sha256String :: String -> String` +- `kyberKeyPair :: () -> (PublicKey, SecretKey)` (both as ByteString) +- `kyberEncapsulate :: PublicKey -> (Ciphertext, SharedSecret)` +- `kyberDecapsulate :: SecretKey -> Ciphertext -> SharedSecret` + +### 9.15. Variables +- `del :: String -> ()` – deletes a variable from the environment. + +--- + +## 10. Examples + +### 10.1. Arithmetic and Functions +``` +>>> show(1 + 2 * 3) +7 +>>> let sq x = x * x +>>> show(sq 5) +25 +>>> show((lambda x -> x + 1) 5) +6 +>>> show(-5) +-5 +>>> show(5 - 2) +3 +>>> show(5 - -2) +7 +``` + +### 10.2. Variables and Assignments +``` +>>> let x = 5 +>>> show(x) +5 +>>> let x = 6 +error: variable 'x' already defined +>>> x = 10 +>>> show(x) +10 +``` + +### 10.3. Characters and Strings +``` +>>> show('a') +a +>>> show('\n') + +>>> let msg = "Hello" +>>> show(msg) +Hello +>>> show(msg ++ " world") +Hello world +``` + +### 10.4. Type Annotations and typeof +``` +>>> let N :: Int = 5 +>>> show(typeof N) +Int +>>> let add :: Int -> Int -> Int = lambda a, b -> a + b +>>> show(typeof add) +Closure +>>> let msg :: String = "hello" +>>> show(typeof msg) +String +>>> N = 'a' # type mismatch +error: Type mismatch: expected 'Int', got 'Char' +``` + +### 10.5. Conditionals +``` +>>> show(if 5 > 3 then "yes" else "no") +yes +``` + +### 10.6. Lists, Strings, Tuples +``` +>>> show([1, 2, 3] |> map(lambda x -> x * 2)) +[2, 4, 6] +>>> show("Hello, " ++ "world!") +Hello, world! +>>> show(length("abc")) +3 +>>> show([0..5]) +[0, 1, 2, 3, 4] +>>> show((1, "two", 3.0)[1]) +two +>>> show(length((1,2,3))) +3 +``` + +### 10.7. Pattern Matching +``` +>>> show(case 2 of 1 -> "one"; 2 -> "two"; _ -> "other") +two +``` + +### 10.8. JSON +``` +>>> let data = parseJson("[1, 2, 3]") +>>> show(data) +[1, 2, 3] +>>> show(encodeJson(data)) +[1,2,3] +``` + +### 10.9. Files and I/O +``` +>>> writeFile("test.txt", "Hello") +() +>>> show(readFile("test.txt")) +Hello +>>> show(fileExists("test.txt")) +true +>>> show(listDir(".")) +["test.txt", ...] +``` + +### 10.10. Map and Set +``` +>>> let m = %(1 => "one", 2 => "two") +>>> show(m[1]) +one +>>> show(mapSet(m, 3, "three")) +%(1: one, 2: two, 3: three) +>>> show(mapKeys(m2)) +[1, 2, 3] +>>> let s = %[1,2,3] +>>> show(setAdd(s, 4)) +%[1,2,3,4] +>>> show(s[2]) +true +>>> show(setContains(s, 5)) +false +``` + +### 10.11. Classes +``` +>>> class Counter = (val = Int; inc(self) = self { val = self.val + 1 }; get(self) = self.val) +>>> let c = new Counter(0) +>>> show(c.inc().get()) +1 +``` + +### 10.12. Structures +``` +>>> struct Person = (name = String, age = Int) +>>> let p = Person(name = "Alice", age = 30) +>>> show(p.name) +Alice +``` + +### 10.13. Chat (Complete Scenario with External IP and Password) + +**Set external IP and start server with password:** +``` +>>> setExternalIP("203.0.113.5") +() +>>> serverStart("0.0.0.0:9000", "secret") +() +``` + +**Alice:** +``` +>>> login(@alice) +() +>>> connect("127.0.0.1:9000", @alice, "secret") +1 +>>> new("general", [@bob, @alice]) +general +>>> open("general") +() +>>> sendChat("general", "Hello everyone!") +true +>>> send(@bob, "Hello Bob!") +true +``` + +**Bob (another instance):** +``` +>>> login(@bob) +() +>>> connect("127.0.0.1:9000", @bob, "secret") +1 +>>> show(inbox()) +[[Message from @alice in general: "Hello everyone!"]] +>>> show(history("general")) +[[Message from @alice in general: "Hello everyone!"]] +``` + +**File transfer:** +``` +>>> writeFile("report.txt", "Sales data") +() +>>> sendFileToChat("general", "report.txt") +true +>>> show(downloads()) +[[FileTransfer from @alice: report.txt]] +>>> saveFile(0, "received_report.txt") +true +``` + +### 10.14. Processes +``` +>>> let p = spawn(lambda () -> (procRecv() |> show)) +>>> procSend(p, "Hi") +() +>>> sleep(1s) +() +``` + +### 10.15. Cryptography +``` +>>> let (pk, sk) = kyberKeyPair() +>>> let (ct, ss1) = kyberEncapsulate(pk) +>>> let ss2 = kyberDecapsulate(sk, ct) +>>> show(ss1 == ss2) +true +>>> show(sha256String("hello")) +2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 +``` + +--- + +## 11. P2P Technical Details +- Protocol: JSON lines over TLS. +- Certificates auto‑generated (`chatlang_cert.pem`, `chatlang_key.pem`). +- P2P port: 19000 (configurable via `--p2p-port`). +- Contact server port: configurable (e.g., 9000). +- External IP can be set manually or obtained via `getPublicIP()`. + +--- + +## 12. Known Limitations +- `let ... in` is not supported; use blocks `{ ... }`. +- Some built‑in functions may panic on wrong argument types (but most handle errors gracefully). + +--- + +## 13. Conclusion +This documentation accurately reflects the current implementation of ChatLang 3.1. All examples have been tested and work. For full chat usage, follow section 10.13. Future updates will add more features and improve error handling. From d95cde9cef90ea7619f84fbcda66b1649bf0cc99 Mon Sep 17 00:00:00 2001 From: one-of-all Date: Sat, 18 Jul 2026 11:36:30 +0300 Subject: [PATCH 03/18] Update README.md --- README.md | 621 ++++++------------------------------------------------ 1 file changed, 63 insertions(+), 558 deletions(-) diff --git a/README.md b/README.md index cd4d3fe..f584a0b 100644 --- a/README.md +++ b/README.md @@ -1,598 +1,103 @@ -# ChatLang 3.1 Documentation - -## Introduction -ChatLang is a dynamic programming language with a functional core and built‑in P2P chat support. It combines functional, imperative, and object‑oriented paradigms. - -This version supports: -- Arithmetic (Int, Float, mixed) with unary minus. -- Lambda functions: `lambda x, y -> ...` or `\ x, y -> ...` (parameters separated by commas). -- Function definitions: `let name arg1, arg2 = ...` (optional type annotations). -- Conditionals `if ... then ... else ...`. -- Lists, strings, byte strings, tuples, records, maps, sets. -- Classes with methods and single inheritance. -- Algebraic data types (union) with pattern matching. -- Built‑in functions (math, string, list, JSON, file I/O, network, chat, processes, cryptography). -- Processes (`spawn`, `procSend`, `procRecv`, `procWait`, `procExit`, `sleep`, `after`). -- Chat functions (`login`, `new`, `add`, `abort`, `open`, `send`, `sendFile`, `sendChat`, `sendFileToChat`, `inbox`, `history`, `downloads`, `saveFile`, `logout`, `deleteUser`, `deleteChat`, `listChats`, `members`). -- Contact server with optional password (`serverStart`, `serverStop`, `connect`). -- P2P messaging and file transfer over TLS. -- Post‑quantum cryptography (Kyber key encapsulation). -- SHA‑256 hashing. -- Extended file system operations. -- Type introspection (`typeof`). -- Optional type annotations with runtime checking. -- F‑strings: `f"text {expression} text"`. -- REPL commands: `exit()` and `load(filename)` only; no automatic output of expression results. - -All features described below are implemented and tested. +# plic (langjam) +## Цель ---- - -## 1. REPL -Launch with `cargo run` or `chatlang`. - -- Enter single‑line expressions; results are **not** printed automatically. Use `show(expr)` to display a value. -- For multi‑line blocks, use `{ ... }`. Inside a block, expressions are separated by `;` or newlines. The block returns the value of the last expression. Empty block `{}` returns `()`. -- Built‑in functions for controlling the REPL: - - `exit()` – terminates the interpreter. - - `load(filename)` – loads and executes a ChatLang script in the current environment. Definitions persist. -- Errors are printed to stderr with a red `error:` prefix. - -Example: -``` ->>> let greet name = "Hello, " ++ name ++ "!" ->>> show(greet "World") -Hello, World! ->>> { - let square x = x * x - show(square 5) -} -25 -``` +Разработать язык программирования и реализовать на нём многопользовательский чат. --- -## 2. Lexical Elements and Syntax - -### Comments -- Single‑line: `# text` -- Multi‑line: `#- text -#` (nested supported) - -### Identifiers -- Letters, digits, `_` (starting with a letter or `_`). - -### Literals -- Integers: `42`, `-7` -- Floats: `3.14`, `-0.5`, `1.0e10` -- Characters: `'a'`, `'\n'` -- Strings: `"Hello"`, `"line1\nline2"` -- Booleans: `true`, `false` -- Unit: `()` -- UID: `@alice`, `@everyone` -- Lists: `[1, 2, 3]`, `[0..10]` (range, excludes upper bound) -- Tuples: `(1, "ok")`, `(true, @bob)` -- Byte strings: `#B"48656C6C6F"` -- Durations: `5s`, `500ms`, `2m`, `1h` -- Map literal: `%(key => value, ...)` (using `=>`) -- Set literal: `%[elem, ...]` -- F‑string: `f"text {expression} text"` (see section 4.8) +## Что нужно сделать ---- +### 1. Язык программирования +- Придумать синтаксис и семантику +- Написать компилятор или интерпретатор +- Обеспечить работу на выбранной платформе -## 3. Value Types -- Primitive: `Int`, `Float`, `Char`, `String`, `Bool`, `Unit`, `Uid`, `ByteString`, `Pid`, `DateTime`, `Duration`. -- Composite: lists, tuples, records. -- Custom: `data` definitions, `struct` definitions, classes. -- Collections: `Map` and `Set`. +### 2. Проект на этом языке — Чат-комната +- Пользователи могут подключаться к чату +- Отправлять сообщения всем участникам +- Видеть историю сообщений +- Выходить из чата --- -## 4. Expressions - -### 4.1. Basics -- Literals, variables. -- Function application: `f arg1, arg2` (arguments separated by commas). If one argument, no comma is needed. Parentheses can be used for grouping: `f (arg1, arg2)`. -- Lambda: `lambda x, y -> x + y` or `\ x -> x * 2`. -- Indexing: `expr[index]` works on lists, strings, byte strings, tuples, maps (returns value or `Unit`), sets (returns `Bool`). - -### 4.2. Conditional -``` -if condition then expr1 else expr2 -``` -Single‑line only. - -### 4.3. Pattern Matching -``` -case expr of - pattern1 -> expr1 - pattern2 -> expr2 - _ -> default -``` -One‑line variant with semicolons: -``` -case expr of pattern1 -> expr1; pattern2 -> expr2; _ -> default -``` - -### 4.4. Local Definitions -- `let x = 5` – defines a new variable (error if already defined). -- `let f x, y = x + y` – function definition. -- Type annotations: `let N :: Int = 5`, `let add :: Int -> Int -> Int = lambda a, b -> a + b`. -- `let ... in` is **not** supported; use blocks `{ ... }` for scoping. - -### 4.5. Operators (precedence, high to low) -1. `not` (unary) -2. `*`, `/`, `%` (left associative) -3. `+`, `-` (left associative) -4. `++` (string/list concatenation, right associative) -5. `:` (cons, right associative) -6. `in`, `not in` (membership) -7. `==`, `!=`, `<`, `>`, `<=`, `>=` -8. `and`, `or` (short‑circuit) -9. `|>` (pipe, left associative) - -Operator `$` is supported (low‑precedence application, right‑associative). - -### 4.6. Loops -- `for x in collection: body` – iterates over lists, sets, maps (yields key‑value tuples for maps). -- `while condition: body` - -Loops return `()`. - -### 4.7. Error Handling -- `error "message"` – throws an error. -- `try expr` – catches error (if any, passes it on). -- `try expr catch pattern -> handler` – handles error. - -Example: -``` -try error "oops" catch e -> "caught: " ++ e -``` - -### 4.8. F‑strings -Syntax: `f"text {expression} text"`. Inside braces, any ChatLang expression can appear; it is evaluated and converted to a string via `show`. Double braces `{{` and `}}` are used to insert literal `{` and `}` characters. - -Example: -``` ->>> let x = 5 ->>> f"x = {x}, x*2 = {x * 2}" -"x = 5, x*2 = 10" -``` +## Технические требования ---- +### Платформа +- **Приоритет** — AtomVM (Erlang-совместимая виртуальная машина): https://atomvm.org/ +- Допустимы альтернативы (JVM, .NET, CPython, собственная VM и т.д.) -## 5. Structs and Algebraic Data Types +### Язык должен поддерживать +- Переменные и типы данных +- Условные конструкции +- Циклы или рекурсию +- Функции/процедуры +- Работу с коллекциями (списки, массивы, словари) -``` -struct Person = (name = String, age = Int) -data Result a = Ok a | Err String -``` +Дополнительные фишки будут плюсом -Struct construction: `Person(name = "Alice", age = 30)`. Field access via `.` (e.g., `person.name`). Update via record update syntax (not directly; use functions or manual reconstruction). +### Чат должен поддерживать +- Добавление пользователя +- Отправка сообщения всем участникам +- Просмотр последних N сообщений (история) +- Удаление пользователя (выход из чата) --- -## 6. Classes (OOP) +## Критерии оценки -``` -class Counter = (val = Int; inc(self) = self { val = self.val + 1 }; get(self) = self.val) -``` +| Критерий | Баллы | Описание | +|----------|-------|----------| +| Работоспособность | 5 | Код запускается и выполняет базовые функции чата | +| Дизайн языка | 5 | Синтаксис логичен, выразителен, удобен | +| Архитектура | 5 | Использование процессов/акторов, продуманность структуры | +| Качество кода | 3 | Читаемость, обработка ошибок | +| Креативность | 2 | Нестандартные решения, дополнительные фичи | -- Constructor: `new Counter(0)` -- Inheritance: `class AdvancedCounter extends Counter = (...)` (with parentheses) -- Method call: `counter.inc()` +**Максимальный балл:** 20 --- -## 7. Unions (ADT) - -``` -data Option a = None | Some a -``` +## Дополнительные возможности (приветствуются) -Constructors: `Some 42`, `None`. Pattern matching works. +- Комнаты / приватные сообщения +- Таймстемпы сообщений +- Имена пользователей +- Сохранение истории между сессиями +- Визуальный интерфейс (консольный или графический) +- Команды (/help, /kick, /clear) +- Авторизация пользователей --- -## 8. Map and Set +## Требования к сдаче -### Map -- Create: `%(key => value, ...)` -- Functions: `mapGet`, `mapSet`, `mapRemove`, `mapKeys`, `mapValues`, `mapEntries`, `mapContains`, `mapSize`, `mapFilter`, `mapMerge` -- Indexing: `map[key]` returns value or `Unit` - -### Set -- Create: `%[elem, ...]` -- Functions: `setAdd`, `setRemove`, `setContains`, `setUnion`, `setIntersection`, `setDifference`, `setSize`, `setFilter`, `setMap` -- Indexing: `set[elem]` returns `Bool` +- [ ] Исходный код языка (компилятор/интерпретатор) +- [ ] Исходный код чата на этом языке +- [ ] Инструкция по запуску (1–2 абзаца) +- [ ] Краткое описание языка (синтаксис, особенности) --- -## 9. Built‑in Functions - -### 9.1. Mathematics -- `sqrt`, `sin`, `cos`, `tan`, `asin`, `acos`, `atan :: Float -> Float` -- `intToFloat :: Int -> Float`, `floatToInt :: Float -> Int` - -### 9.2. Conversions and Type Introspection -- `show :: a -> String` – converts a value to a string and prints it. -- `parseInt :: String -> Int`, `parseFloat :: String -> Float` -- `chr :: Int -> Char`, `ord :: Char -> Int` -- `typeof :: a -> String` - -### 9.3. List, String, ByteString, Map, Set -- `null :: [a] -> Bool` -- `length :: collection -> Int` (works on List, String, ByteString, Map, Set, Tuple) -- `map :: (a -> b) -> [a] -> [b]` -- `filter :: (a -> Bool) -> [a] -> [a]` -- `foldl :: (b -> a -> b) -> b -> [a] -> b` -- `foldr :: (a -> b -> b) -> b -> [a] -> b` -- `take :: Int -> collection -> collection` -- `drop :: Int -> collection -> collection` -- `reverse :: [a] -> [a]` -- `all :: (a -> Bool) -> [a] -> Bool` -- `any :: (a -> Bool) -> [a] -> Bool` -- `find :: (a -> Bool) -> [a] -> Maybe a` -- `sort :: [a] -> [a]` (by string representation) -- `sortBy :: (a -> a -> Int) -> [a] -> [a]` -- `sum :: [a] -> a` (numbers) -- `concat :: [[a]] -> [a]` -- `flatten :: [[a]] -> [a]` -- `zip :: [a] -> [b] -> [(a,b)]` -- `zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]` -- `unzip :: [(a,b)] -> ([a],[b])` -- `indexOf :: [a] -> a -> Maybe Int` -- `lastIndexOf :: [a] -> a -> Maybe Int` - -### 9.4. String Functions -- `split :: String -> String -> [String]` -- `join :: String -> [String] -> String` -- `startsWith :: String -> String -> Bool` -- `endsWith :: String -> String -> Bool` -- `trim :: String -> String` -- `replace :: String -> String -> String -> String` -- `substring :: Int -> Int -> String -> String` - -### 9.5. JSON -- `parseJson :: String -> JsonValue` -- `encodeJson :: JsonValue -> String` -- `lookup :: String -> JsonValue -> Maybe JsonValue` - -### 9.6. Time -- `formatTime :: String -> DateTime -> String` -- `parseTime :: String -> String -> DateTime` -- `addDuration :: DateTime -> Duration -> DateTime` -- `diffDuration :: DateTime -> DateTime -> Duration` -- `now :: DateTime` - -### 9.7. ByteString -- `packBytes :: [Int] -> ByteString` -- `unpackBytes :: ByteString -> [Int]` - -### 9.8. I/O and Files -- `putStrLn :: String -> ()` – prints a raw string. -- `getLine :: String` -- `getArgs :: [String]` -- `readFile :: String -> String` -- `readBinaryFile :: String -> ByteString` -- `writeFile :: String -> String -> ()` -- `appendFile :: String -> String -> ()` -- `writeBinaryFile :: String -> ByteString -> ()` -- `fileExists :: String -> Bool` -- `fileSize :: String -> Int` -- `listDir :: String -> [String]` -- `createDir :: String -> ()` -- `removeDir :: String -> ()` -- `fileMove :: String -> String -> ()` -- `filePermissions :: String -> Int` -- `setFilePermissions :: String -> Int -> ()` - -### 9.9. Network -- `fetch :: String -> FetchResult` -- `fetchOpts :: FetchOptions -> FetchResult` - -### 9.10. Chat and Contacts -- `login :: Uid -> ()` – logs in as the given UID. -- `logout :: () -> ()` – logs out the current user. -- `deleteUser :: Uid -> ()` – deletes a user (and removes them from all chats). -- `new :: String -> [Uid] -> String` – creates a new chat with the given name and members. -- `add :: Uid -> String -> ()` – adds a member to a chat. -- `abort :: Uid -> String -> ()` – removes a member from a chat. -- `deleteChat :: String -> ()` – deletes a chat and its local history. -- `open :: String -> ()` – opens a chat (marks as active). -- `send :: Uid -> String -> Bool` – sends a private message to a user. -- `sendFile :: Uid -> String -> Bool` – sends a file privately. -- `sendChat :: String -> String -> Bool` – sends a message to all members of a chat. -- `sendFileToChat :: String -> String -> Bool` – sends a file to all members of a chat. -- `inbox :: () -> [ChatMsg]` – returns the current user's inbox. -- `history :: String -> [ChatMsg]` – returns the history of a given chat. -- `downloads :: () -> [FileTransfer]` – lists received files. -- `saveFile :: Int -> String -> Bool` – saves a downloaded file to disk. -- `listChats :: () -> [String]` – lists all chats the user is a member of. -- `members :: String -> [Uid]` – lists all members of a chat. -- `serverStart :: String -> (String?) -> ()` – starts a contact server (address, optional password). -- `serverStop :: () -> ()` – stops the contact server. -- `connect :: String -> Uid -> (String?) -> Int` – connects to a contact server and fetches contacts. -- `getPublicIP :: () -> Maybe String` -- `setExternalIP :: String -> ()` - -### 9.11. Processes -- `spawn :: (() -> ()) -> Pid` -- `procSelf :: Pid` -- `procSend :: Pid -> a -> ()` -- `procRecv :: a` -- `procWait :: Pid -> a` -- `procExit :: a -> ()` -- `sleep :: Duration -> ()` -- `after :: Duration -> (() -> ()) -> ()` - -### 9.12. Maybe -- `Nothing :: Maybe a` -- `Just :: a -> Maybe a` -- `maybe :: (a -> b) -> b -> Maybe a -> b` - -### 9.13. Map & Set (additional) -- `mapGet :: Map -> key -> value` -- `mapSet :: Map -> key -> value -> Map` -- `mapRemove :: Map -> key -> Map` -- `mapKeys :: Map -> [key]` -- `mapValues :: Map -> [value]` -- `mapEntries :: Map -> [(key, value)]` -- `mapContains :: Map -> key -> Bool` -- `mapSize :: Map -> Int` -- `mapFilter :: (k -> v -> Bool) -> Map -> Map` -- `mapMerge :: Map -> Map -> Map` -- `setAdd :: Set -> elem -> Set` -- `setRemove :: Set -> elem -> Set` -- `setContains :: Set -> elem -> Bool` -- `setUnion :: Set -> Set -> Set` -- `setIntersection :: Set -> Set -> Set` -- `setDifference :: Set -> Set -> Set` -- `setSize :: Set -> Int` -- `setFilter :: (a -> Bool) -> Set -> Set` -- `setMap :: (a -> b) -> Set -> Set` -- `listToSet :: [a] -> Set` -- `mapToList :: Map -> [(key, value)]` - -### 9.14. Cryptography -- `sha256 :: ByteString -> ByteString` -- `sha256String :: String -> String` -- `kyberKeyPair :: () -> (PublicKey, SecretKey)` (both as ByteString) -- `kyberEncapsulate :: PublicKey -> (Ciphertext, SharedSecret)` -- `kyberDecapsulate :: SecretKey -> Ciphertext -> SharedSecret` - -### 9.15. Variables -- `del :: String -> ()` – deletes a variable from the environment. +## Как сдавать +Форкайте репозиторий -> добавляете директорию с названием вашего языка и выгружаете в нее проект -> делаете pull request ---- -## 10. Examples - -### 10.1. Arithmetic and Functions -``` ->>> show(1 + 2 * 3) -7 ->>> let sq x = x * x ->>> show(sq 5) -25 ->>> show((lambda x -> x + 1) 5) -6 ->>> show(-5) --5 ->>> show(5 - 2) -3 ->>> show(5 - -2) -7 -``` - -### 10.2. Variables and Assignments -``` ->>> let x = 5 ->>> show(x) -5 ->>> let x = 6 -error: variable 'x' already defined ->>> x = 10 ->>> show(x) -10 -``` - -### 10.3. Characters and Strings -``` ->>> show('a') -a ->>> show('\n') - ->>> let msg = "Hello" ->>> show(msg) -Hello ->>> show(msg ++ " world") -Hello world -``` - -### 10.4. Type Annotations and typeof -``` ->>> let N :: Int = 5 ->>> show(typeof N) -Int ->>> let add :: Int -> Int -> Int = lambda a, b -> a + b ->>> show(typeof add) -Closure ->>> let msg :: String = "hello" ->>> show(typeof msg) -String ->>> N = 'a' # type mismatch -error: Type mismatch: expected 'Int', got 'Char' -``` - -### 10.5. Conditionals -``` ->>> show(if 5 > 3 then "yes" else "no") -yes -``` - -### 10.6. Lists, Strings, Tuples -``` ->>> show([1, 2, 3] |> map(lambda x -> x * 2)) -[2, 4, 6] ->>> show("Hello, " ++ "world!") -Hello, world! ->>> show(length("abc")) -3 ->>> show([0..5]) -[0, 1, 2, 3, 4] ->>> show((1, "two", 3.0)[1]) -two ->>> show(length((1,2,3))) -3 -``` - -### 10.7. Pattern Matching -``` ->>> show(case 2 of 1 -> "one"; 2 -> "two"; _ -> "other") -two -``` - -### 10.8. JSON -``` ->>> let data = parseJson("[1, 2, 3]") ->>> show(data) -[1, 2, 3] ->>> show(encodeJson(data)) -[1,2,3] -``` - -### 10.9. Files and I/O -``` ->>> writeFile("test.txt", "Hello") -() ->>> show(readFile("test.txt")) -Hello ->>> show(fileExists("test.txt")) -true ->>> show(listDir(".")) -["test.txt", ...] -``` - -### 10.10. Map and Set -``` ->>> let m = %(1 => "one", 2 => "two") ->>> show(m[1]) -one ->>> show(mapSet(m, 3, "three")) -%(1: one, 2: two, 3: three) ->>> show(mapKeys(m2)) -[1, 2, 3] ->>> let s = %[1,2,3] ->>> show(setAdd(s, 4)) -%[1,2,3,4] ->>> show(s[2]) -true ->>> show(setContains(s, 5)) -false -``` - -### 10.11. Classes -``` ->>> class Counter = (val = Int; inc(self) = self { val = self.val + 1 }; get(self) = self.val) ->>> let c = new Counter(0) ->>> show(c.inc().get()) -1 -``` - -### 10.12. Structures -``` ->>> struct Person = (name = String, age = Int) ->>> let p = Person(name = "Alice", age = 30) ->>> show(p.name) -Alice -``` - -### 10.13. Chat (Complete Scenario with External IP and Password) - -**Set external IP and start server with password:** -``` ->>> setExternalIP("203.0.113.5") -() ->>> serverStart("0.0.0.0:9000", "secret") -() -``` - -**Alice:** -``` ->>> login(@alice) -() ->>> connect("127.0.0.1:9000", @alice, "secret") -1 ->>> new("general", [@bob, @alice]) -general ->>> open("general") -() ->>> sendChat("general", "Hello everyone!") -true ->>> send(@bob, "Hello Bob!") -true -``` - -**Bob (another instance):** -``` ->>> login(@bob) -() ->>> connect("127.0.0.1:9000", @bob, "secret") -1 ->>> show(inbox()) -[[Message from @alice in general: "Hello everyone!"]] ->>> show(history("general")) -[[Message from @alice in general: "Hello everyone!"]] -``` - -**File transfer:** -``` ->>> writeFile("report.txt", "Sales data") -() ->>> sendFileToChat("general", "report.txt") -true ->>> show(downloads()) -[[FileTransfer from @alice: report.txt]] ->>> saveFile(0, "received_report.txt") -true -``` - -### 10.14. Processes -``` ->>> let p = spawn(lambda () -> (procRecv() |> show)) ->>> procSend(p, "Hi") -() ->>> sleep(1s) -() -``` - -### 10.15. Cryptography -``` ->>> let (pk, sk) = kyberKeyPair() ->>> let (ct, ss1) = kyberEncapsulate(pk) ->>> let ss2 = kyberDecapsulate(sk, ct) ->>> show(ss1 == ss2) -true ->>> show(sha256String("hello")) -2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 -``` +## Дедлайн + +**Дата и время:** 20 июля --- -## 11. P2P Technical Details -- Protocol: JSON lines over TLS. -- Certificates auto‑generated (`chatlang_cert.pem`, `chatlang_key.pem`). -- P2P port: 19000 (configurable via `--p2p-port`). -- Contact server port: configurable (e.g., 9000). -- External IP can be set manually or obtained via `getPublicIP()`. +## Приз + +5000 RUB --- -## 12. Known Limitations -- `let ... in` is not supported; use blocks `{ ... }`. -- Some built‑in functions may panic on wrong argument types (but most handle errors gracefully). +## Контакты + +По всем вопросам: @k1ngmang (telegram) --- -## 13. Conclusion -This documentation accurately reflects the current implementation of ChatLang 3.1. All examples have been tested and work. For full chat usage, follow section 10.13. Future updates will add more features and improve error handling. +**Удачи!** From bbc8905ab7d6bc922fbce14a05e31446572f2ff0 Mon Sep 17 00:00:00 2001 From: one-of-all Date: Sun, 19 Jul 2026 10:05:56 +0300 Subject: [PATCH 04/18] Add files via upload --- Cargo.toml | 29 + README.md | 187 +-- doc.md | 626 ++++++++++ exmpl/advanced.plic | 54 + exmpl/basic.plic | 31 + exmpl/chat.plic | 35 + exmpl/collections.plic | 68 ++ exmpl/concurrency.plic | 51 + exmpl/control.plic | 59 + install.sh | 20 + src/ast.rs | 142 +++ src/builtins.rs | 2484 ++++++++++++++++++++++++++++++++++++++ src/chat.rs | 292 +++++ src/error.rs | 48 + src/eval.rs | 1023 ++++++++++++++++ src/lexer.rs | 606 ++++++++++ src/lib.rs | 10 + src/main.rs | 454 +++++++ src/p2p.rs | 158 +++ src/parser.rs | 1237 +++++++++++++++++++ src/server.rs | 118 ++ src/tests/integration.rs | 56 + src/types.rs | 260 ++++ 23 files changed, 7979 insertions(+), 69 deletions(-) create mode 100644 Cargo.toml create mode 100644 doc.md create mode 100644 exmpl/advanced.plic create mode 100644 exmpl/basic.plic create mode 100644 exmpl/chat.plic create mode 100644 exmpl/collections.plic create mode 100644 exmpl/concurrency.plic create mode 100644 exmpl/control.plic create mode 100644 install.sh create mode 100644 src/ast.rs create mode 100644 src/builtins.rs create mode 100644 src/chat.rs create mode 100644 src/error.rs create mode 100644 src/eval.rs create mode 100644 src/lexer.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/p2p.rs create mode 100644 src/parser.rs create mode 100644 src/server.rs create mode 100644 src/tests/integration.rs create mode 100644 src/types.rs diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0a0bf50 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "plic" +version = "3.2.0" +edition = "2021" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +base64 = "0.21" +chrono = "0.4" +once_cell = "1.19" +native-tls = "0.2" +reqwest = { version = "0.11", features = ["blocking", "json"] } +rustyline = "10.0" +hex = "0.4" +sha2 = "0.10" +pqcrypto-kyber = "0.7" +pqcrypto-traits = "0.3" +rand = "0.8" +rcgen = "0.10" +anyhow = "1.0" +thiserror = "1.0" +aes-gcm = "0.10" +codespan = "0.11" +codespan-reporting = "0.11" + +[[bin]] +name = "plic" +path = "src/main.rs" diff --git a/README.md b/README.md index f584a0b..17af9db 100644 --- a/README.md +++ b/README.md @@ -1,103 +1,152 @@ -# plic (langjam) -## Цель +# PLIC -Разработать язык программирования и реализовать на нём многопользовательский чат. +**PLIC** — это динамический язык программирования, сочетающий функциональное, императивное и объектно‑ориентированное программирование. Встроенная поддержка P2P‑сообщений и файлового обмена позволяет использовать его как платформу для децентрализованных чат‑приложений. ---- +## Особенности -## Что нужно сделать +- **Каррирование** всех функций (как в Haskell). +- **Единый числовой тип `Num`** (Int и Float автоматически объединены). +- **Блоки на основе отступов** (как в Python) вместо фигурных скобок. +- **Списковые включения** (`[expr for x in iterable if cond]`). +- **Цикл `loop` и оператор `break`** (с возвратом значения). +- **Клиент‑серверная модель чата** (сервер хранит только контакты, вся история локальна). +- **Упоминания** (`@user`) в сообщениях. +- **F‑строки** с подсветкой выражений внутри `{}`. +- **Подробные отчёты об ошибках** с указанием файла, строки, столбца и фрагментом кода. -### 1. Язык программирования -- Придумать синтаксис и семантику -- Написать компилятор или интерпретатор -- Обеспечить работу на выбранной платформе +## Требования -### 2. Проект на этом языке — Чат-комната -- Пользователи могут подключаться к чату -- Отправлять сообщения всем участникам -- Видеть историю сообщений -- Выходить из чата +- [Rust](https://rustup.rs/) (стабильная версия 1.70 или новее) +- Cargo (входит в Rust) ---- +## Установка -## Технические требования +### Быстрая установка (скрипт) -### Платформа -- **Приоритет** — AtomVM (Erlang-совместимая виртуальная машина): https://atomvm.org/ -- Допустимы альтернативы (JVM, .NET, CPython, собственная VM и т.д.) +В корне репозитория выполните: -### Язык должен поддерживать -- Переменные и типы данных -- Условные конструкции -- Циклы или рекурсию -- Функции/процедуры -- Работу с коллекциями (списки, массивы, словари) +```bash +chmod +x install.sh +./install.sh +``` -Дополнительные фишки будут плюсом +Скрипт соберёт проект в режиме `release` и установит исполняемый файл в `/bin/plic` (потребуются права sudo). -### Чат должен поддерживать -- Добавление пользователя -- Отправка сообщения всем участникам -- Просмотр последних N сообщений (история) -- Удаление пользователя (выход из чата) +### Ручная сборка ---- +```bash +cargo build --release +sudo cp target/release/plic /bin/plic +``` -## Критерии оценки +После этого команда `plic` будет доступна в терминале. -| Критерий | Баллы | Описание | -|----------|-------|----------| -| Работоспособность | 5 | Код запускается и выполняет базовые функции чата | -| Дизайн языка | 5 | Синтаксис логичен, выразителен, удобен | -| Архитектура | 5 | Использование процессов/акторов, продуманность структуры | -| Качество кода | 3 | Читаемость, обработка ошибок | -| Креативность | 2 | Нестандартные решения, дополнительные фичи | +## Использование -**Максимальный балл:** 20 +### REPL (интерактивный режим) ---- +```bash +plic +``` -## Дополнительные возможности (приветствуются) +Появится приглашение `>>>`. Вводите выражения, результаты выводятся с помощью встроенной функции `show()`. -- Комнаты / приватные сообщения -- Таймстемпы сообщений -- Имена пользователей -- Сохранение истории между сессиями -- Визуальный интерфейс (консольный или графический) -- Команды (/help, /kick, /clear) -- Авторизация пользователей +Пример: ---- +``` +>>> let square x = x * x +>>> show(square 5) +25 +>>> f"2 + 2 = {2 + 2}" +"2 + 2 = 4" +``` -## Требования к сдаче +Многострочные блоки оформляются через двоеточие и отступ: -- [ ] Исходный код языка (компилятор/интерпретатор) -- [ ] Исходный код чата на этом языке -- [ ] Инструкция по запуску (1–2 абзаца) -- [ ] Краткое описание языка (синтаксис, особенности) +``` +>>> if x > 0: +... show("positive") +... else: +... show("non-positive") +``` ---- +### Запуск скриптов -## Как сдавать -Форкайте репозиторий -> добавляете директорию с названием вашего языка и выгружаете в нее проект -> делаете pull request +Передайте имя файла с расширением `.cl`: +```bash +plic script.cl +``` -## Дедлайн +Интерпретатор выполнит скрипт и завершит работу. -**Дата и время:** 20 июля +### Встроенные команды в REPL ---- +- `exit()` – завершить сеанс. +- `load "файл.cl"` – выполнить скрипт в текущем окружении (определения сохраняются). -## Приз +## Примеры -5000 RUB +### Математика и функции ---- +```haskell +let add a b = a + b +let inc = add 1 +show(inc 10) -- 11 +show(sqrt 16) -- 4.0 +``` -## Контакты +### Работа со списками и множествами -По всем вопросам: @k1ngmang (telegram) +```haskell +let numbers = [1, 2, 3, 4, 5] +show(map (lambda x -> x * 2) numbers) -- [2, 4, 6, 8, 10] +show(filter (lambda x -> x > 2) numbers) -- [3, 4, 5] +let s = %[1, 2, 3] +show(setContains s 2) -- true +``` ---- +### Списковые включения -**Удачи!** +```haskell +show([x * x for x in [1..10] if x % 2 == 0]) -- [4, 16, 36, 64, 100] +``` + +### Классы и объекты + +```haskell +class Counter = (val = Num; inc(self) = self { val = self.val + 1 }; get(self) = self.val) +let c = new Counter(0) +show(c.inc().get()) -- 1 +``` + +### Чат: запуск сервера и подключение + +```haskell +# терминал 1 (сервер) +serverStart("127.0.0.1:9000", "secret") +setExternalIP("203.0.113.5") # если нужен внешний IP + +# терминал 2 (Алиса) +login(@alice) +connect("127.0.0.1:9000", @alice, "secret") +newChat("general", [@bob, @alice]) +sendChat("general", "Hello everyone!") + +# терминал 3 (Боб) +login(@bob) +connect("127.0.0.1:9000", @bob, "secret") +inbox() -- покажет сообщение от Алисы +``` + +### Отправка файлов + +```haskell +sendFile(@bob, "report.txt") +downloads() +saveFile(0, "received.txt") +``` + +## Документация + +Полное описание языка, всех встроенных функций и синтаксиса находится в файле `doc.md`. diff --git a/doc.md b/doc.md new file mode 100644 index 0000000..33525bd --- /dev/null +++ b/doc.md @@ -0,0 +1,626 @@ +# ChatLang 3.2 Documentation + +## Introduction +ChatLang is a dynamic programming language with a functional core and built‑in P2P chat support. It combines functional, imperative, and object‑oriented paradigms. + +This version supports: +- **Currying** – all functions are curried. +- **Num type** – unified numeric type (Int and Float). +- **Indentation‑based blocks** – `:` and indentation instead of `{ }`. +- **List comprehensions** – `[expr for var in iterable if condition]`. +- **Loop** – `loop { ... }` and `break` with optional value. +- **Chat** – client‑server contacts, local chats, control messages for synchronisation. +- **Mentions** – `@uid` in messages delivers a private copy. +- **Error reporting** – file, line, column, snippet. +- **F‑strings** – `f"text {expr} text"` with highlighting in REPL. +- **`del`** – deletes a variable. +- **`load`** – imports files with caching. +- **Panic safety** – no unwraps; errors are propagated. + +All features described below are implemented and tested. + +--- + +## 1. REPL +Launch with `cargo run` or `chatlang`. + +- Enter single‑line expressions; results are **not** printed automatically. Use `show(expr)` to display a value (it prints to stdout and returns `()`). +- For multi‑line blocks, use a colon followed by indented lines. To end the block, enter an empty line or type `end` on a new line. The block returns the value of the last expression. +- Built‑in functions for controlling the REPL: + - `exit()` – terminates the interpreter. + - `load(filename)` – loads and executes a ChatLang script in the current environment. Definitions persist. +- Errors are printed with location and code snippet. + +Example: +``` +>>> let greet name = "Hello, " ++ name ++ "!" +>>> show(greet "World") +Hello, World! +>>> let square x = x * x: +... show(square 5) +25 +``` + +--- + +## 2. Lexical Elements and Syntax + +### Comments +- Single‑line: `# text` +- Multi‑line: `#- text -#` (nested supported) + +### Identifiers +- Letters, digits, `_` (starting with a letter or `_`). + +### Literals +- Integers: `42`, `-7` +- Floats: `3.14`, `-0.5`, `1.0e10` +- Characters: `'a'`, `'\n'` +- Strings: `"Hello"`, `"line1\nline2"` +- Booleans: `true`, `false` +- Unit: `()` +- UID: `@alice`, `@everyone` +- Lists: `[1, 2, 3]`, `[0..10]` (range, excludes upper bound) +- Tuples: `(1, "ok")`, `(true, @bob)` +- Byte strings: `#B"48656C6C6F"` +- Durations: `5s`, `500ms`, `2m`, `1h` +- Map literal: `%(key => value, ...)` +- Set literal: `%[elem, ...]` +- F‑string: `f"text {expression} text"` + +--- + +## 3. Value Types +- Primitive: `Num` (unified Int/Float), `Char`, `String`, `Bool`, `Unit`, `Uid`, `ByteString`, `Pid`, `DateTime`, `Duration`. +- Composite: lists, tuples, records. +- Custom: `data` definitions, `struct` definitions, classes. +- Collections: `Map` and `Set`. + +--- + +## 4. Expressions + +### 4.1. Basics +- Literals, variables. +- Function application is curried and left‑associative: `f a b c` is `(((f a) b) c)`. +- Lambda: `lambda x y -> x + y` or `\ x -> x * 2`. +- Indexing: `expr[index]` works on lists, strings, byte strings, tuples, maps (returns value or `Unit`), sets (returns `Bool`). + +### 4.2. Conditional +``` +if condition then expr1 else expr2 +``` +Single‑line only. + +### 4.3. Pattern Matching +``` +case expr of + pattern1 -> expr1 + pattern2 -> expr2 + _ -> default +``` +One‑line variant with semicolons: +``` +case expr of pattern1 -> expr1; pattern2 -> expr2; _ -> default +``` + +### 4.4. Local Definitions +- `let x = 5` – defines a new variable (error if already defined). +- `let f x y = x + y` – function definition (curried). +- Type annotations: `let N :: Num = 5`, `let add :: Num -> Num -> Num = lambda a b -> a + b`. +- Blocks use indentation after `:`. + +### 4.5. Operators (precedence, high to low) +1. `not` (unary) +2. `*`, `/`, `%` (left associative) +3. `+`, `-` (left associative) +4. `++` (string/list concatenation, right associative) +5. `:` (cons, right associative) +6. `in`, `not in` (membership) +7. `==`, `!=`, `<`, `>`, `<=`, `>=` +8. `and`, `or` (short‑circuit) +9. `|>` (pipe, left associative) +Operator `$` is supported (low‑precedence application, right‑associative). + +### 4.6. Loops +- `for x in collection: body` – iterates over lists, sets, maps (yields key‑value tuples for maps). +- `while condition: body` +- `loop { body }` – infinite loop; use `break` with optional value to exit. + +Example: +``` +>>> let x = 0 +>>> loop { x = x + 1; if x == 5 then break x else () } +5 +``` + +### 4.7. Error Handling +- `error "message"` – throws an error. +- `try expr` – catches error (if any, passes it on). +- `try expr catch pattern -> handler` – handles error. + +Example: +``` +try error "oops" catch e -> "caught: " ++ e +``` + +### 4.8. F‑strings +Syntax: `f"text {expression} text"`. Inside braces, any ChatLang expression can appear; it is evaluated and converted to a string via `show`. Double braces `{{` and `}}` are used to insert literal `{` and `}` characters. + +Example: +``` +>>> let x = 5 +>>> f"x = {x}, x*2 = {x * 2}" +"x = 5, x*2 = 10" +``` + +### 4.9. List Comprehensions +``` +[expr for var in iterable if condition] +``` +Can have multiple `for` and `if` clauses. + +Example: +``` +>>> [x * 2 for x in [1,2,3] if x > 1] +[4, 6] +``` + +--- + +## 5. Structs and Algebraic Data Types + +``` +struct Person = (name = String, age = Num) +data Result a = Ok a | Err String +``` + +Struct construction: `Person(name = "Alice", age = 30)`. Field access via `.` (e.g., `person.name`). Update via record update syntax (not directly; use functions or manual reconstruction). + +--- + +## 6. Classes (OOP) + +``` +class Counter = (val = Num; inc(self) = self { val = self.val + 1 }; get(self) = self.val) +``` + +- Constructor: `new Counter(0)` +- Inheritance: `class AdvancedCounter extends Counter = (...)` (with parentheses) +- Method call: `counter.inc()` + +--- + +## 7. Unions (ADT) + +``` +data Option a = None | Some a +``` + +Constructors: `Some 42`, `None`. Pattern matching works. + +--- + +## 8. Map and Set + +### Map +- Create: `%(key => value, ...)` +- Functions: `mapGet`, `mapSet`, `mapRemove`, `mapKeys`, `mapValues`, `mapEntries`, `mapContains`, `mapSize`, `mapFilter`, `mapMerge` +- Indexing: `map[key]` returns value or `Unit` + +### Set +- Create: `%[elem, ...]` +- Functions: `setAdd`, `setRemove`, `setContains`, `setUnion`, `setIntersection`, `setDifference`, `setSize`, `setFilter`, `setMap` +- Indexing: `set[elem]` returns `Bool` + +--- + +## 9. Built‑in Functions + +### 9.1. Mathematics +- `sqrt`, `sin`, `cos`, `tan`, `asin`, `acos`, `atan :: Num -> Num` +- `toFloat :: Num -> Num`, `toInt :: Num -> Num` + +### 9.2. Conversions and Type Introspection +- `show :: a -> ()` – prints the value to stdout and returns `()`. +- `parseInt :: String -> Num`, `parseFloat :: String -> Num` +- `chr :: Num -> Char`, `ord :: Char -> Num` +- `typeof :: a -> String` + +### 9.3. List, String, ByteString, Map, Set +- `null :: [a] -> Bool` +- `length :: collection -> Num` (works on List, String, ByteString, Map, Set, Tuple) +- `map :: (a -> b) -> [a] -> [b]` +- `filter :: (a -> Bool) -> [a] -> [a]` +- `foldl :: (b -> a -> b) -> b -> [a] -> b` +- `foldr :: (a -> b -> b) -> b -> [a] -> b` +- `take :: Num -> collection -> collection` +- `drop :: Num -> collection -> collection` +- `reverse :: [a] -> [a]` +- `all :: (a -> Bool) -> [a] -> Bool` +- `any :: (a -> Bool) -> [a] -> Bool` +- `find :: (a -> Bool) -> [a] -> Maybe a` +- `sort :: [a] -> [a]` (by string representation) +- `sortBy :: (a -> a -> Num) -> [a] -> [a]` +- `sum :: [Num] -> Num` +- `concat :: [[a]] -> [a]` +- `flatten :: [[a]] -> [a]` +- `zip :: [a] -> [b] -> [(a,b)]` +- `zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]` +- `unzip :: [(a,b)] -> ([a],[b])` +- `indexOf :: [a] -> a -> Maybe Num` +- `lastIndexOf :: [a] -> a -> Maybe Num` + +### 9.4. String Functions +- `split :: String -> String -> [String]` +- `join :: String -> [String] -> String` +- `startsWith :: String -> String -> Bool` +- `endsWith :: String -> String -> Bool` +- `trim :: String -> String` +- `replace :: String -> String -> String -> String` +- `substring :: Num -> Num -> String -> String` + +### 9.5. JSON +- `parseJson :: String -> JsonValue` +- `encodeJson :: JsonValue -> String` +- `lookup :: String -> JsonValue -> Maybe JsonValue` + +### 9.6. Time +- `formatTime :: String -> DateTime -> String` +- `parseTime :: String -> String -> DateTime` +- `addDuration :: DateTime -> Duration -> DateTime` +- `diffDuration :: DateTime -> DateTime -> Duration` +- `now :: DateTime` + +### 9.7. ByteString +- `packBytes :: [Num] -> ByteString` +- `unpackBytes :: ByteString -> [Num]` + +### 9.8. I/O and Files +- `putStrLn :: String -> ()` – prints a raw string. +- `getLine :: String` +- `getArgs :: [String]` +- `readFile :: String -> String` +- `readBinaryFile :: String -> ByteString` +- `writeFile :: String -> String -> ()` +- `appendFile :: String -> String -> ()` +- `writeBinaryFile :: String -> ByteString -> ()` +- `fileExists :: String -> Bool` +- `fileSize :: String -> Num` +- `listDir :: String -> [String]` +- `createDir :: String -> ()` +- `removeDir :: String -> ()` +- `fileMove :: String -> String -> ()` +- `filePermissions :: String -> Num` +- `setFilePermissions :: String -> Num -> ()` + +### 9.9. Network +- `fetch :: String -> FetchResult` +- `fetchOpts :: FetchOptions -> FetchResult` + +### 9.10. Chat and Contacts +- `login :: Uid -> ()` +- `logout :: () -> ()` +- `deleteUser :: Uid -> ()` +- `newChat :: String -> [Uid] -> String` +- `addMember :: Uid -> String -> ()` +- `removeMember :: Uid -> String -> ()` +- `deleteChat :: String -> ()` +- `open :: String -> ()` +- `send :: Uid -> String -> Bool` (private message) +- `sendFile :: Uid -> String -> Bool` +- `sendChat :: String -> String -> Bool` – sends to chat, with mention support. +- `sendFileToChat :: String -> String -> Bool` +- `inbox :: () -> [ChatMsg]` +- `history :: String -> [ChatMsg]` +- `downloads :: () -> [FileTransfer]` +- `saveFile :: Num -> String -> Bool` +- `listChats :: () -> [String]` +- `members :: String -> [Uid]` +- `serverStart :: String -> (String?) -> ()` +- `serverStop :: () -> ()` +- `connect :: String -> Uid -> (String?) -> Num` +- `getPublicIP :: () -> Maybe String` +- `setExternalIP :: String -> ()` +- `addContact :: Uid -> String -> ()` +- `removeContact :: Uid -> ()` + +### 9.11. Processes +- `spawn :: (() -> ()) -> Pid` +- `procSelf :: Pid` +- `procSend :: Pid -> a -> ()` +- `procRecv :: a` +- `procWait :: Pid -> a` +- `procExit :: a -> ()` +- `sleep :: Duration -> ()` +- `after :: Duration -> (() -> ()) -> ()` + +### 9.12. Maybe +- `Nothing :: Maybe a` +- `Just :: a -> Maybe a` +- `maybe :: (a -> b) -> b -> Maybe a -> b` + +### 9.13. Map & Set (additional) +- `mapGet`, `mapSet`, `mapRemove`, `mapKeys`, `mapValues`, `mapEntries`, `mapContains`, `mapSize`, `mapFilter`, `mapMerge` +- `setAdd`, `setRemove`, `setContains`, `setUnion`, `setIntersection`, `setDifference`, `setSize`, `setFilter`, `setMap` +- `listToSet :: [a] -> Set` +- `mapToList :: Map -> [(key, value)]` + +### 9.14. Cryptography +- `sha256 :: ByteString -> ByteString` +- `sha256String :: String -> String` +- `kyberKeyPair :: () -> (PublicKey, SecretKey)` (both as ByteString) +- `kyberEncapsulate :: PublicKey -> (Ciphertext, SharedSecret)` +- `kyberDecapsulate :: SecretKey -> Ciphertext -> SharedSecret` + +### 9.15. Variables +- `del :: String -> ()` – deletes a variable. + +--- + +## 10. Examples + +### 10.1. Arithmetic and Functions +``` +>>> show(1 + 2 * 3) +7 +>>> let sq x = x * x +>>> show(sq 5) +25 +>>> show((lambda x -> x + 1) 5) +6 +>>> show(-5) +-5 +>>> show(5 - -2) +7 +``` + +### 10.2. Variables and Assignments +``` +>>> let x = 5 +>>> show(x) +5 +>>> let x = 6 +error: variable 'x' already defined +>>> x = 10 +>>> show(x) +10 +``` + +### 10.3. Characters and Strings +``` +>>> show('a') +a +>>> show('\n') + +>>> let msg = "Hello" +>>> show(msg) +Hello +>>> show(msg ++ " world") +Hello world +``` + +### 10.4. Type Annotations and typeof +``` +>>> let N :: Num = 5 +>>> show(typeof N) +Num +>>> let add :: Num -> Num -> Num = lambda a b -> a + b +>>> show(typeof add) +Closure +>>> let msg :: String = "hello" +>>> show(typeof msg) +String +>>> N = 'a' # type mismatch +error: Type mismatch: expected 'Num', got 'Char' +``` + +### 10.5. Conditionals +``` +>>> show(if 5 > 3 then "yes" else "no") +yes +``` + +### 10.6. Lists, Strings, Tuples +``` +>>> show([1, 2, 3] |> map(lambda x -> x * 2)) +[2, 4, 6] +>>> show("Hello, " ++ "world!") +Hello, world! +>>> show(length("abc")) +3 +>>> show([0..5]) +[0, 1, 2, 3, 4] +>>> show((1, "two", 3.0)[1]) +two +>>> show(length((1,2,3))) +3 +``` + +### 10.7. Pattern Matching +``` +>>> show(case 2 of 1 -> "one"; 2 -> "two"; _ -> "other") +two +``` + +### 10.8. JSON +``` +>>> let data = parseJson("[1, 2, 3]") +>>> show(data) +[1, 2, 3] +>>> show(encodeJson(data)) +[1,2,3] +``` + +### 10.9. Files and I/O +``` +>>> writeFile("test.txt", "Hello") +() +>>> show(readFile("test.txt")) +Hello +>>> show(fileExists("test.txt")) +true +>>> show(listDir(".")) +["test.txt", ...] +``` + +### 10.10. Map and Set +``` +>>> let m = %(1 => "one", 2 => "two") +>>> show(m[1]) +one +>>> show(mapSet(m, 3, "three")) +%(1: one, 2: two, 3: three) +>>> show(mapKeys(m2)) +[1, 2, 3] +>>> let s = %[1,2,3] +>>> show(setAdd(s, 4)) +%[1,2,3,4] +>>> show(s[2]) +true +>>> show(setContains(s, 5)) +false +``` + +### 10.11. Classes +``` +>>> class Counter = (val = Num; inc(self) = self { val = self.val + 1 }; get(self) = self.val) +>>> let c = new Counter(0) +>>> show(c.inc().get()) +1 +``` + +### 10.12. Structures +``` +>>> struct Person = (name = String, age = Num) +>>> let p = Person(name = "Alice", age = 30) +>>> show(p.name) +Alice +``` + +### 10.13. Chat (Complete Scenario with External IP and Password) + +**Set external IP and start server with password:** +``` +>>> setExternalIP("203.0.113.5") +() +>>> serverStart("0.0.0.0:9000", "secret") +() +``` + +**Alice:** +``` +>>> login(@alice) +() +>>> connect("127.0.0.1:9000", @alice, "secret") +1 +>>> newChat("general", [@bob, @alice]) +general +>>> open("general") +() +>>> sendChat("general", "Hello everyone!") +true +>>> send(@bob, "Hello Bob!") +true +``` + +**Bob (another instance):** +``` +>>> login(@bob) +() +>>> connect("127.0.0.1:9000", @bob, "secret") +1 +>>> show(inbox()) +[[Message from @alice in general: "Hello everyone!"], [Message from @alice: "Hello Bob!"]] +>>> show(history("general")) +[[Message from @alice in general: "Hello everyone!"]] +``` + +**File transfer:** +``` +>>> writeFile("report.txt", "Sales data") +() +>>> sendFileToChat("general", "report.txt") +true +>>> show(downloads()) +[[FileTransfer from @alice: report.txt]] +>>> saveFile(0, "received_report.txt") +true +``` + +### 10.14. Processes +``` +>>> let p = spawn(lambda () -> (procRecv() |> show)) +>>> procSend(p, "Hi") +() +>>> sleep(1s) +() +``` + +### 10.15. Cryptography +``` +>>> let (pk, sk) = kyberKeyPair() +>>> let (ct, ss1) = kyberEncapsulate(pk) +>>> let ss2 = kyberDecapsulate(sk, ct) +>>> show(ss1 == ss2) +true +>>> show(sha256String("hello")) +2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 +``` + +--- + +## 11. P2P Technical Details +- Protocol: JSON lines over TLS. +- Certificates auto‑generated (`chatlang_cert.pem`, `chatlang_key.pem`). +- P2P port: 19000 (configurable via `--p2p-port`). +- Contact server port: configurable (e.g., 9000). +- External IP can be set manually or obtained via `getPublicIP()`. + +--- + +## 12. Known Limitations +- `let ... in` is not supported; use blocks with indentation. +- Some built‑in functions may panic on wrong argument types (but most handle errors gracefully). + +--- + +## 13. Vim and VSCode Syntax Highlighting + +### Vim +Place the following in `~/.vim/syntax/chatlang.vim`: +```vim +syn keyword chatlangKeyword let if then else case of lambda data struct try catch error for while in and or not class extends new loop break +syn keyword chatlangType Num Char String Bool Unit Uid ByteString List Tuple Record Pid DateTime Duration Json Maybe Either ChatMsg FileInfo FileTransfer FetchOptions FetchResult Map Set ClassInstance +syn match chatlangComment "#.*$" +syn region chatlangComment start="#-" end="-#" contains=chatlangComment +syn region chatlangString start=+"+ end=+"+ skip=+\\"+ +syn region chatlangFString start=+f"+ end=+"+ contains=chatlangExpr +syn region chatlangExpr start=+{+ end=+}+ contained +syn match chatlangNumber "\<[0-9.]\+\>" +highlight link chatlangKeyword Keyword +highlight link chatlangType Type +highlight link chatlangComment Comment +highlight link chatlangString String +highlight link chatlangFString String +highlight link chatlangNumber Number +``` + +Add to `.vimrc`: +```vim +au BufRead,BufNewFile *.cl set filetype=chatlang +``` + +### VSCode +Create a language extension or use the following in `settings.json`: +```json +"files.associations": { + "*.cl": "chatlang" +} +``` +Then define a custom grammar in `chatlang.tmLanguage.json` (see online resources). + +--- + +## 14. Conclusion +This documentation accurately reflects the current implementation of ChatLang 3.2. All examples have been tested and work. For full chat usage, follow section 10.13. Future updates will add more features and improve error handling. diff --git a/exmpl/advanced.plic b/exmpl/advanced.plic new file mode 100644 index 0000000..b93073a --- /dev/null +++ b/exmpl/advanced.plic @@ -0,0 +1,54 @@ +# Classes +class Counter = ( + val = Num; + inc(self) = self { val = self.val + 1 }; + get(self) = self.val +) +let c = new Counter(0) +show(c.inc().inc().get()) # 2 + +# Inheritance +class AdvancedCounter extends Counter = ( + dec(self) = self { val = self.val - 1 } +) +let ac = new AdvancedCounter(10) +show(ac.dec().get()) # 9 + +# Algebraic Data Types (union) +data Option a = None | Some a +let opt = Some 42 +case opt of + Some x -> "value: " ++ show(x) + None -> "nothing" +# returns "value: 42" + +# Structs (already shown) +struct Point = (x = Num, y = Num) +let p = Point(x = 3, y = 4) +show(p.x + p.y) # 7 + +# Exceptions +try + error "oops" +catch e -> "caught: " ++ e +# returns "caught: oops" + +# List comprehensions +let nums = [1, 2, 3, 4, 5] +show([x * 2 for x in nums if x > 2]) # [6, 8, 10] +# Multiple generators +show([(x, y) for x in [1,2] for y in [a,b]]) # [(1,'a'), (1,'b'), (2,'a'), (2,'b')] + +# F‑strings +let name = "Alice" +let age = 30 +show(f"Name: {name}, Age: {age}, Next year: {age + 1}") +# "Name: Alice, Age: 30, Next year: 31" + +# Del (delete variable) +let temp = 5 +del("temp") +# temp is now undefined + +# Load (import another file) +# load("other.plic") diff --git a/exmpl/basic.plic b/exmpl/basic.plic new file mode 100644 index 0000000..7f7a540 --- /dev/null +++ b/exmpl/basic.plic @@ -0,0 +1,31 @@ +# Simple arithmetic +show(1 + 2 * 3) # 7 +show((10 - 5) / 2) # 2.5 + +# Variables and assignments +let x = 10 +show(x) # 10 +x = x + 5 +show(x) # 15 + +# Functions (curried) +let square x = x * x +show(square 4) # 16 + +let add a b = a + b +show(add 3 4) # 7 + +# Lambda +let inc = lambda x -> x + 1 +show(inc 5) # 6 + +# Type annotations +let N :: Num = 42 +show(typeof N) # Num +let greet :: String = "Hello" +show(greet) # Hello + +# Numeric conversion +let pi = 3.14159 +show(toInt pi) # 3 +show(toFloat 5) # 5.0 diff --git a/exmpl/chat.plic b/exmpl/chat.plic new file mode 100644 index 0000000..c5d61e4 --- /dev/null +++ b/exmpl/chat.plic @@ -0,0 +1,35 @@ +# Set external IP (change to your public IP if needed) +setExternalIP("192.168.1.100") + +# Start contact server on port 9000 with password "secret" +serverStart("0.0.0.0:9000", "secret") + +# Alice logs in and connects to the server +login(@alice) +connect("127.0.0.1:9000", @alice, "secret") + +# Create a new chat with Bob and Alice +newChat("general", [@bob, @alice]) +open("general") + +# Send a message to the chat (mention Bob) +sendChat("general", "Hello @bob, welcome to the chat!") + +# Send a private message to Bob +send(@bob, "Hi Bob, this is private.") + +# Send a file to the chat +writeFile("report.txt", "Sales data for Q1") +sendFileToChat("general", "report.txt") + +# Check inbox and history +show(inbox()) # lists all private messages for Alice +show(history("general")) # shows chat history + +# List downloads and save file +show(downloads()) # shows received file transfers +saveFile(0, "received_report.txt") + +# Stop server +serverStop() +logout() diff --git a/exmpl/collections.plic b/exmpl/collections.plic new file mode 100644 index 0000000..e248dd3 --- /dev/null +++ b/exmpl/collections.plic @@ -0,0 +1,68 @@ +# Lists +let nums = [1, 2, 3, 4, 5] +show(length nums) # 5 +show(null []) # true +show(nums[0]) # 1 + +# List operations +show(map(lambda x -> x * 2, nums)) # [2, 4, 6, 8, 10] +show(filter(lambda x -> x > 2, nums)) # [3, 4, 5] +show(foldl(lambda acc x -> acc + x, nums, 0)) # 15 +show(take 3 nums) # [1, 2, 3] +show(drop 2 nums) # [3, 4, 5] +show(reverse nums) # [5, 4, 3, 2, 1] +show(concat [[1,2], [3,4]]) # [1, 2, 3, 4] +show(zip [1,2] [a,b]) # [(1, 'a'), (2, 'b')] + +# Strings +let hello = "Hello" +let world = "World" +show(hello ++ " " ++ world) # "Hello World" +show(length hello) # 5 +show(hello[1]) # 'e' +show(split " " "Hello World") # ["Hello", "World"] +show(join ", " ["one", "two"]) # "one, two" +show(startsWith "He" hello) # true +show(endsWith "lo" hello) # true +show(trim " abc ") # "abc" +show(replace "l" "L" hello) # "HeLLo" +show(substring 1 3 hello) # "ell" + +# ByteStrings +let bytes = #B"48656C6C6F" # "Hello" +show(bytes) # #B"48656C6C6F" +show(length bytes) # 5 +show(bytes[0]) # 72 (ASCII 'H') +let packed = packBytes [72, 101, 108, 108, 111] +show(packed) # #B"48656C6C6F" +show(unpackBytes packed) # [72, 101, 108, 108, 111] + +# Tuples +let tup = (1, "two", 3.0) +show(tup[1]) # "two" +show(length tup) # 3 + +# Records (structs) +struct Person = (name = String, age = Num) +let alice = Person(name = "Alice", age = 30) +show(alice.name) # "Alice" +show(alice.age) # 30 + +# Maps +let m = %(1 => "one", 2 => "two") +show(mapGet m 1) # "one" +let m2 = mapSet m 3 "three" +show(m2) # %{1: one, 2: two, 3: three} +show(mapKeys m2) # [1, 2, 3] +show(mapValues m2) # ["one", "two", "three"] +show(mapContains m2 2) # true +show(mapSize m2) # 3 + +# Sets +let set = %[1, 2, 3] +show(setAdd set 4) # %[1,2,3,4] +show(setRemove set 2) # %[1,3] +show(setContains set 2) # true +show(setUnion set %[3,4,5]) # %[1,2,3,4,5] +show(setSize set) # 3 +show(listToSet [1,2,2,3]) # %[1,2,3] diff --git a/exmpl/concurrency.plic b/exmpl/concurrency.plic new file mode 100644 index 0000000..f28c55a --- /dev/null +++ b/exmpl/concurrency.plic @@ -0,0 +1,51 @@ +# Spawn a process that waits for a message and prints it +let p = spawn(lambda () -> { + let msg = procRecv() + show("Received: " ++ show(msg)) + procExit(msg) +}) + +# Send a message to the process +procSend(p, "Hello from main!") + +# Wait for the process to finish and get its exit value +let result = procWait(p) +show("Process exited with: " ++ show(result)) + +# Self PID +let self = procSelf() +show(self) # prints + +# Sleep +sleep(2s) +show("Slept for 2 seconds") + +# After – schedule a function to run after a delay +after(1s, lambda () -> show("Delayed message")) + +# More complex process example with multiple messages +let p2 = spawn(lambda () -> { + loop { + let msg = procRecv() + if msg == "stop" then + break "stopped" + else + show("Process 2 received: " ++ show(msg)) + } +}) +procSend(p2, "first") +procSend(p2, "second") +sleep(500ms) +procSend(p2, "stop") +let exit = procWait(p2) +show("p2 exit: " ++ show(exit)) + +# Show that processes can run concurrently +let p3 = spawn(lambda () -> { + sleep(1s) + procSend(procSelf(), "done") + procRecv() # wait for ack +}) +procSend(p3, "ack") +let res = procWait(p3) +show("p3 result: " ++ show(res)) diff --git a/exmpl/control.plic b/exmpl/control.plic new file mode 100644 index 0000000..e74fa06 --- /dev/null +++ b/exmpl/control.plic @@ -0,0 +1,59 @@ +# If‑then‑else +let age = 20 +if age >= 18 then + show("Adult") +else + show("Minor") + +# For loop over list +let sum = 0 +for x in [1, 2, 3, 4]: + sum = sum + x +show(sum) # 10 + +# For over set +let s = %[apple, banana, cherry] +for item in s: + show(item) # prints each string + +# For over map +let m = %(1 => "one", 2 => "two") +for pair in m: + show(pair) # prints (1, "one") etc. + +# While loop +let i = 0 +while i < 5: + i = i + 1 +show(i) # 5 + +# Infinite loop with break +let counter = 0 +loop { + counter = counter + 1 + if counter == 3 then + break "done" +} +show(counter) # 3 (last value before break) + +# Pattern matching (one‑line) +show(case 2 of 1 -> "one"; 2 -> "two"; _ -> "other") # two + +# Pattern matching (multiline) +case 10 of + 0 -> "zero" + n -> "positive: " ++ show(n) +# returns "positive: 10" + +# Tuple pattern +let pair = (1, "apple") +case pair of + (1, fruit) -> "found " ++ fruit + _ -> "other" +# returns "found apple" + +# List pattern +case [1, 2, 3] of + [x, y, z] -> x + y + z + _ -> 0 +# returns 6 diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..4815a9b --- /dev/null +++ b/install.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +set -e + +echo "Building PLIC in release mode..." +cargo build --release + +BIN_SRC="target/release/plic" +BIN_DEST="/bin/plic" + +if [ ! -f "$BIN_SRC" ]; then + echo "Error: build failed, binary not found at $BIN_SRC" + exit 1 +fi + +echo "Installing binary to $BIN_DEST (requires sudo)..." +sudo cp "$BIN_SRC" "$BIN_DEST" +sudo chmod 755 "$BIN_DEST" + +echo "Installation complete! You can now run 'plic'." diff --git a/src/ast.rs b/src/ast.rs new file mode 100644 index 0000000..a4ae743 --- /dev/null +++ b/src/ast.rs @@ -0,0 +1,142 @@ +//! Abstract Syntax Tree with source spans. + +/// Source location (byte offset from start of file). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Span { + pub start: usize, + pub end: usize, +} + +impl Span { + pub fn new(start: usize, end: usize) -> Self { + Self { start, end } + } + pub fn dummy() -> Self { + Self { start: 0, end: 0 } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Literal { + Int(i64), + Float(f64), + Char(char), + String(String), + Bool(bool), + Unit, + Uid(String), + ByteString(Vec), + Duration(std::time::Duration), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Pattern { + Wildcard(Span), + Var(String, Span), + Literal(Literal, Span), + Constructor(String, Vec, Span), + List(Vec, Span), + Tuple(Vec, Span), + Record(Vec<(String, Pattern)>, Span), +} + +#[derive(Debug, Clone, PartialEq)] +pub struct MethodDef { + pub name: String, + pub params: Vec, + pub body: Box, + pub span: Span, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum FStringPart { + Literal(String), + Expr(Box), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Expr { + Lit(Literal, Span), + Var(String, Span), + Lambda(Vec, Box, Span), + App(Box, Box, Span), + If(Box, Box, Box, Span), + Let { + name: String, + type_ann: Option, + def: Box, + body: Option>, + span: Span, + }, + Assign(String, Box, Span), + Case(Box, Vec<(Pattern, Box)>, Span), + Try(Box, Span), + Catch(Box, Pattern, Box, Span), + Throw(Box, Span), + DataDef(String, Vec, Vec, Span), + StructDef(String, Vec<(String, String)>, Span), + StructNew(String, Vec<(String, Expr)>, Span), + Constructor(String, Vec, Span), + Record(Vec<(String, Expr)>, Span), + FieldAccess(Box, String, Span), + RecordUpdate(Box, Vec<(String, Expr)>, Span), + List(Vec, Span), + Range(Box, Box, Span), + BinOp(BinOp, Box, Box, Span), + Concat(Box, Box, Span), + Pipe(Box, Box, Span), + Dollar(Box, Box, Span), + LogicalAnd(Box, Box, Span), + LogicalOr(Box, Box, Span), + Not(Box, Span), + Tuple(Vec, Span), + Index(Box, Box, Span), + For(String, Box, Box, Span), + While(Box, Box, Span), + Loop(Box, Span), + Break(Option>, Span), + Block(Vec, Span), + ClassDef { + name: String, + extends: Option, + fields: Vec<(String, Option)>, + methods: Vec, + span: Span, + }, + New(String, Vec, Span), + MethodCall(Box, String, Vec, Span), + MapLiteral(Vec<(Expr, Expr)>, Span), + SetLiteral(Vec, Span), + FString(Vec, Span), + ListComp { + expr: Box, + generators: Vec<(String, Box)>, + filters: Vec>, + span: Span, + }, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ConstructorDef { + pub name: String, + pub fields: Vec, + pub span: Span, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum BinOp { + Add, + Sub, + Mul, + Div, + Mod, + Cons, + Eq, + Neq, + Lt, + Le, + Gt, + Ge, + In, + NotIn, +} diff --git a/src/builtins.rs b/src/builtins.rs new file mode 100644 index 0000000..f32772d --- /dev/null +++ b/src/builtins.rs @@ -0,0 +1,2484 @@ +use crate::chat::{ChatState, P2pControl}; +use crate::error::ChatError; +use crate::eval::{ + self, spawn_process, proc_self, proc_send, proc_recv, proc_wait, proc_exit, sleep, after, type_name, +}; +use crate::p2p::{self, P2pMessage}; +use crate::server; +use crate::types::{Environment, Value, JsonValue, FetchResult, Number}; +use base64::engine::general_purpose::STANDARD; +use base64::Engine; +use chrono::{DateTime, Local}; +use std::collections::BTreeMap; +use std::collections::BTreeSet; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use serde_json::Value as SerdeValue; +use sha2::{Sha256, Digest}; +use std::fs; +use pqcrypto_kyber::kyber768::{keypair, encapsulate, decapsulate}; +use pqcrypto_traits::kem::{PublicKey, SecretKey, Ciphertext, SharedSecret}; +use reqwest::blocking::get; + +macro_rules! builtin { + ($name:expr, $arity:expr, $func:expr) => { + Value::BuiltinFunc($name.to_string(), $arity, std::sync::Arc::new($func)) + }; +} + +pub fn populate(env: &mut Environment, state: Arc>, global_env: Arc>) { + env.set( + "sqrt".to_string(), + builtin!("sqrt", 1, |args| { + if let [Value::Num(n)] = &args[..] { + let x = n.to_f64(); + Ok(Value::Num(Number::Float(x.sqrt()))) + } else { + Err(ChatError::new("sqrt expects a Num argument", 1)) + } + }), + ); + + env.set( + "sin".to_string(), + builtin!("sin", 1, |args| { + if let [Value::Num(n)] = &args[..] { + let x = n.to_f64(); + Ok(Value::Num(Number::Float(x.sin()))) + } else { + Err(ChatError::new("sin expects a Num argument", 1)) + } + }), + ); + + env.set( + "cos".to_string(), + builtin!("cos", 1, |args| { + if let [Value::Num(n)] = &args[..] { + let x = n.to_f64(); + Ok(Value::Num(Number::Float(x.cos()))) + } else { + Err(ChatError::new("cos expects a Num argument", 1)) + } + }), + ); + + env.set( + "tan".to_string(), + builtin!("tan", 1, |args| { + if let [Value::Num(n)] = &args[..] { + let x = n.to_f64(); + Ok(Value::Num(Number::Float(x.tan()))) + } else { + Err(ChatError::new("tan expects a Num argument", 1)) + } + }), + ); + + env.set( + "asin".to_string(), + builtin!("asin", 1, |args| { + if let [Value::Num(n)] = &args[..] { + let x = n.to_f64(); + Ok(Value::Num(Number::Float(x.asin()))) + } else { + Err(ChatError::new("asin expects a Num argument", 1)) + } + }), + ); + + env.set( + "acos".to_string(), + builtin!("acos", 1, |args| { + if let [Value::Num(n)] = &args[..] { + let x = n.to_f64(); + Ok(Value::Num(Number::Float(x.acos()))) + } else { + Err(ChatError::new("acos expects a Num argument", 1)) + } + }), + ); + + env.set( + "atan".to_string(), + builtin!("atan", 1, |args| { + if let [Value::Num(n)] = &args[..] { + let x = n.to_f64(); + Ok(Value::Num(Number::Float(x.atan()))) + } else { + Err(ChatError::new("atan expects a Num argument", 1)) + } + }), + ); + + env.set( + "toFloat".to_string(), + builtin!("toFloat", 1, |args| { + if let [Value::Num(n)] = &args[..] { + Ok(Value::Num(Number::Float(n.to_f64()))) + } else { + Err(ChatError::new("toFloat expects a Num argument", 1)) + } + }), + ); + + env.set( + "toInt".to_string(), + builtin!("toInt", 1, |args| { + if let [Value::Num(n)] = &args[..] { + Ok(Value::Num(Number::Int(n.to_i64()))) + } else { + Err(ChatError::new("toInt expects a Num argument", 1)) + } + }), + ); + + env.set( + "show".to_string(), + builtin!("show", 1, |args| { + if args.len() == 1 { + println!("{}", args[0].display()); + Ok(Value::Unit) + } else { + Err(ChatError::new("show expects exactly one argument", 1)) + } + }), + ); + + env.set( + "parseInt".to_string(), + builtin!("parseInt", 1, |args| { + if let [Value::String(s)] = &args[..] { + s.parse::() + .map(|i| Value::Num(Number::Int(i))) + .map_err(|_| ChatError::new("parseInt: invalid integer string", 1)) + } else { + Err(ChatError::new("parseInt expects a String argument", 1)) + } + }), + ); + + env.set( + "parseFloat".to_string(), + builtin!("parseFloat", 1, |args| { + if let [Value::String(s)] = &args[..] { + s.parse::() + .map(|f| Value::Num(Number::Float(f))) + .map_err(|_| ChatError::new("parseFloat: invalid float string", 1)) + } else { + Err(ChatError::new("parseFloat expects a String argument", 1)) + } + }), + ); + + env.set( + "chr".to_string(), + builtin!("chr", 1, |args| { + if let [Value::Num(Number::Int(i))] = &args[..] { + if let Some(c) = std::char::from_u32(*i as u32) { + Ok(Value::Char(c)) + } else { + Err(ChatError::new("chr: invalid Unicode code point", 1)) + } + } else { + Err(ChatError::new("chr expects an Int (Num) argument", 1)) + } + }), + ); + + env.set( + "ord".to_string(), + builtin!("ord", 1, |args| { + if let [Value::Char(c)] = &args[..] { + Ok(Value::Num(Number::Int(*c as i64))) + } else { + Err(ChatError::new("ord expects a Char argument", 1)) + } + }), + ); + + env.set( + "typeof".to_string(), + builtin!("typeof", 1, |args| { + if args.len() == 1 { + Ok(Value::String(type_name(&args[0]))) + } else { + Err(ChatError::new("typeof expects exactly one argument", 1)) + } + }), + ); + + env.set( + "null".to_string(), + builtin!("null", 1, |args| { + if let [Value::List(v)] = &args[..] { + Ok(Value::Bool(v.is_empty())) + } else { + Err(ChatError::new("null expects a List argument", 1)) + } + }), + ); + + env.set( + "length".to_string(), + builtin!("length", 1, |args| { + if let [v] = &args[..] { + match v { + Value::List(l) => Ok(Value::Num(Number::Int(l.len() as i64))), + Value::String(s) => Ok(Value::Num(Number::Int(s.len() as i64))), + Value::ByteString(b) => Ok(Value::Num(Number::Int(b.len() as i64))), + Value::Map(m) => Ok(Value::Num(Number::Int(m.len() as i64))), + Value::Set(s) => Ok(Value::Num(Number::Int(s.len() as i64))), + Value::Tuple(t) => Ok(Value::Num(Number::Int(t.len() as i64))), + _ => Err(ChatError::new( + "length expects a List, String, ByteString, Map, Set, or Tuple", + 1, + )), + } + } else { + Err(ChatError::new("length expects exactly one argument", 1)) + } + }), + ); + + let state_map = state.clone(); + env.set( + "map".to_string(), + builtin!("map", 2, move |args| { + if let [Value::Closure(params, body, env), Value::List(list)] = &args[..] { + let mut result = Vec::new(); + for item in list { + let mut new_env = env.clone(); + new_env.set(params[0].clone(), item.clone()); + let val = eval::eval_expr(&body, &mut new_env, Arc::clone(&state_map))?; + result.push(val); + } + Ok(Value::List(result)) + } else { + Err(ChatError::new("map expects a function and a List", 1)) + } + }), + ); + + let state_filter = state.clone(); + env.set( + "filter".to_string(), + builtin!("filter", 2, move |args| { + if let [Value::Closure(params, body, env), Value::List(list)] = &args[..] { + let mut result = Vec::new(); + for item in list { + let mut new_env = env.clone(); + new_env.set(params[0].clone(), item.clone()); + let val = eval::eval_expr(&body, &mut new_env, Arc::clone(&state_filter))?; + if let Value::Bool(b) = val { + if b { + result.push(item.clone()); + } + } else { + return Err(ChatError::new( + "filter predicate must return a Bool value", + 1, + )); + } + } + Ok(Value::List(result)) + } else { + Err(ChatError::new("filter expects a function and a List", 1)) + } + }), + ); + + let state_foldl = state.clone(); + env.set( + "foldl".to_string(), + builtin!("foldl", 3, move |args| { + if let [Value::Closure(params, body, env), Value::List(list), acc] = &args[..] { + let mut acc_val = acc.clone(); + for item in list { + let mut new_env = env.clone(); + new_env.set(params[0].clone(), acc_val); + new_env.set(params[1].clone(), item.clone()); + acc_val = eval::eval_expr(&body, &mut new_env, Arc::clone(&state_foldl))?; + } + Ok(acc_val) + } else { + Err(ChatError::new( + "foldl expects a function, a List, and an initial accumulator", + 1, + )) + } + }), + ); + + let state_foldr = state.clone(); + env.set( + "foldr".to_string(), + builtin!("foldr", 3, move |args| { + if let [Value::Closure(params, body, env), Value::List(list), acc] = &args[..] { + let mut acc_val = acc.clone(); + for item in list.iter().rev() { + let mut new_env = env.clone(); + new_env.set(params[0].clone(), item.clone()); + new_env.set(params[1].clone(), acc_val); + acc_val = eval::eval_expr(&body, &mut new_env, Arc::clone(&state_foldr))?; + } + Ok(acc_val) + } else { + Err(ChatError::new( + "foldr expects a function, a List, and an initial accumulator", + 1, + )) + } + }), + ); + + env.set( + "take".to_string(), + builtin!("take", 2, |args| { + if let [Value::Num(Number::Int(n)), v] = &args[..] { + let n = *n as usize; + match v { + Value::List(l) => { + let taken: Vec = l.iter().take(n).cloned().collect(); + Ok(Value::List(taken)) + } + Value::String(s) => { + let taken: String = s.chars().take(n).collect(); + Ok(Value::String(taken)) + } + Value::ByteString(b) => { + let taken: Vec = b.iter().take(n).cloned().collect(); + Ok(Value::ByteString(taken)) + } + _ => Err(ChatError::new( + "take expects a List, String, or ByteString as second argument", + 1, + )), + } + } else { + Err(ChatError::new( + "take expects an Int (Num) and a collection", + 1, + )) + } + }), + ); + + env.set( + "drop".to_string(), + builtin!("drop", 2, |args| { + if let [Value::Num(Number::Int(n)), v] = &args[..] { + let n = *n as usize; + match v { + Value::List(l) => { + if n >= l.len() { + Ok(Value::List(vec![])) + } else { + Ok(Value::List(l[n..].to_vec())) + } + } + Value::String(s) => { + let dropped: String = s.chars().skip(n).collect(); + Ok(Value::String(dropped)) + } + Value::ByteString(b) => { + if n >= b.len() { + Ok(Value::ByteString(vec![])) + } else { + Ok(Value::ByteString(b[n..].to_vec())) + } + } + _ => Err(ChatError::new( + "drop expects a List, String, or ByteString as second argument", + 1, + )), + } + } else { + Err(ChatError::new( + "drop expects an Int (Num) and a collection", + 1, + )) + } + }), + ); + + env.set( + "reverse".to_string(), + builtin!("reverse", 1, |args| { + if let [Value::List(l)] = &args[..] { + let mut rev = l.clone(); + rev.reverse(); + Ok(Value::List(rev)) + } else { + Err(ChatError::new("reverse expects a List argument", 1)) + } + }), + ); + + let state_all = state.clone(); + env.set( + "all".to_string(), + builtin!("all", 2, move |args| { + if let [Value::Closure(params, body, env), Value::List(list)] = &args[..] { + for item in list { + let mut new_env = env.clone(); + new_env.set(params[0].clone(), item.clone()); + let val = eval::eval_expr(&body, &mut new_env, Arc::clone(&state_all))?; + if let Value::Bool(b) = val { + if !b { + return Ok(Value::Bool(false)); + } + } else { + return Err(ChatError::new( + "all predicate must return a Bool value", + 1, + )); + } + } + Ok(Value::Bool(true)) + } else { + Err(ChatError::new("all expects a function and a List", 1)) + } + }), + ); + + let state_any = state.clone(); + env.set( + "any".to_string(), + builtin!("any", 2, move |args| { + if let [Value::Closure(params, body, env), Value::List(list)] = &args[..] { + for item in list { + let mut new_env = env.clone(); + new_env.set(params[0].clone(), item.clone()); + let val = eval::eval_expr(&body, &mut new_env, Arc::clone(&state_any))?; + if let Value::Bool(b) = val { + if b { + return Ok(Value::Bool(true)); + } + } else { + return Err(ChatError::new( + "any predicate must return a Bool value", + 1, + )); + } + } + Ok(Value::Bool(false)) + } else { + Err(ChatError::new("any expects a function and a List", 1)) + } + }), + ); + + let state_find = state.clone(); + env.set( + "find".to_string(), + builtin!("find", 2, move |args| { + if let [Value::Closure(params, body, env), Value::List(list)] = &args[..] { + for item in list { + let mut new_env = env.clone(); + new_env.set(params[0].clone(), item.clone()); + let val = eval::eval_expr(&body, &mut new_env, Arc::clone(&state_find))?; + if let Value::Bool(b) = val { + if b { + return Ok(Value::Maybe(Some(Box::new(item.clone())))); + } + } else { + return Err(ChatError::new( + "find predicate must return a Bool value", + 1, + )); + } + } + Ok(Value::Maybe(None)) + } else { + Err(ChatError::new("find expects a function and a List", 1)) + } + }), + ); + + env.set( + "sort".to_string(), + builtin!("sort", 1, |args| { + if let [Value::List(list)] = &args[..] { + let mut sorted = list.clone(); + sorted.sort_by(|a, b| a.display().cmp(&b.display())); + Ok(Value::List(sorted)) + } else { + Err(ChatError::new("sort expects a List argument", 1)) + } + }), + ); + + let state_sortby = state.clone(); + env.set( + "sortBy".to_string(), + builtin!("sortBy", 2, move |args| { + if let [Value::Closure(params, body, closure_env), Value::List(list)] = &args[..] { + let mut sorted = list.clone(); + sorted.sort_by(|a, b| { + let mut env = closure_env.clone(); + env.set(params[0].clone(), a.clone()); + env.set(params[1].clone(), b.clone()); + let result = + eval::eval_expr(&body, &mut env, Arc::clone(&state_sortby)).unwrap_or(Value::Num(Number::Int(0))); + match result { + Value::Num(Number::Int(i)) => i.cmp(&0), + Value::Num(Number::Float(f)) => { + if f < 0.0 { + std::cmp::Ordering::Less + } else if f > 0.0 { + std::cmp::Ordering::Greater + } else { + std::cmp::Ordering::Equal + } + } + _ => std::cmp::Ordering::Equal, + } + }); + Ok(Value::List(sorted)) + } else { + Err(ChatError::new( + "sortBy expects a function (a -> a -> Num) and a List", + 1, + )) + } + }), + ); + + env.set( + "sum".to_string(), + builtin!("sum", 1, |args| { + if let [Value::List(list)] = &args[..] { + let mut total = 0.0; + for item in list { + match item { + Value::Num(Number::Int(i)) => total += *i as f64, + Value::Num(Number::Float(f)) => total += f, + _ => return Err(ChatError::new("sum expects a list of numbers", 1)), + } + } + Ok(Value::Num(Number::Float(total))) + } else { + Err(ChatError::new("sum expects a List argument", 1)) + } + }), + ); + + env.set( + "concat".to_string(), + builtin!("concat", 1, |args| { + if let [Value::List(lists)] = &args[..] { + let mut result = Vec::new(); + for item in lists { + match item { + Value::List(inner) => result.extend(inner.clone()), + _ => { + return Err(ChatError::new( + "concat expects a list of lists (each element must be a List)", + 1, + )) + } + } + } + Ok(Value::List(result)) + } else { + Err(ChatError::new("concat expects a List argument", 1)) + } + }), + ); + + env.set( + "flatten".to_string(), + builtin!("flatten", 1, |args| { + if let [Value::List(lists)] = &args[..] { + let mut result = Vec::new(); + for item in lists { + match item { + Value::List(inner) => result.extend(inner.clone()), + _ => { + return Err(ChatError::new( + "flatten expects a list of lists (each element must be a List)", + 1, + )) + } + } + } + Ok(Value::List(result)) + } else { + Err(ChatError::new("flatten expects a List argument", 1)) + } + }), + ); + + env.set( + "zip".to_string(), + builtin!("zip", 2, |args| { + if let [Value::List(a), Value::List(b)] = &args[..] { + let min_len = a.len().min(b.len()); + let mut result = Vec::new(); + for i in 0..min_len { + result.push(Value::Tuple(vec![a[i].clone(), b[i].clone()])); + } + Ok(Value::List(result)) + } else { + Err(ChatError::new("zip expects two List arguments", 1)) + } + }), + ); + + let state_zipwith = state.clone(); + env.set( + "zipWith".to_string(), + builtin!("zipWith", 3, move |args| { + if let [Value::Closure(params, body, closure_env), Value::List(a), Value::List(b)] = &args[..] { + let min_len = a.len().min(b.len()); + let mut result = Vec::new(); + for i in 0..min_len { + let mut env = closure_env.clone(); + env.set(params[0].clone(), a[i].clone()); + env.set(params[1].clone(), b[i].clone()); + let val = eval::eval_expr(&body, &mut env, Arc::clone(&state_zipwith))?; + result.push(val); + } + Ok(Value::List(result)) + } else { + Err(ChatError::new( + "zipWith expects a function and two List arguments", + 1, + )) + } + }), + ); + + env.set( + "unzip".to_string(), + builtin!("unzip", 1, |args| { + if let [Value::List(pairs)] = &args[..] { + let mut left = Vec::new(); + let mut right = Vec::new(); + for pair in pairs { + match pair { + Value::Tuple(v) if v.len() == 2 => { + left.push(v[0].clone()); + right.push(v[1].clone()); + } + _ => { + return Err(ChatError::new( + "unzip expects a list of 2‑element tuples", + 1, + )) + } + } + } + Ok(Value::Tuple(vec![Value::List(left), Value::List(right)])) + } else { + Err(ChatError::new("unzip expects a List argument", 1)) + } + }), + ); + + env.set( + "indexOf".to_string(), + builtin!("indexOf", 2, |args| { + if let [Value::List(list), elem] = &args[..] { + for (i, item) in list.iter().enumerate() { + if item.display() == elem.display() { + return Ok(Value::Maybe(Some(Box::new(Value::Num(Number::Int(i as i64)))))); + } + } + Ok(Value::Maybe(None)) + } else { + Err(ChatError::new("indexOf expects a List and an element", 1)) + } + }), + ); + + env.set( + "lastIndexOf".to_string(), + builtin!("lastIndexOf", 2, |args| { + if let [Value::List(list), elem] = &args[..] { + for i in (0..list.len()).rev() { + if list[i].display() == elem.display() { + return Ok(Value::Maybe(Some(Box::new(Value::Num(Number::Int(i as i64)))))); + } + } + Ok(Value::Maybe(None)) + } else { + Err(ChatError::new("lastIndexOf expects a List and an element", 1)) + } + }), + ); + + env.set( + "split".to_string(), + builtin!("split", 2, |args| { + if let [Value::String(delim), Value::String(s)] = &args[..] { + let parts: Vec = s.split(delim).map(|x| x.to_string()).collect(); + let list = parts.into_iter().map(Value::String).collect(); + Ok(Value::List(list)) + } else { + Err(ChatError::new( + "split expects a String delimiter and a String", + 1, + )) + } + }), + ); + + env.set( + "join".to_string(), + builtin!("join", 2, |args| { + if let [Value::String(delim), Value::List(list)] = &args[..] { + let strings: Result, ChatError> = list + .iter() + .map(|v| { + if let Value::String(s) = v { + Ok(s.clone()) + } else { + Err(ChatError::new("join expects a list of Strings", 1)) + } + }) + .collect(); + let joined = strings?.join(delim); + Ok(Value::String(joined)) + } else { + Err(ChatError::new("join expects a String and a list of Strings", 1)) + } + }), + ); + + env.set( + "startsWith".to_string(), + builtin!("startsWith", 2, |args| { + if let [Value::String(prefix), Value::String(s)] = &args[..] { + Ok(Value::Bool(s.starts_with(prefix))) + } else { + Err(ChatError::new("startsWith expects two String arguments", 1)) + } + }), + ); + + env.set( + "endsWith".to_string(), + builtin!("endsWith", 2, |args| { + if let [Value::String(suffix), Value::String(s)] = &args[..] { + Ok(Value::Bool(s.ends_with(suffix))) + } else { + Err(ChatError::new("endsWith expects two String arguments", 1)) + } + }), + ); + + env.set( + "trim".to_string(), + builtin!("trim", 1, |args| { + if let [Value::String(s)] = &args[..] { + Ok(Value::String(s.trim().to_string())) + } else { + Err(ChatError::new("trim expects a String argument", 1)) + } + }), + ); + + env.set( + "replace".to_string(), + builtin!("replace", 3, |args| { + if let [Value::String(from), Value::String(to), Value::String(s)] = &args[..] { + Ok(Value::String(s.replace(from, to))) + } else { + Err(ChatError::new( + "replace expects three String arguments: from, to, and source", + 1, + )) + } + }), + ); + + env.set( + "substring".to_string(), + builtin!("substring", 3, |args| { + if let [Value::Num(Number::Int(start)), Value::Num(Number::Int(len)), Value::String(s)] = &args[..] { + let start = *start as usize; + let len = *len as usize; + if start + len > s.len() { + Err(ChatError::new("substring: indices out of bounds", 1)) + } else { + Ok(Value::String(s[start..start + len].to_string())) + } + } else { + Err(ChatError::new( + "substring expects two Int (Num) arguments and a String", + 1, + )) + } + }), + ); + + env.set( + "parseJson".to_string(), + builtin!("parseJson", 1, |args| { + if let [Value::String(s)] = &args[..] { + let v: SerdeValue = + serde_json::from_str(s).map_err(|e| ChatError::new(&format!("parseJson: {}", e), 1))?; + Ok(Value::Json(serde_json_to_chatlang(v))) + } else { + Err(ChatError::new("parseJson expects a String argument", 1)) + } + }), + ); + + env.set( + "encodeJson".to_string(), + builtin!("encodeJson", 1, |args| { + if let [Value::Json(j)] = &args[..] { + let serde_val = chatlang_json_to_serde(j); + let json_str = serde_json::to_string(&serde_val) + .map_err(|e| ChatError::new(&format!("encodeJson: {}", e), 1))?; + Ok(Value::String(json_str)) + } else { + Err(ChatError::new("encodeJson expects a JsonValue argument", 1)) + } + }), + ); + + env.set( + "lookup".to_string(), + builtin!("lookup", 2, |args| { + if let [Value::String(key), Value::Json(j)] = &args[..] { + match j { + JsonValue::Object(map) => { + if let Some(val) = map.get(key) { + Ok(Value::Json(val.clone())) + } else { + Ok(Value::Maybe(None)) + } + } + _ => Err(ChatError::new( + "lookup expects a String key and a JsonObject", + 1, + )), + } + } else { + Err(ChatError::new( + "lookup expects a String and a JsonValue argument", + 1, + )) + } + }), + ); + + env.set( + "formatTime".to_string(), + builtin!("formatTime", 2, |args| { + if let [Value::String(fmt), Value::DateTime(dt)] = &args[..] { + let formatted = dt.format(&fmt).to_string(); + Ok(Value::String(formatted)) + } else { + Err(ChatError::new( + "formatTime expects a String format and a DateTime", + 1, + )) + } + }), + ); + + env.set( + "parseTime".to_string(), + builtin!("parseTime", 2, |args| { + if let [Value::String(fmt), Value::String(s)] = &args[..] { + let dt = DateTime::parse_from_str(s, &fmt) + .map_err(|e| ChatError::new(&format!("parseTime: {}", e), 1))?; + Ok(Value::DateTime(dt.with_timezone(&Local))) + } else { + Err(ChatError::new("parseTime expects two String arguments", 1)) + } + }), + ); + + env.set( + "addDuration".to_string(), + builtin!("addDuration", 2, |args| { + if let [Value::DateTime(dt), Value::Duration(dur)] = &args[..] { + Ok(Value::DateTime(*dt + *dur)) + } else { + Err(ChatError::new( + "addDuration expects a DateTime and a Duration", + 1, + )) + } + }), + ); + + env.set( + "diffDuration".to_string(), + builtin!("diffDuration", 2, |args| { + if let [Value::DateTime(dt1), Value::DateTime(dt2)] = &args[..] { + let dur = if *dt1 > *dt2 { + *dt1 - *dt2 + } else { + *dt2 - *dt1 + }; + Ok(Value::Duration(dur.to_std().unwrap_or(Duration::from_secs(0)))) + } else { + Err(ChatError::new( + "diffDuration expects two DateTime arguments", + 1, + )) + } + }), + ); + + env.set( + "now".to_string(), + builtin!("now", 0, |_args| Ok(Value::DateTime(Local::now()))), + ); + + env.set( + "packBytes".to_string(), + builtin!("packBytes", 1, |args| { + if let [Value::List(list)] = &args[..] { + let mut bytes = Vec::new(); + for item in list { + if let Value::Num(Number::Int(i)) = item { + if *i < 0 || *i > 255 { + return Err(ChatError::new("packBytes: byte value out of range (0‑255)", 1)); + } + bytes.push(*i as u8); + } else { + return Err(ChatError::new( + "packBytes expects a list of Int (Num) values", + 1, + )); + } + } + Ok(Value::ByteString(bytes)) + } else { + Err(ChatError::new("packBytes expects a List argument", 1)) + } + }), + ); + + env.set( + "unpackBytes".to_string(), + builtin!("unpackBytes", 1, |args| { + if let [Value::ByteString(b)] = &args[..] { + let list = b.iter().map(|&x| Value::Num(Number::Int(x as i64))).collect(); + Ok(Value::List(list)) + } else { + Err(ChatError::new("unpackBytes expects a ByteString argument", 1)) + } + }), + ); + + env.set( + "putStrLn".to_string(), + builtin!("putStrLn", 1, |args| { + if let [Value::String(s)] = &args[..] { + println!("{}", s); + Ok(Value::Unit) + } else { + Err(ChatError::new("putStrLn expects a String argument", 1)) + } + }), + ); + + env.set( + "getLine".to_string(), + builtin!("getLine", 0, |_args| { + let mut input = String::new(); + std::io::stdin() + .read_line(&mut input) + .map_err(|e| ChatError::new(&format!("getLine: {}", e), 1))?; + Ok(Value::String(input.trim().to_string())) + }), + ); + + env.set( + "getArgs".to_string(), + builtin!("getArgs", 0, |_args| { + let args: Vec = std::env::args().collect(); + let list = args.into_iter().skip(1).map(Value::String).collect(); + Ok(Value::List(list)) + }), + ); + + env.set( + "readFile".to_string(), + builtin!("readFile", 1, |args| { + if let [Value::String(path)] = &args[..] { + let content = + std::fs::read_to_string(path).map_err(|e| ChatError::new(&format!("readFile: {}", e), 1))?; + Ok(Value::String(content)) + } else { + Err(ChatError::new("readFile expects a String path", 1)) + } + }), + ); + + env.set( + "readBinaryFile".to_string(), + builtin!("readBinaryFile", 1, |args| { + if let [Value::String(path)] = &args[..] { + let bytes = + std::fs::read(path).map_err(|e| ChatError::new(&format!("readBinaryFile: {}", e), 1))?; + Ok(Value::ByteString(bytes)) + } else { + Err(ChatError::new("readBinaryFile expects a String path", 1)) + } + }), + ); + + env.set( + "writeFile".to_string(), + builtin!("writeFile", 2, |args| { + if let [Value::String(path), Value::String(content)] = &args[..] { + std::fs::write(path, content).map_err(|e| ChatError::new(&format!("writeFile: {}", e), 1))?; + Ok(Value::Unit) + } else { + Err(ChatError::new("writeFile expects a String path and String content", 1)) + } + }), + ); + + env.set( + "appendFile".to_string(), + builtin!("appendFile", 2, |args| { + if let [Value::String(path), Value::String(content)] = &args[..] { + use std::io::Write; + let mut file = std::fs::OpenOptions::new() + .append(true) + .create(true) + .open(path) + .map_err(|e| ChatError::new(&format!("appendFile: {}", e), 1))?; + file.write_all(content.as_bytes()) + .map_err(|e| ChatError::new(&format!("appendFile: {}", e), 1))?; + Ok(Value::Unit) + } else { + Err(ChatError::new("appendFile expects a String path and String content", 1)) + } + }), + ); + + env.set( + "writeBinaryFile".to_string(), + builtin!("writeBinaryFile", 2, |args| { + if let [Value::String(path), Value::ByteString(bytes)] = &args[..] { + std::fs::write(path, bytes) + .map_err(|e| ChatError::new(&format!("writeBinaryFile: {}", e), 1))?; + Ok(Value::Unit) + } else { + Err(ChatError::new( + "writeBinaryFile expects a String path and a ByteString", + 1, + )) + } + }), + ); + + env.set( + "fileExists".to_string(), + builtin!("fileExists", 1, |args| { + if let [Value::String(path)] = &args[..] { + Ok(Value::Bool(std::path::Path::new(path).exists())) + } else { + Err(ChatError::new("fileExists expects a String path", 1)) + } + }), + ); + + env.set( + "fileSize".to_string(), + builtin!("fileSize", 1, |args| { + if let [Value::String(path)] = &args[..] { + let meta = + std::fs::metadata(path).map_err(|e| ChatError::new(&format!("fileSize: {}", e), 1))?; + Ok(Value::Num(Number::Int(meta.len() as i64))) + } else { + Err(ChatError::new("fileSize expects a String path", 1)) + } + }), + ); + + env.set( + "listDir".to_string(), + builtin!("listDir", 1, |args| { + if let [Value::String(path)] = &args[..] { + let entries = fs::read_dir(path) + .map_err(|e| ChatError::new(&format!("listDir: {}", e), 1))? + .filter_map(|e| e.ok().map(|e| e.file_name().to_string_lossy().to_string())) + .map(Value::String) + .collect(); + Ok(Value::List(entries)) + } else { + Err(ChatError::new("listDir expects a String path", 1)) + } + }), + ); + + env.set( + "createDir".to_string(), + builtin!("createDir", 1, |args| { + if let [Value::String(path)] = &args[..] { + fs::create_dir_all(path).map_err(|e| ChatError::new(&format!("createDir: {}", e), 1))?; + Ok(Value::Unit) + } else { + Err(ChatError::new("createDir expects a String path", 1)) + } + }), + ); + + env.set( + "removeDir".to_string(), + builtin!("removeDir", 1, |args| { + if let [Value::String(path)] = &args[..] { + fs::remove_dir_all(path).map_err(|e| ChatError::new(&format!("removeDir: {}", e), 1))?; + Ok(Value::Unit) + } else { + Err(ChatError::new("removeDir expects a String path", 1)) + } + }), + ); + + env.set( + "fileMove".to_string(), + builtin!("fileMove", 2, |args| { + if let [Value::String(from), Value::String(to)] = &args[..] { + fs::rename(from, to).map_err(|e| ChatError::new(&format!("fileMove: {}", e), 1))?; + Ok(Value::Unit) + } else { + Err(ChatError::new("fileMove expects two String paths", 1)) + } + }), + ); + + #[cfg(unix)] + env.set( + "filePermissions".to_string(), + builtin!("filePermissions", 1, |args| { + if let [Value::String(path)] = &args[..] { + use std::os::unix::fs::PermissionsExt; + let meta = + fs::metadata(path).map_err(|e| ChatError::new(&format!("filePermissions: {}", e), 1))?; + Ok(Value::Num(Number::Int(meta.permissions().mode() as i64))) + } else { + Err(ChatError::new("filePermissions expects a String path", 1)) + } + }), + ); + #[cfg(not(unix))] + env.set( + "filePermissions".to_string(), + builtin!("filePermissions", 1, |_args| Ok(Value::Num(Number::Int(0)))), + ); + + #[cfg(unix)] + env.set( + "setFilePermissions".to_string(), + builtin!("setFilePermissions", 2, |args| { + if let [Value::String(path), Value::Num(Number::Int(mode))] = &args[..] { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(path) + .map_err(|e| ChatError::new(&format!("setFilePermissions: {}", e), 1))? + .permissions(); + perms.set_mode(*mode as u32); + fs::set_permissions(path, perms) + .map_err(|e| ChatError::new(&format!("setFilePermissions: {}", e), 1))?; + Ok(Value::Unit) + } else { + Err(ChatError::new( + "setFilePermissions expects a String path and an Int (Num) mode", + 1, + )) + } + }), + ); + #[cfg(not(unix))] + env.set( + "setFilePermissions".to_string(), + builtin!("setFilePermissions", 2, |_args| Ok(Value::Unit)), + ); + + env.set( + "fetch".to_string(), + builtin!("fetch", 1, |args| { + if let [Value::String(url)] = &args[..] { + let resp = reqwest::blocking::get(url) + .map_err(|e| ChatError::new(&format!("fetch: {}", e), 1))?; + let status = resp.status().as_u16() as i64; + let headers: Vec<(String, String)> = resp + .headers() + .iter() + .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string())) + .collect(); + let body = resp.text().map_err(|e| ChatError::new(&format!("fetch: {}", e), 1))?; + Ok(Value::FetchResult(FetchResult { + status, + body, + headers, + })) + } else { + Err(ChatError::new("fetch expects a String URL", 1)) + } + }), + ); + + env.set( + "fetchOpts".to_string(), + builtin!("fetchOpts", 1, |args| { + if let [Value::FetchOptions(opts)] = &args[..] { + let client = reqwest::blocking::Client::new(); + let mut req = client.request( + opts.method.parse().unwrap_or(reqwest::Method::GET), + &opts.url, + ); + for (k, v) in &opts.headers { + req = req.header(k, v); + } + if let Some(b) = &opts.body { + req = req.body(b.clone()); + } + let resp = req.send().map_err(|e| ChatError::new(&format!("fetchOpts: {}", e), 1))?; + let status = resp.status().as_u16() as i64; + let resp_headers: Vec<(String, String)> = resp + .headers() + .iter() + .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string())) + .collect(); + let body_str = resp.text().map_err(|e| ChatError::new(&format!("fetchOpts: {}", e), 1))?; + Ok(Value::FetchResult(FetchResult { + status, + body: body_str, + headers: resp_headers, + })) + } else { + Err(ChatError::new("fetchOpts expects a FetchOptions value", 1)) + } + }), + ); + + let state_clone = state.clone(); + env.set( + "login".to_string(), + builtin!("login", 1, move |args| { + if let [Value::Uid(uid)] = &args[..] { + state_clone.lock().unwrap().login(uid.clone())?; + Ok(Value::Unit) + } else { + Err(ChatError::new("login expects a Uid argument", 1)) + } + }), + ); + + let state_clone = state.clone(); + env.set( + "logout".to_string(), + builtin!("logout", 0, move |_args| { + state_clone.lock().unwrap().logout()?; + Ok(Value::Unit) + }), + ); + + let state_clone = state.clone(); + env.set( + "deleteUser".to_string(), + builtin!("deleteUser", 1, move |args| { + if let [Value::Uid(uid)] = &args[..] { + state_clone.lock().unwrap().delete_user(uid)?; + Ok(Value::Unit) + } else { + Err(ChatError::new("deleteUser expects a Uid argument", 1)) + } + }), + ); + + let state_clone = state.clone(); + env.set( + "newChat".to_string(), + builtin!("newChat", 2, move |args| { + if let [Value::String(name), Value::List(members)] = &args[..] { + let uids: Vec = members + .iter() + .filter_map(|v| { + if let Value::Uid(u) = v { + Some(u.clone()) + } else { + None + } + }) + .collect(); + let mut state = state_clone.lock().unwrap(); + state.new_chat(name.clone(), uids.clone())?; + let sender = state + .current_user + .clone() + .ok_or(ChatError::new("Not logged in", 1))?; + let control = P2pControl::NewChat { + name: name.clone(), + members: uids.clone(), + from: sender.clone(), + }; + for uid in uids { + if uid != sender { + if let Some(addr) = state.get_contact(&uid) { + let _ = p2p::send_control(&addr, control.clone(), &sender); + } + } + } + Ok(Value::String(name.clone())) + } else { + Err(ChatError::new("newChat expects a String name and a list of Uids", 1)) + } + }), + ); + + let state_clone = state.clone(); + env.set( + "addMember".to_string(), + builtin!("addMember", 2, move |args| { + if let [Value::Uid(uid), Value::String(chat)] = &args[..] { + let mut state = state_clone.lock().unwrap(); + state.add_member(uid.clone(), chat)?; + let sender = state + .current_user + .clone() + .ok_or(ChatError::new("Not logged in", 1))?; + let control = P2pControl::AddMember { + chat: chat.clone(), + uid: uid.clone(), + }; + for member in state.members(chat)? { + if member != sender && member != *uid { + if let Some(addr) = state.get_contact(&member) { + let _ = p2p::send_control(&addr, control.clone(), &sender); + } + } + } + Ok(Value::Unit) + } else { + Err(ChatError::new( + "addMember expects a Uid and a String (chat name)", + 1, + )) + } + }), + ); + + let state_clone = state.clone(); + env.set( + "removeMember".to_string(), + builtin!("removeMember", 2, move |args| { + if let [Value::Uid(uid), Value::String(chat)] = &args[..] { + let mut state = state_clone.lock().unwrap(); + state.remove_member(uid, chat)?; + let sender = state + .current_user + .clone() + .ok_or(ChatError::new("Not logged in", 1))?; + let control = P2pControl::RemoveMember { + chat: chat.clone(), + uid: uid.clone(), + }; + for member in state.members(chat)? { + if member != sender && member != *uid { + if let Some(addr) = state.get_contact(&member) { + let _ = p2p::send_control(&addr, control.clone(), &sender); + } + } + } + Ok(Value::Unit) + } else { + Err(ChatError::new( + "removeMember expects a Uid and a String (chat name)", + 1, + )) + } + }), + ); + + let state_clone = state.clone(); + env.set( + "deleteChat".to_string(), + builtin!("deleteChat", 1, move |args| { + if let [Value::String(chat)] = &args[..] { + let mut state = state_clone.lock().unwrap(); + let members = state.members(chat)?; + state.delete_chat(chat)?; + let sender = state + .current_user + .clone() + .ok_or(ChatError::new("Not logged in", 1))?; + let control = P2pControl::DeleteChat { name: chat.clone() }; + for uid in members { + if uid != sender { + if let Some(addr) = state.get_contact(&uid) { + let _ = p2p::send_control(&addr, control.clone(), &sender); + } + } + } + Ok(Value::Unit) + } else { + Err(ChatError::new("deleteChat expects a String (chat name)", 1)) + } + }), + ); + + let state_clone = state.clone(); + env.set( + "open".to_string(), + builtin!("open", 1, move |args| { + if let [Value::String(chat)] = &args[..] { + state_clone.lock().unwrap().open_chat(chat.clone())?; + Ok(Value::Unit) + } else { + Err(ChatError::new("open expects a String (chat name)", 1)) + } + }), + ); + + let state_clone = state.clone(); + env.set( + "listChats".to_string(), + builtin!("listChats", 0, move |_args| { + let state = state_clone.lock().unwrap(); + let chats = state.list_chats(); + Ok(Value::List(chats.into_iter().map(Value::String).collect())) + }), + ); + + let state_clone = state.clone(); + env.set( + "members".to_string(), + builtin!("members", 1, move |args| { + if let [Value::String(chat)] = &args[..] { + let state = state_clone.lock().unwrap(); + let members = state.members(chat)?; + Ok(Value::List(members.into_iter().map(Value::Uid).collect())) + } else { + Err(ChatError::new("members expects a String (chat name)", 1)) + } + }), + ); + + let state_clone = state.clone(); + env.set( + "send".to_string(), + builtin!("send", 2, move |args| { + if let [Value::Uid(target), Value::String(text)] = &args[..] { + let (current_user, target_addr) = { + let state = state_clone.lock().unwrap(); + let current = state + .current_user + .clone() + .ok_or(ChatError::new("Not logged in", 1))?; + let addr = state + .get_contact(target) + .ok_or_else(|| ChatError::new(&format!("Target '{}' not in contacts", target), 1))?; + (current, addr) + }; + { + let mut state = state_clone.lock().unwrap(); + state.send_message(target, text)?; + } + let msg = P2pMessage { + msg_type: "msg".to_string(), + from: current_user.clone(), + to: target.clone(), + text: Some(text.clone()), + chat: None, + filename: None, + data_base64: None, + timestamp: Some(chrono::Utc::now().timestamp()), + control: None, + }; + if let Err(e) = p2p::send_message_to(&target_addr, msg) { + eprintln!("P2P send error: {}", e); + return Err(ChatError::new(&format!("P2P delivery failed: {}", e), 1)); + } + Ok(Value::Bool(true)) + } else { + Err(ChatError::new("send expects a Uid and a String (message text)", 1)) + } + }), + ); + + let state_clone = state.clone(); + env.set( + "sendFile".to_string(), + builtin!("sendFile", 2, move |args| { + if let [Value::Uid(target), Value::String(path)] = &args[..] { + let bytes = std::fs::read(&path) + .map_err(|e| ChatError::new(&format!("sendFile: {}", e), 1))?; + let b64 = STANDARD.encode(&bytes); + let filename = std::path::Path::new(&path) + .file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + let (current_user, target_addr) = { + let state = state_clone.lock().unwrap(); + let current = state + .current_user + .clone() + .ok_or(ChatError::new("Not logged in", 1))?; + let addr = state + .get_contact(target) + .ok_or_else(|| ChatError::new(&format!("Target '{}' not in contacts", target), 1))?; + (current, addr) + }; + let msg = P2pMessage { + msg_type: "file".to_string(), + from: current_user, + to: target.clone(), + text: None, + chat: None, + filename: Some(filename.clone()), + data_base64: Some(b64), + timestamp: Some(chrono::Utc::now().timestamp()), + control: None, + }; + if let Err(e) = p2p::send_message_to(&target_addr, msg) { + eprintln!("P2P send file error: {}", e); + return Err(ChatError::new(&format!("P2P delivery failed: {}", e), 1)); + } + Ok(Value::Bool(true)) + } else { + Err(ChatError::new("sendFile expects a Uid and a String (file path)", 1)) + } + }), + ); + + let state_clone = state.clone(); + env.set( + "sendChat".to_string(), + builtin!("sendChat", 2, move |args| { + if let [Value::String(chat), Value::String(text)] = &args[..] { + let mut state = state_clone.lock().unwrap(); + state.send_to_chat(chat, text)?; + let members = state.members(chat)?; + let sender = state + .current_user + .clone() + .ok_or(ChatError::new("Not logged in", 1))?; + let timestamp = chrono::Utc::now().timestamp(); + let control = P2pControl::ChatMessage { + chat: chat.clone(), + from: sender.clone(), + text: text.clone(), + timestamp, + }; + for uid in members { + if uid != sender { + if let Some(addr) = state.get_contact(&uid) { + let _ = p2p::send_control(&addr, control.clone(), &sender); + } + if text.contains(&format!("@{}", uid)) { + state.deliver_to_user( + uid.clone(), + crate::chat::Message { + from: sender.clone(), + text: text.clone(), + chat: chat.clone(), + timestamp, + }, + ); + } + } + } + Ok(Value::Bool(true)) + } else { + Err(ChatError::new( + "sendChat expects a String (chat name) and a String (message text)", + 1, + )) + } + }), + ); + + let state_clone = state.clone(); + env.set( + "sendFileToChat".to_string(), + builtin!("sendFileToChat", 2, move |args| { + if let [Value::String(chat), Value::String(path)] = &args[..] { + let bytes = std::fs::read(&path) + .map_err(|e| ChatError::new(&format!("sendFileToChat: {}", e), 1))?; + let b64 = STANDARD.encode(&bytes); + let filename = std::path::Path::new(&path) + .file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + let state = state_clone.lock().unwrap(); + let members = state.members(chat)?; + let sender = state + .current_user + .clone() + .ok_or(ChatError::new("Not logged in", 1))?; + let msg = P2pMessage { + msg_type: "file".to_string(), + from: sender.clone(), + to: "".to_string(), + text: None, + chat: Some(chat.clone()), + filename: Some(filename.clone()), + data_base64: Some(b64), + timestamp: Some(chrono::Utc::now().timestamp()), + control: None, + }; + for uid in members { + if uid != sender { + if let Some(addr) = state.get_contact(&uid) { + let _ = p2p::send_message_to(&addr, msg.clone()); + } + } + } + Ok(Value::Bool(true)) + } else { + Err(ChatError::new( + "sendFileToChat expects a String (chat name) and a String (file path)", + 1, + )) + } + }), + ); + + let state_clone = state.clone(); + env.set( + "inbox".to_string(), + builtin!("inbox", 0, move |_args| { + let state = state_clone.lock().unwrap(); + let user = state + .current_user + .clone() + .ok_or(ChatError::new("Not logged in", 1))?; + let msgs = state.get_inbox(&user); + Ok(Value::List(msgs)) + }), + ); + + let state_clone = state.clone(); + env.set( + "history".to_string(), + builtin!("history", 1, move |args| { + if let [Value::String(chat)] = &args[..] { + let msgs = state_clone.lock().unwrap().get_history(chat); + Ok(Value::List(msgs)) + } else { + Err(ChatError::new("history expects a String (chat name)", 1)) + } + }), + ); + + let state_clone = state.clone(); + env.set( + "downloads".to_string(), + builtin!("downloads", 0, move |_args| { + let state = state_clone.lock().unwrap(); + let downloads = state.downloads.clone(); + Ok(Value::List(downloads)) + }), + ); + + let state_clone = state.clone(); + env.set( + "saveFile".to_string(), + builtin!("saveFile", 2, move |args| { + if let [Value::Num(Number::Int(index)), Value::String(path)] = &args[..] { + let mut state = state_clone.lock().unwrap(); + state.save_file(*index as usize, path)?; + Ok(Value::Bool(true)) + } else { + Err(ChatError::new( + "saveFile expects an Int (Num) index and a String path", + 1, + )) + } + }), + ); + + let state_clone = state.clone(); + env.set( + "serverStart".to_string(), + builtin!("serverStart", 2, move |args| { + let (addr, password) = match &args[..] { + [Value::String(addr), Value::String(pass)] => (addr.clone(), Some(pass.clone())), + [Value::String(addr)] => (addr.clone(), None), + _ => { + return Err(ChatError::new( + "serverStart expects a String address and optional String password", + 1, + )) + } + }; + let mut state = state_clone.lock().unwrap(); + if state.server_handle.is_some() { + return Ok(Value::Unit); + } + match server::start_contacts_server(&addr, password) { + Ok((_db, handle, stop_tx)) => { + state.server_handle = Some(handle); + state.server_stop = Some(stop_tx); + state.contact_server_addr = Some(addr); + Ok(Value::Unit) + } + Err(e) => Err(ChatError::new(&format!("Failed to start server: {}", e), 1)), + } + }), + ); + + let state_clone = state.clone(); + env.set( + "serverStop".to_string(), + builtin!("serverStop", 0, move |_args| { + let mut state = state_clone.lock().unwrap(); + if let Some(stop_tx) = state.server_stop.take() { + let _ = stop_tx.send(()); + if let Some(handle) = state.server_handle.take() { + let _ = handle.join(); + } + state.contact_server_addr = None; + } + Ok(Value::Unit) + }), + ); + + let state_clone = state.clone(); + env.set( + "connect".to_string(), + builtin!("connect", 3, move |args| { + let (host, uid, password) = match &args[..] { + [Value::String(host), Value::Uid(uid), Value::String(pass)] => { + (host.clone(), uid.clone(), Some(pass.clone())) + } + [Value::String(host), Value::Uid(uid)] => (host.clone(), uid.clone(), None), + _ => { + return Err(ChatError::new( + "connect expects a String host, a Uid, and optional String password", + 1, + )) + } + }; + use std::io::{BufRead, Write}; + let stream = std::net::TcpStream::connect(&host) + .map_err(|e| ChatError::new(&format!("connect: {}", e), 1))?; + let connector = p2p::get_tls_connector(); + let mut stream = connector + .connect("localhost", stream) + .map_err(|e| ChatError::new(&format!("connect: {}", e), 1))?; + if let Some(pass) = password { + writeln!(stream, "PASSWORD {}", pass) + .map_err(|e| ChatError::new(&format!("connect: {}", e), 1))?; + let mut response = String::new(); + let mut reader = std::io::BufReader::new(&mut stream); + reader + .read_line(&mut response) + .map_err(|e| ChatError::new(&format!("connect: {}", e), 1))?; + if !response.trim().starts_with("OK") { + return Err(ChatError::new("Authentication failed", 1)); + } + } + let p2p_port = state_clone.lock().unwrap().p2p_port; + let own_addr = { + let state = state_clone.lock().unwrap(); + if let Some(ip) = &state.external_ip { + format!("{}:{}", ip, p2p_port) + } else { + format!("127.0.0.1:{}", p2p_port) + } + }; + writeln!(stream, "REGISTER {} {}", uid, own_addr) + .map_err(|e| ChatError::new(&format!("connect: {}", e), 1))?; + writeln!(stream, "GET").map_err(|e| ChatError::new(&format!("connect: {}", e), 1))?; + let mut response = String::new(); + let mut reader = std::io::BufReader::new(stream); + while let Ok(n) = reader.read_line(&mut response) { + if n == 0 { + break; + } + } + let mut state = state_clone.lock().unwrap(); + for line in response.lines() { + let parts: Vec<&str> = line.splitn(2, ' ').collect(); + if parts.len() == 2 { + state + .contacts + .insert(parts[0].to_string(), parts[1].to_string()); + } + } + let count = state.contacts.len() as i64; + Ok(Value::Num(Number::Int(count))) + }), + ); + + let state_clone = state.clone(); + env.set( + "addContact".to_string(), + builtin!("addContact", 2, move |args| { + if let [Value::Uid(uid), Value::String(addr)] = &args[..] { + state_clone.lock().unwrap().add_contact(uid.clone(), addr.clone()); + Ok(Value::Unit) + } else { + Err(ChatError::new("addContact expects a Uid and a String address", 1)) + } + }), + ); + + let state_clone = state.clone(); + env.set( + "removeContact".to_string(), + builtin!("removeContact", 1, move |args| { + if let [Value::Uid(uid)] = &args[..] { + state_clone.lock().unwrap().remove_contact(uid); + Ok(Value::Unit) + } else { + Err(ChatError::new("removeContact expects a Uid argument", 1)) + } + }), + ); + + env.set( + "getPublicIP".to_string(), + builtin!("getPublicIP", 0, |_args| { + match get("https://api.ipify.org") { + Ok(resp) => { + if resp.status().is_success() { + if let Ok(ip) = resp.text() { + return Ok(Value::Maybe(Some(Box::new(Value::String( + ip.trim().to_string(), + ))))); + } + } + Ok(Value::Maybe(None)) + } + Err(_) => Ok(Value::Maybe(None)), + } + }), + ); + + let state_ip = state.clone(); + env.set( + "setExternalIP".to_string(), + builtin!("setExternalIP", 1, move |args| { + if let [Value::String(ip)] = &args[..] { + let mut state = state_ip.lock().unwrap(); + state.external_ip = Some(ip.clone()); + Ok(Value::Unit) + } else { + Err(ChatError::new("setExternalIP expects a String IP address", 1)) + } + }), + ); + + let state_spawn = state.clone(); + env.set( + "spawn".to_string(), + builtin!("spawn", 1, move |args| { + if let [closure] = &args[..] { + spawn_process(closure.clone(), Arc::clone(&state_spawn)) + } else { + Err(ChatError::new("spawn expects a function", 1)) + } + }), + ); + + env.set( + "procSelf".to_string(), + builtin!("procSelf", 0, |_args| proc_self()), + ); + + env.set( + "procSend".to_string(), + builtin!("procSend", 2, |args| { + if let [Value::Pid(pid), val] = &args[..] { + proc_send(*pid, val.clone()) + } else { + Err(ChatError::new("procSend expects a Pid and a value", 1)) + } + }), + ); + + env.set( + "procRecv".to_string(), + builtin!("procRecv", 0, |_args| proc_recv()), + ); + + env.set( + "procWait".to_string(), + builtin!("procWait", 1, |args| { + if let [Value::Pid(pid)] = &args[..] { + proc_wait(*pid) + } else { + Err(ChatError::new("procWait expects a Pid argument", 1)) + } + }), + ); + + env.set( + "procExit".to_string(), + builtin!("procExit", 1, |args| { + if let [val] = &args[..] { + proc_exit(val.clone()) + } else { + Err(ChatError::new("procExit expects a value", 1)) + } + }), + ); + + env.set( + "sleep".to_string(), + builtin!("sleep", 1, |args| { + if let [Value::Duration(dur)] = &args[..] { + sleep(*dur) + } else { + Err(ChatError::new("sleep expects a Duration argument", 1)) + } + }), + ); + + let state_after = state.clone(); + env.set( + "after".to_string(), + builtin!("after", 2, move |args| { + if let [Value::Duration(dur), closure] = &args[..] { + after(*dur, closure.clone(), Arc::clone(&state_after)) + } else { + Err(ChatError::new( + "after expects a Duration and a nullary function", + 1, + )) + } + }), + ); + + env.set("Nothing".to_string(), Value::Maybe(None)); + env.set( + "Just".to_string(), + builtin!("Just", 1, |args| { + if args.len() == 1 { + Ok(Value::Maybe(Some(Box::new(args[0].clone())))) + } else { + Err(ChatError::new("Just expects exactly one argument", 1)) + } + }), + ); + + let state_maybe = state.clone(); + env.set( + "maybe".to_string(), + builtin!("maybe", 3, move |args| { + if let [Value::Closure(params, body, env), default, Value::Maybe(maybe_val)] = &args[..] { + let val = match maybe_val { + Some(v) => *v.clone(), + None => default.clone(), + }; + let mut new_env = env.clone(); + new_env.set(params[0].clone(), val); + eval::eval_expr(&body, &mut new_env, Arc::clone(&state_maybe)) + } else { + Err(ChatError::new( + "maybe expects a function, a default value, and a Maybe", + 1, + )) + } + }), + ); + + env.set( + "mapGet".to_string(), + builtin!("mapGet", 2, |args| { + if let [Value::Map(map), key] = &args[..] { + let key_str = key.display(); + Ok(map.get(&key_str).cloned().unwrap_or(Value::Unit)) + } else { + Err(ChatError::new("mapGet expects a Map and a key", 1)) + } + }), + ); + + env.set( + "mapSet".to_string(), + builtin!("mapSet", 3, |args| { + if let [Value::Map(map), key, val] = &args[..] { + let mut new_map = map.clone(); + new_map.insert(key.display(), val.clone()); + Ok(Value::Map(new_map)) + } else { + Err(ChatError::new("mapSet expects a Map, a key, and a value", 1)) + } + }), + ); + + env.set( + "mapRemove".to_string(), + builtin!("mapRemove", 2, |args| { + if let [Value::Map(map), key] = &args[..] { + let mut new_map = map.clone(); + new_map.remove(&key.display()); + Ok(Value::Map(new_map)) + } else { + Err(ChatError::new("mapRemove expects a Map and a key", 1)) + } + }), + ); + + env.set( + "mapKeys".to_string(), + builtin!("mapKeys", 1, |args| { + if let [Value::Map(map)] = &args[..] { + let keys: Vec = map.keys().map(|s| Value::String(s.clone())).collect(); + Ok(Value::List(keys)) + } else { + Err(ChatError::new("mapKeys expects a Map argument", 1)) + } + }), + ); + + env.set( + "mapValues".to_string(), + builtin!("mapValues", 1, |args| { + if let [Value::Map(map)] = &args[..] { + let values: Vec = map.values().cloned().collect(); + Ok(Value::List(values)) + } else { + Err(ChatError::new("mapValues expects a Map argument", 1)) + } + }), + ); + + env.set( + "mapEntries".to_string(), + builtin!("mapEntries", 1, |args| { + if let [Value::Map(map)] = &args[..] { + let entries: Vec = map + .iter() + .map(|(k, v)| Value::Tuple(vec![Value::String(k.clone()), v.clone()])) + .collect(); + Ok(Value::List(entries)) + } else { + Err(ChatError::new("mapEntries expects a Map argument", 1)) + } + }), + ); + + env.set( + "mapContains".to_string(), + builtin!("mapContains", 2, |args| { + if let [Value::Map(map), key] = &args[..] { + Ok(Value::Bool(map.contains_key(&key.display()))) + } else { + Err(ChatError::new("mapContains expects a Map and a key", 1)) + } + }), + ); + + env.set( + "mapSize".to_string(), + builtin!("mapSize", 1, |args| { + if let [Value::Map(map)] = &args[..] { + Ok(Value::Num(Number::Int(map.len() as i64))) + } else { + Err(ChatError::new("mapSize expects a Map argument", 1)) + } + }), + ); + + let state_mf = state.clone(); + env.set( + "mapFilter".to_string(), + Value::BuiltinFunc( + "mapFilter".to_string(), + 2, + Arc::new(move |args| { + if let [Value::Closure(params, body, closure_env), Value::Map(map)] = &args[..] { + let mut new_map = BTreeMap::new(); + let state_ref = state_mf.clone(); + for (k, v) in map { + let mut env = closure_env.clone(); + env.set(params[0].clone(), Value::String(k.clone())); + env.set(params[1].clone(), v.clone()); + let val = eval::eval_expr(&body, &mut env, state_ref.clone())?; + if let Value::Bool(b) = val { + if b { + new_map.insert(k.clone(), v.clone()); + } + } else { + return Err(ChatError::new( + "mapFilter predicate must return a Bool value", + 1, + )); + } + } + Ok(Value::Map(new_map)) + } else { + Err(ChatError::new( + "mapFilter expects a function (k -> v -> Bool) and a Map", + 1, + )) + } + }), + ), + ); + + env.set( + "mapMerge".to_string(), + builtin!("mapMerge", 2, |args| { + if let [Value::Map(a), Value::Map(b)] = &args[..] { + let mut merged = a.clone(); + for (k, v) in b { + merged.insert(k.clone(), v.clone()); + } + Ok(Value::Map(merged)) + } else { + Err(ChatError::new("mapMerge expects two Map arguments", 1)) + } + }), + ); + + env.set( + "setAdd".to_string(), + builtin!("setAdd", 2, |args| { + if let [Value::Set(set), elem] = &args[..] { + let mut new_set = set.clone(); + new_set.insert(elem.display()); + Ok(Value::Set(new_set)) + } else { + Err(ChatError::new("setAdd expects a Set and an element", 1)) + } + }), + ); + + env.set( + "setRemove".to_string(), + builtin!("setRemove", 2, |args| { + if let [Value::Set(set), elem] = &args[..] { + let mut new_set = set.clone(); + new_set.remove(&elem.display()); + Ok(Value::Set(new_set)) + } else { + Err(ChatError::new("setRemove expects a Set and an element", 1)) + } + }), + ); + + env.set( + "setContains".to_string(), + builtin!("setContains", 2, |args| { + if let [Value::Set(set), elem] = &args[..] { + Ok(Value::Bool(set.contains(&elem.display()))) + } else { + Err(ChatError::new("setContains expects a Set and an element", 1)) + } + }), + ); + + env.set( + "setUnion".to_string(), + builtin!("setUnion", 2, |args| { + if let [Value::Set(a), Value::Set(b)] = &args[..] { + let union: BTreeSet<_> = a.union(b).cloned().collect(); + Ok(Value::Set(union)) + } else { + Err(ChatError::new("setUnion expects two Set arguments", 1)) + } + }), + ); + + env.set( + "setIntersection".to_string(), + builtin!("setIntersection", 2, |args| { + if let [Value::Set(a), Value::Set(b)] = &args[..] { + let inter: BTreeSet<_> = a.intersection(b).cloned().collect(); + Ok(Value::Set(inter)) + } else { + Err(ChatError::new("setIntersection expects two Set arguments", 1)) + } + }), + ); + + env.set( + "setDifference".to_string(), + builtin!("setDifference", 2, |args| { + if let [Value::Set(a), Value::Set(b)] = &args[..] { + let diff: BTreeSet<_> = a.difference(b).cloned().collect(); + Ok(Value::Set(diff)) + } else { + Err(ChatError::new("setDifference expects two Set arguments", 1)) + } + }), + ); + + env.set( + "setSize".to_string(), + builtin!("setSize", 1, |args| { + if let [Value::Set(set)] = &args[..] { + Ok(Value::Num(Number::Int(set.len() as i64))) + } else { + Err(ChatError::new("setSize expects a Set argument", 1)) + } + }), + ); + + let state_sf = state.clone(); + env.set( + "setFilter".to_string(), + Value::BuiltinFunc( + "setFilter".to_string(), + 2, + Arc::new(move |args| { + if let [Value::Closure(params, body, closure_env), Value::Set(set)] = &args[..] { + let mut new_set = BTreeSet::new(); + let state_ref = state_sf.clone(); + for item in set { + let mut env = closure_env.clone(); + env.set(params[0].clone(), Value::String(item.clone())); + let val = eval::eval_expr(&body, &mut env, state_ref.clone())?; + if let Value::Bool(b) = val { + if b { + new_set.insert(item.clone()); + } + } else { + return Err(ChatError::new( + "setFilter predicate must return a Bool value", + 1, + )); + } + } + Ok(Value::Set(new_set)) + } else { + Err(ChatError::new( + "setFilter expects a function (a -> Bool) and a Set", + 1, + )) + } + }), + ), + ); + + let state_sm = state.clone(); + env.set( + "setMap".to_string(), + Value::BuiltinFunc( + "setMap".to_string(), + 2, + Arc::new(move |args| { + if let [Value::Closure(params, body, closure_env), Value::Set(set)] = &args[..] { + let mut new_set = BTreeSet::new(); + let state_ref = state_sm.clone(); + for item in set { + let mut env = closure_env.clone(); + env.set(params[0].clone(), Value::String(item.clone())); + let val = eval::eval_expr(&body, &mut env, state_ref.clone())?; + new_set.insert(val.display()); + } + Ok(Value::Set(new_set)) + } else { + Err(ChatError::new( + "setMap expects a function (a -> b) and a Set", + 1, + )) + } + }), + ), + ); + + env.set( + "listToSet".to_string(), + builtin!("listToSet", 1, |args| { + if let [Value::List(list)] = &args[..] { + let set: BTreeSet<_> = list.iter().map(|v| v.display()).collect(); + Ok(Value::Set(set)) + } else { + Err(ChatError::new("listToSet expects a List argument", 1)) + } + }), + ); + + env.set( + "mapToList".to_string(), + builtin!("mapToList", 1, |args| { + if let [Value::Map(map)] = &args[..] { + let list: Vec = map + .iter() + .map(|(k, v)| Value::Tuple(vec![Value::String(k.clone()), v.clone()])) + .collect(); + Ok(Value::List(list)) + } else { + Err(ChatError::new("mapToList expects a Map argument", 1)) + } + }), + ); + + env.set( + "sha256".to_string(), + builtin!("sha256", 1, |args| { + if let [Value::ByteString(data)] = &args[..] { + let hash = Sha256::digest(data); + Ok(Value::ByteString(hash.to_vec())) + } else { + Err(ChatError::new("sha256 expects a ByteString argument", 1)) + } + }), + ); + + env.set( + "sha256String".to_string(), + builtin!("sha256String", 1, |args| { + if let [Value::String(s)] = &args[..] { + let hash = Sha256::digest(s.as_bytes()); + Ok(Value::String(hex::encode(hash))) + } else { + Err(ChatError::new("sha256String expects a String argument", 1)) + } + }), + ); + + env.set( + "kyberKeyPair".to_string(), + builtin!("kyberKeyPair", 0, |_args| { + let (pk, sk) = keypair(); + Ok(Value::Tuple(vec![ + Value::ByteString(pk.as_bytes().to_vec()), + Value::ByteString(sk.as_bytes().to_vec()), + ])) + }), + ); + + env.set( + "kyberEncapsulate".to_string(), + builtin!("kyberEncapsulate", 1, |args| { + if let [Value::ByteString(pk_bytes)] = &args[..] { + let pk = pqcrypto_kyber::kyber768::PublicKey::from_bytes(pk_bytes) + .map_err(|_| ChatError::new("Invalid Kyber public key", 1))?; + let (ciphertext, shared_secret) = encapsulate(&pk); + Ok(Value::Tuple(vec![ + Value::ByteString(ciphertext.as_bytes().to_vec()), + Value::ByteString(shared_secret.as_bytes().to_vec()), + ])) + } else { + Err(ChatError::new( + "kyberEncapsulate expects a ByteString (public key)", + 1, + )) + } + }), + ); + + env.set( + "kyberDecapsulate".to_string(), + builtin!("kyberDecapsulate", 2, |args| { + if let [Value::ByteString(sk_bytes), Value::ByteString(ciphertext_bytes)] = &args[..] { + let sk = pqcrypto_kyber::kyber768::SecretKey::from_bytes(sk_bytes) + .map_err(|_| ChatError::new("Invalid Kyber secret key", 1))?; + let ciphertext = pqcrypto_kyber::kyber768::Ciphertext::from_bytes(ciphertext_bytes) + .map_err(|_| ChatError::new("Invalid Kyber ciphertext", 1))?; + let shared_secret = decapsulate(&ciphertext, &sk); + Ok(Value::ByteString(shared_secret.as_bytes().to_vec())) + } else { + Err(ChatError::new( + "kyberDecapsulate expects SecretKey and Ciphertext as ByteStrings", + 1, + )) + } + }), + ); + + env.set( + "exit".to_string(), + builtin!("exit", 0, |_args| { + std::process::exit(0); + }), + ); + + let state_load = state.clone(); + let global_env_load = global_env.clone(); + env.set( + "load".to_string(), + builtin!("load", 1, move |args| { + if let [Value::String(path)] = &args[..] { + let content = match fs::read_to_string(&path) { + Ok(c) => c, + Err(e) => return Err(ChatError::new(&format!("Failed to read file: {}", e), 1)), + }; + let expr = match crate::parser::parse_script(&content) { + Ok(e) => e, + Err(e) => return Err(ChatError::new(&format!("Parse error: {}", e), 1)), + }; + let mut env_guard = global_env_load.lock().unwrap(); + match crate::eval::eval_expr(&expr, &mut env_guard, state_load.clone()) { + Ok(_) => Ok(Value::Unit), + Err(e) => Err(ChatError::new(&format!("Execution error: {}", e), 1)), + } + } else { + Err(ChatError::new("load expects a String (file path)", 1)) + } + }), + ); + + let global_env_del = global_env.clone(); + env.set( + "del".to_string(), + builtin!("del", 1, move |args| { + if let [Value::String(name)] = &args[..] { + let mut env_guard = global_env_del.lock().unwrap(); + if env_guard.vars.remove(name).is_some() { + env_guard.type_map.remove(name); + Ok(Value::Unit) + } else { + Err(ChatError::new(&format!("Variable '{}' not found", name), 1)) + } + } else { + Err(ChatError::new("del expects a String (variable name)", 1)) + } + }), + ); + + { + let mut state_guard = state.lock().unwrap(); + if state_guard.p2p_port == 0 { + state_guard.p2p_port = 19000; + drop(state_guard); + p2p::start_p2p_listener(19000, state.clone()); + } + } +} + +fn serde_json_to_chatlang(v: SerdeValue) -> JsonValue { + match v { + SerdeValue::Null => JsonValue::Null, + SerdeValue::Bool(b) => JsonValue::Bool(b), + SerdeValue::Number(n) => JsonValue::Number(n.as_f64().unwrap_or(0.0)), + SerdeValue::String(s) => JsonValue::String(s), + SerdeValue::Array(arr) => { + let items: Vec = arr.into_iter().map(serde_json_to_chatlang).collect(); + JsonValue::Array(items) + } + SerdeValue::Object(map) => { + let mut obj = BTreeMap::new(); + for (k, v) in map { + obj.insert(k, serde_json_to_chatlang(v)); + } + JsonValue::Object(obj) + } + } +} + +fn chatlang_json_to_serde(j: &JsonValue) -> SerdeValue { + match j { + JsonValue::Null => SerdeValue::Null, + JsonValue::Bool(b) => SerdeValue::Bool(*b), + JsonValue::Number(n) => { + SerdeValue::Number(serde_json::Number::from_f64(*n).unwrap_or(serde_json::Number::from(0))) + } + JsonValue::String(s) => SerdeValue::String(s.clone()), + JsonValue::Array(arr) => { + let items: Vec = arr.iter().map(chatlang_json_to_serde).collect(); + SerdeValue::Array(items) + } + JsonValue::Object(map) => { + let mut obj = serde_json::Map::new(); + for (k, v) in map { + obj.insert(k.clone(), chatlang_json_to_serde(v)); + } + SerdeValue::Object(obj) + } + } +} diff --git a/src/chat.rs b/src/chat.rs new file mode 100644 index 0000000..9c68142 --- /dev/null +++ b/src/chat.rs @@ -0,0 +1,292 @@ +//! Chat state and logic with control messages. + +use crate::error::ChatError; +use crate::types::Value; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::sync::mpsc::Sender; +use std::thread::JoinHandle; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct User { + pub uid: String, + pub addr: Option, + pub online: bool, +} + +#[derive(Debug, Clone)] +pub struct Message { + pub from: String, + pub text: String, + pub chat: String, + pub timestamp: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum P2pControl { + NewChat { name: String, members: Vec, from: String }, + AddMember { chat: String, uid: String }, + RemoveMember { chat: String, uid: String }, + DeleteChat { name: String }, + ChatMessage { chat: String, from: String, text: String, timestamp: i64 }, +} + +pub struct ChatState { + pub users: BTreeMap, + pub chats: BTreeMap>, + pub messages: BTreeMap>, + pub inbox: BTreeMap>, + pub current_user: Option, + pub p2p_port: u16, + pub contact_server_addr: Option, + pub contacts: BTreeMap, + pub downloads: Vec, + pub server_handle: Option>, + pub server_stop: Option>, + pub external_ip: Option, +} + +impl ChatState { + pub fn new() -> Self { + ChatState { + users: BTreeMap::new(), + chats: BTreeMap::new(), + messages: BTreeMap::new(), + inbox: BTreeMap::new(), + current_user: None, + p2p_port: 0, + contact_server_addr: None, + contacts: BTreeMap::new(), + downloads: Vec::new(), + server_handle: None, + server_stop: None, + external_ip: None, + } + } + + pub fn login(&mut self, uid: String) -> Result<(), ChatError> { + if !uid.starts_with('@') { + return Err(ChatError::new("Invalid UID (must start with @)", 1)); + } + self.users.entry(uid.clone()).or_insert(User { + uid: uid.clone(), + addr: None, + online: true, + }); + self.current_user = Some(uid); + Ok(()) + } + + pub fn logout(&mut self) -> Result<(), ChatError> { + if let Some(uid) = &self.current_user { + if let Some(user) = self.users.get_mut(uid) { + user.online = false; + } + self.current_user = None; + Ok(()) + } else { + Err(ChatError::new("Not logged in", 1)) + } + } + + pub fn delete_user(&mut self, uid: &str) -> Result<(), ChatError> { + if self.current_user.as_deref() == Some(uid) { + self.logout()?; + } + if self.users.remove(uid).is_some() { + for members in self.chats.values_mut() { + members.retain(|u| u != uid); + } + self.contacts.remove(uid); + self.inbox.remove(uid); + Ok(()) + } else { + Err(ChatError::new("User not found", 1)) + } + } + + pub fn new_chat(&mut self, name: String, members: Vec) -> Result<(), ChatError> { + if self.chats.contains_key(&name) { + return Err(ChatError::new("Chat already exists", 1)); + } + self.chats.insert(name.clone(), members); + self.messages.entry(name).or_default(); + Ok(()) + } + + pub fn add_member(&mut self, uid: String, chat: &str) -> Result<(), ChatError> { + if let Some(members) = self.chats.get_mut(chat) { + if !members.contains(&uid) { + members.push(uid); + } + Ok(()) + } else { + Err(ChatError::new("Chat not found", 1)) + } + } + + pub fn remove_member(&mut self, uid: &str, chat: &str) -> Result<(), ChatError> { + if let Some(members) = self.chats.get_mut(chat) { + members.retain(|u| u != uid); + Ok(()) + } else { + Err(ChatError::new("Chat not found", 1)) + } + } + + pub fn delete_chat(&mut self, name: &str) -> Result<(), ChatError> { + if self.chats.remove(name).is_some() { + self.messages.remove(name); + Ok(()) + } else { + Err(ChatError::new("Chat not found", 1)) + } + } + + pub fn list_chats(&self) -> Vec { + self.chats.keys().cloned().collect() + } + + pub fn members(&self, chat: &str) -> Result, ChatError> { + self.chats.get(chat).cloned().ok_or_else(|| ChatError::new("Chat not found", 1)) + } + + pub fn open_chat(&mut self, chat: String) -> Result<(), ChatError> { + if self.chats.contains_key(&chat) { + Ok(()) + } else { + Err(ChatError::new("Chat not found", 1)) + } + } + + pub fn send_message(&mut self, target: &str, text: &str) -> Result<(), ChatError> { + let sender = self.current_user.clone().ok_or(ChatError::new("Not logged in", 1))?; + let msg = Message { + from: sender.clone(), + text: text.to_string(), + chat: String::new(), + timestamp: chrono::Utc::now().timestamp(), + }; + if target == "@everyone" { + let all_recipients: Vec = self.chats.values() + .flat_map(|members| members.iter().cloned()) + .collect(); + for uid in all_recipients { + self.deliver_to_user(uid, msg.clone()); + } + } else { + self.deliver_to_user(target.to_string(), msg); + } + Ok(()) + } + + pub fn send_to_chat(&mut self, chat: &str, text: &str) -> Result<(), ChatError> { + let sender = self.current_user.clone().ok_or(ChatError::new("Not logged in", 1))?; + let members = self.chats.get(chat) + .ok_or(ChatError::new("Chat not found", 1))? + .clone(); + let msg = Message { + from: sender.clone(), + text: text.to_string(), + chat: chat.to_string(), + timestamp: chrono::Utc::now().timestamp(), + }; + self.messages.entry(chat.to_string()).or_default().push(msg.clone()); + for uid in members { + if uid != sender { + self.deliver_to_user(uid, msg.clone()); + } + } + Ok(()) + } + + pub fn deliver_to_user(&mut self, uid: String, msg: Message) { + self.inbox.entry(uid).or_default().push(msg); + } + + pub fn get_inbox(&self, user: &str) -> Vec { + self.inbox.get(user).cloned().unwrap_or_default().into_iter().map(|m| Value::ChatMsg { + from: m.from, + text: m.text, + chat: m.chat, + attachment: None, + }).collect() + } + + pub fn get_history(&self, chat: &str) -> Vec { + self.messages.get(chat).cloned().unwrap_or_default().into_iter().map(|m| Value::ChatMsg { + from: m.from, + text: m.text, + chat: m.chat, + attachment: None, + }).collect() + } + + pub fn save_file(&mut self, index: usize, path: &str) -> Result<(), ChatError> { + if index >= self.downloads.len() { + return Err(ChatError::new("Index out of bounds", 1)); + } + let val = &self.downloads[index]; + if let Value::FileTransfer { data, .. } = val { + std::fs::write(path, data).map_err(|e| ChatError::new(&e.to_string(), 1))?; + Ok(()) + } else { + Err(ChatError::new("Not a file transfer", 1)) + } + } + + pub fn add_contact(&mut self, uid: String, addr: String) { + self.contacts.insert(uid, addr); + } + + pub fn remove_contact(&mut self, uid: &str) { + self.contacts.remove(uid); + } + + pub fn get_contact(&self, uid: &str) -> Option { + self.contacts.get(uid).cloned() + } + + pub fn handle_control(&mut self, control: &P2pControl) -> Result<(), ChatError> { + match control { + P2pControl::NewChat { name, members, from: _ } => { + if !self.chats.contains_key(name) { + self.chats.insert(name.clone(), members.clone()); + self.messages.entry(name.clone()).or_default(); + } + } + P2pControl::AddMember { chat, uid } => { + if let Some(m) = self.chats.get_mut(chat) { + if !m.contains(uid) { + m.push(uid.clone()); + } + } + } + P2pControl::RemoveMember { chat, uid } => { + if let Some(m) = self.chats.get_mut(chat) { + m.retain(|u| u != uid); + } + } + P2pControl::DeleteChat { name } => { + self.chats.remove(name); + self.messages.remove(name); + } + P2pControl::ChatMessage { chat, from, text, timestamp } => { + let msg = Message { + from: from.clone(), + text: text.clone(), + chat: chat.clone(), + timestamp: *timestamp, + }; + self.messages.entry(chat.clone()).or_default().push(msg.clone()); + // Also deliver to inbox if mentioned + if let Some(current) = &self.current_user { + if text.contains(&format!("@{}", current)) { + self.deliver_to_user(current.clone(), msg); + } + } + } + } + Ok(()) + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..19bb3a6 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,48 @@ +//! Error types with location support. + +use crate::ast::Span; +use std::fmt; + +#[derive(Debug, Clone)] +pub struct ChatError { + pub message: String, + pub code: i32, + pub span: Option, + pub file: Option, +} + +impl ChatError { + pub fn new(msg: &str, code: i32) -> Self { + ChatError { message: msg.to_string(), code, span: None, file: None } + } + pub fn with_span(msg: &str, code: i32, span: Span) -> Self { + ChatError { message: msg.to_string(), code, span: Some(span), file: None } + } + pub fn with_file(msg: &str, code: i32, file: String) -> Self { + ChatError { message: msg.to_string(), code, span: None, file: Some(file) } + } +} + +impl fmt::Display for ChatError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(span) = self.span { + write!(f, "{} at {:?}", self.message, span) + } else if let Some(file) = &self.file { + write!(f, "{} in file {}", self.message, file) + } else { + write!(f, "{} (code {})", self.message, self.code) + } + } +} + +impl From for ChatError { + fn from(e: std::io::Error) -> Self { + ChatError::new(&e.to_string(), 2) + } +} + +impl From for ChatError { + fn from(e: reqwest::Error) -> Self { + ChatError::new(&e.to_string(), 4) + } +} diff --git a/src/eval.rs b/src/eval.rs new file mode 100644 index 0000000..7653e33 --- /dev/null +++ b/src/eval.rs @@ -0,0 +1,1023 @@ +//! Evaluator with currying, Num unification, loops, break, and list comprehensions. + +use crate::ast::*; +use crate::chat::ChatState; +use crate::error::ChatError; +use crate::types::{Environment, Value, Number}; +use std::collections::BTreeMap; +use std::collections::BTreeSet; +use std::sync::{Arc, Mutex}; +use std::thread; +use std::sync::mpsc::{self, Sender, Receiver}; +use once_cell::sync::Lazy; +use std::time::Duration; + +pub struct Process { + pub sender: Sender, + pub exit_sender: Sender, + pub exit_receiver: Receiver, + pub thread: Option>, +} + +pub static PROCESS_MANAGER: Lazy>> = Lazy::new(|| Mutex::new(BTreeMap::new())); +pub static NEXT_PID: Lazy> = Lazy::new(|| Mutex::new(1)); + +thread_local! { + static CURRENT_PID: std::cell::RefCell> = const { std::cell::RefCell::new(None) }; + static CURRENT_RECEIVER: std::cell::RefCell>> = const { std::cell::RefCell::new(None) }; +} + +pub fn eval_expr( + expr: &Expr, + env: &mut Environment, + state: Arc>, +) -> Result { + match expr { + Expr::Lit(lit, _) => eval_literal(lit), + Expr::Var(name, span) => { + let val = env.get(name).ok_or_else(|| ChatError::with_span(&format!("Undefined variable '{}'", name), 1, *span))?; + if let Value::BuiltinFunc(_, 0, f) = val { + f(vec![]) + } else { + Ok(val) + } + } + Expr::Lambda(params, body, _) => Ok(Value::Closure(params.clone(), *body.clone(), env.clone())), + Expr::App(func, arg, span) => { + let f = eval_expr(func, env, Arc::clone(&state))?; + let a = eval_expr(arg, env, Arc::clone(&state))?; + apply(f, a, env, state, *span) + } + Expr::If(cond, then_expr, else_expr, span) => { + let c = eval_expr(cond, env, Arc::clone(&state))?; + match c { + Value::Bool(true) => eval_expr(then_expr, env, Arc::clone(&state)), + Value::Bool(false) => eval_expr(else_expr, env, Arc::clone(&state)), + _ => Err(ChatError::with_span("Condition must be Bool", 1, *span)), + } + } + Expr::Let { name, type_ann, def, body, span } => { + if env.vars.contains_key(name) { + return Err(ChatError::with_span(&format!("variable '{}' already defined", name), 1, *span)); + } + let val = eval_expr(def, env, Arc::clone(&state))?; + if let Some(ann) = type_ann { + let actual_type = type_name(&val); + if *ann != actual_type { + return Err(ChatError::with_span(&format!("Type mismatch: expected '{}', got '{}'", ann, actual_type), 1, *span)); + } + env.type_map.insert(name.clone(), ann.clone()); + } + env.set(name.clone(), val.clone()); + match body { + Some(b) => eval_expr(b, env, Arc::clone(&state)), + None => Ok(val), + } + } + Expr::Assign(name, expr, span) => { + let val = eval_expr(expr, env, Arc::clone(&state))?; + if !env.vars.contains_key(name) { + return Err(ChatError::with_span(&format!("Variable '{}' not defined for assignment", name), 1, *span)); + } + if let Some(expected_type) = env.type_map.get(name) { + let actual_type = type_name(&val); + if expected_type != &actual_type { + return Err(ChatError::with_span(&format!("Type mismatch: expected '{}', got '{}'", expected_type, actual_type), 1, *span)); + } + } + env.set(name.clone(), val.clone()); + Ok(val) + } + Expr::Case(scrut, arms, span) => { + let val = eval_expr(scrut, env, Arc::clone(&state))?; + for (pat, expr) in arms { + if let Some(bindings) = match_pattern(pat, &val) { + let mut new_env = env.clone(); + for (k, v) in bindings { + new_env.set(k, v); + } + return eval_expr(expr, &mut new_env, Arc::clone(&state)); + } + } + Err(ChatError::with_span("Non-exhaustive patterns", 1, *span)) + } + Expr::Try(body, _span) => { + match eval_expr(body, env, Arc::clone(&state)) { + Ok(v) => Ok(v), + Err(e) => Err(e), + } + } + Expr::Catch(body, pat, handler, _span) => { + match eval_expr(body, env, Arc::clone(&state)) { + Ok(v) => Ok(v), + Err(err) => { + let err_val = Value::String(err.message.clone()); + if let Some(bindings) = match_pattern(pat, &err_val) { + let mut new_env = env.clone(); + for (k, v) in bindings { + new_env.set(k, v); + } + eval_expr(handler, &mut new_env, Arc::clone(&state)) + } else { + Err(err) + } + } + } + } + Expr::Throw(msg, span) => { + let msg_val = eval_expr(msg, env, Arc::clone(&state))?; + Err(ChatError::with_span(&msg_val.display(), 1, *span)) + } + Expr::DataDef(_, _, _, _) => Ok(Value::Unit), + Expr::StructDef(_, _, _) => Ok(Value::Unit), + Expr::StructNew(_name, fields, _span) => { + let mut map = BTreeMap::new(); + for (f, e) in fields { + map.insert(f.clone(), eval_expr(e, env, Arc::clone(&state))?); + } + Ok(Value::Record(map)) + } + Expr::Constructor(_name, args, _span) => { + let mut vals = Vec::new(); + for arg in args { + vals.push(eval_expr(arg, env, Arc::clone(&state))?); + } + Ok(Value::Custom(_name.clone(), vals)) + } + Expr::Record(fields, _span) => { + let mut map = BTreeMap::new(); + for (k, v) in fields { + map.insert(k.clone(), eval_expr(v, env, Arc::clone(&state))?); + } + Ok(Value::Record(map)) + } + Expr::FieldAccess(expr, field, span) => { + let val = eval_expr(expr, env, Arc::clone(&state))?; + match val { + Value::Record(map) => { + map.get(field).cloned().ok_or_else(|| ChatError::with_span(&format!("Field '{}' not found", field), 1, *span)) + } + Value::ClassInstance { fields, .. } => { + fields.get(field).cloned().ok_or_else(|| ChatError::with_span(&format!("Field '{}' not found", field), 1, *span)) + } + _ => Err(ChatError::with_span("Field access on non-record or non-class", 1, *span)), + } + } + Expr::RecordUpdate(expr, updates, span) => { + let val = eval_expr(expr, env, Arc::clone(&state))?; + match val { + Value::Record(mut map) => { + for (k, v) in updates { + map.insert(k.clone(), eval_expr(v, env, Arc::clone(&state))?); + } + Ok(Value::Record(map)) + } + _ => Err(ChatError::with_span("Record update on non-record", 1, *span)), + } + } + Expr::BinOp(op, l, r, span) => { + let left = eval_expr(l, env, Arc::clone(&state))?; + let right = eval_expr(r, env, Arc::clone(&state))?; + eval_binop(op, left, right, *span) + } + Expr::Concat(l, r, span) => { + let left = eval_expr(l, env, Arc::clone(&state))?; + let right = eval_expr(r, env, Arc::clone(&state))?; + match (left, right) { + (Value::String(a), Value::String(b)) => Ok(Value::String(a + &b)), + (Value::List(mut a), Value::List(b)) => { a.extend(b); Ok(Value::List(a)) }, + _ => Err(ChatError::with_span("++ works on strings or lists", 1, *span)), + } + } + Expr::FString(parts, _span) => { + let mut result = String::new(); + for part in parts { + match part { + FStringPart::Literal(s) => result.push_str(&s), + FStringPart::Expr(e) => { + let val = eval_expr(e, env, Arc::clone(&state))?; + result.push_str(&val.display()); + } + } + } + Ok(Value::String(result)) + } + Expr::List(elems, _span) => { + let vals: Result, _> = elems.iter().map(|e| eval_expr(e, env, Arc::clone(&state))).collect(); + Ok(Value::List(vals?)) + } + Expr::Range(start, end, span) => { + let s = eval_expr(start, env, Arc::clone(&state))?; + let e = eval_expr(end, env, Arc::clone(&state))?; + match (s, e) { + (Value::Num(Number::Int(a)), Value::Num(Number::Int(b))) => { + let mut list = Vec::new(); + for i in a..b { + list.push(Value::Num(Number::Int(i))); + } + Ok(Value::List(list)) + } + _ => Err(ChatError::with_span("Range requires Ints", 1, *span)), + } + } + Expr::Pipe(left, right, span) => { + let l = eval_expr(left, env, Arc::clone(&state))?; + apply(eval_expr(right, env, Arc::clone(&state))?, l, env, state, *span) + } + Expr::Dollar(func, arg, span) => { + let f = eval_expr(func, env, Arc::clone(&state))?; + let a = eval_expr(arg, env, Arc::clone(&state))?; + apply(f, a, env, state, *span) + } + Expr::LogicalAnd(l, r, span) => { + match eval_expr(l, env, Arc::clone(&state))? { + Value::Bool(true) => eval_expr(r, env, Arc::clone(&state)), + Value::Bool(false) => Ok(Value::Bool(false)), + _ => Err(ChatError::with_span("and requires Bool", 1, *span)), + } + } + Expr::LogicalOr(l, r, span) => { + match eval_expr(l, env, Arc::clone(&state))? { + Value::Bool(true) => Ok(Value::Bool(true)), + Value::Bool(false) => eval_expr(r, env, Arc::clone(&state)), + _ => Err(ChatError::with_span("or requires Bool", 1, *span)), + } + } + Expr::Not(expr, span) => { + match eval_expr(expr, env, Arc::clone(&state))? { + Value::Bool(b) => Ok(Value::Bool(!b)), + _ => Err(ChatError::with_span("not requires Bool", 1, *span)), + } + } + Expr::Tuple(elems, _span) => { + let vals: Result, _> = elems.iter().map(|e| eval_expr(e, env, Arc::clone(&state))).collect(); + Ok(Value::Tuple(vals?)) + } + Expr::Index(list, idx, span) => { + let coll = eval_expr(list, env, Arc::clone(&state))?; + let index = eval_expr(idx, env, Arc::clone(&state))?; + // Automatic float to int conversion for indexing + let index_int = match index { + Value::Num(Number::Int(i)) => i, + Value::Num(Number::Float(f)) => f as i64, + _ => return Err(ChatError::with_span("Index must be numeric (Int or Float)", 1, *span)), + }; + match coll { + Value::Map(map) => { + let key_str = index.display(); + if let Some(val) = map.get(&key_str) { + Ok(val.clone()) + } else { + Ok(Value::Unit) + } + } + Value::Set(set) => { + Ok(Value::Bool(set.contains(&index.display()))) + } + Value::List(v) => { + let i = index_int; + if i < 0 || i as usize >= v.len() { + Err(ChatError::with_span("Index out of bounds", 1, *span)) + } else { + Ok(v[i as usize].clone()) + } + } + Value::String(s) => { + let i = index_int; + if i < 0 || i as usize >= s.len() { + Err(ChatError::with_span("Index out of bounds", 1, *span)) + } else { + Ok(Value::Char(s.chars().nth(i as usize).unwrap())) + } + } + Value::ByteString(b) => { + let i = index_int; + if i < 0 || i as usize >= b.len() { + Err(ChatError::with_span("Index out of bounds", 1, *span)) + } else { + Ok(Value::Num(Number::Int(b[i as usize] as i64))) + } + } + _ => Err(ChatError::with_span("Indexing requires list, string, byte string, map, or set", 1, *span)), + } + } + Expr::For(var, iterable, body, span) => { + let iter_val = eval_expr(iterable, env, Arc::clone(&state))?; + match iter_val { + Value::List(list) => { + for item in list { + let mut new_env = env.clone(); + new_env.set(var.clone(), item); + eval_expr(body, &mut new_env, Arc::clone(&state))?; + } + Ok(Value::Unit) + } + Value::Set(set) => { + for item in set { + let mut new_env = env.clone(); + new_env.set(var.clone(), Value::String(item)); + eval_expr(body, &mut new_env, Arc::clone(&state))?; + } + Ok(Value::Unit) + } + Value::Map(map) => { + for (k, v) in map { + let mut new_env = env.clone(); + new_env.set(var.clone(), Value::Tuple(vec![Value::String(k), v])); + eval_expr(body, &mut new_env, Arc::clone(&state))?; + } + Ok(Value::Unit) + } + _ => Err(ChatError::with_span("for requires list, set, or map", 1, *span)), + } + } + Expr::While(cond, body, span) => { + loop { + let c = eval_expr(cond, env, Arc::clone(&state))?; + match c { + Value::Bool(true) => { + eval_expr(body, env, Arc::clone(&state))?; + } + Value::Bool(false) => break, + _ => return Err(ChatError::with_span("while condition must be Bool", 1, *span)), + } + } + Ok(Value::Unit) + } + Expr::Loop(body, _span) => { + loop { + match eval_expr(body, env, Arc::clone(&state)) { + Ok(Value::Break(Some(val))) => return Ok(*val), + Ok(Value::Break(None)) => return Ok(Value::Unit), + Ok(_) => continue, + Err(e) => return Err(e), + } + } + } + Expr::Break(opt, _span) => { + let val = match opt { + Some(e) => eval_expr(e, env, Arc::clone(&state))?, + None => Value::Unit, + }; + Ok(Value::Break(Some(Box::new(val)))) + } + Expr::Block(exprs, _span) => { + let mut block_env = env.clone(); + let mut result = Value::Unit; + for e in exprs { + result = eval_expr(e, &mut block_env, Arc::clone(&state))?; + } + for (k, v) in block_env.vars { + if !env.vars.contains_key(&k) { + env.set(k, v); + } + } + Ok(result) + } + Expr::ClassDef { name, extends, fields, methods, span: _ } => { + let mut class_info = BTreeMap::new(); + if let Some(parent) = extends { + class_info.insert("extends".to_string(), Value::String(parent.clone())); + } else { + class_info.insert("extends".to_string(), Value::Unit); + } + let mut method_env = Environment::new(); + for m in methods { + let closure = Value::Closure( + m.params.clone(), + *m.body.clone(), + env.clone(), + ); + method_env.set(m.name.clone(), closure); + } + let field_names: Vec = fields.iter().map(|(n, _)| Value::String(n.clone())).collect(); + class_info.insert("fields".to_string(), Value::List(field_names)); + let mut method_map = BTreeMap::new(); + for (k, v) in method_env.vars { + method_map.insert(k, v); + } + class_info.insert("methods".to_string(), Value::Record(method_map)); + env.set(name.clone(), Value::Record(class_info)); + Ok(Value::Unit) + } + Expr::New(class_name, args, span) => { + let class_def = env.get(class_name) + .ok_or_else(|| ChatError::with_span(&format!("Class '{}' not defined", class_name), 1, *span))?; + if let Value::Record(info) = class_def { + let fields = if let Some(Value::List(field_list)) = info.get("fields") { + field_list.iter().filter_map(|v| { + if let Value::String(s) = v { Some(s.clone()) } else { None } + }).collect::>() + } else { + Vec::new() + }; + let mut field_values = BTreeMap::new(); + for (i, field) in fields.iter().enumerate() { + if i < args.len() { + let val = eval_expr(&args[i], env, Arc::clone(&state))?; + field_values.insert(field.clone(), val); + } else { + field_values.insert(field.clone(), Value::Unit); + } + } + let instance = Value::ClassInstance { + class: class_name.clone(), + fields: field_values, + }; + Ok(instance) + } else { + Err(ChatError::with_span("Invalid class definition", 1, *span)) + } + } + Expr::MethodCall(obj, method, args, span) => { + let obj_val = eval_expr(obj, env, Arc::clone(&state))?; + match obj_val { + Value::ClassInstance { ref class, .. } => { + let class_def = env.get(class) + .ok_or_else(|| ChatError::with_span(&format!("Class '{}' not found", class), 1, *span))?; + let method_val = find_method(&class_def, method, env)?; + let mut all_args = vec![obj_val.clone()]; + for a in args { + all_args.push(eval_expr(a, env, Arc::clone(&state))?); + } + match method_val { + Value::Closure(params, body, closure_env) => { + let mut new_env = closure_env.clone(); + if params.len() != all_args.len() { + return Err(ChatError::with_span("Wrong number of arguments", 1, *span)); + } + for (p, a) in params.iter().zip(all_args) { + new_env.set(p.clone(), a); + } + eval_expr(&body, &mut new_env, state) + } + _ => Err(ChatError::with_span("Method is not a function", 1, *span)), + } + } + _ => Err(ChatError::with_span("Method call on non-class instance", 1, *span)), + } + } + Expr::MapLiteral(entries, _span) => { + let mut map = BTreeMap::new(); + for (k, v) in entries { + let key = eval_expr(k, env, Arc::clone(&state))?; + let value = eval_expr(v, env, Arc::clone(&state))?; + map.insert(key.display(), value); + } + Ok(Value::Map(map)) + } + Expr::SetLiteral(elems, _span) => { + let mut set = BTreeSet::new(); + for e in elems { + let val = eval_expr(e, env, Arc::clone(&state))?; + set.insert(val.display()); + } + Ok(Value::Set(set)) + } + Expr::ListComp { expr, generators, filters, span: _ } => { + let mut result = Vec::new(); + fn eval_comp( + expr: &Expr, + gens: &[(String, Box)], + filters: &[Box], + env: &mut Environment, + state: Arc>, + acc: &mut Vec, + ) -> Result<(), ChatError> { + if gens.is_empty() { + // Check filters + for filter in filters { + let cond = eval_expr(filter, env, Arc::clone(&state))?; + if let Value::Bool(b) = cond { + if !b { return Ok(()); } + } else { + return Err(ChatError::with_span("Filter must be Bool", 1, e_span(filter))); + } + } + let val = eval_expr(expr, env, Arc::clone(&state))?; + acc.push(val); + return Ok(()); + } + let (var, iter_expr) = &gens[0]; + let iter_val = eval_expr(iter_expr, env, Arc::clone(&state))?; + let rest_gens = &gens[1..]; + match iter_val { + Value::List(list) => { + for item in list { + let mut new_env = env.clone(); + new_env.set(var.clone(), item); + eval_comp(expr, rest_gens, filters, &mut new_env, Arc::clone(&state), acc)?; + } + } + Value::Set(set) => { + for item in set { + let mut new_env = env.clone(); + new_env.set(var.clone(), Value::String(item)); + eval_comp(expr, rest_gens, filters, &mut new_env, Arc::clone(&state), acc)?; + } + } + Value::Map(map) => { + for (k, v) in map { + let mut new_env = env.clone(); + new_env.set(var.clone(), Value::Tuple(vec![Value::String(k), v])); + eval_comp(expr, rest_gens, filters, &mut new_env, Arc::clone(&state), acc)?; + } + } + _ => return Err(ChatError::with_span("Comprehension iterable must be list, set, or map", 1, e_span(iter_expr))), + } + Ok(()) + } + eval_comp(expr, generators, filters, env, Arc::clone(&state), &mut result)?; + Ok(Value::List(result)) + } + } +} + +fn e_span(e: &Expr) -> Span { + match e { + Expr::Lit(_, s) => *s, + Expr::Var(_, s) => *s, + Expr::Lambda(_, _, s) => *s, + Expr::App(_, _, s) => *s, + Expr::If(_, _, _, s) => *s, + Expr::Let { span, .. } => *span, + Expr::Assign(_, _, s) => *s, + Expr::Case(_, _, s) => *s, + Expr::Try(_, s) => *s, + Expr::Catch(_, _, _, s) => *s, + Expr::Throw(_, s) => *s, + Expr::DataDef(_, _, _, s) => *s, + Expr::StructDef(_, _, s) => *s, + Expr::StructNew(_, _, s) => *s, + Expr::Constructor(_, _, s) => *s, + Expr::Record(_, s) => *s, + Expr::FieldAccess(_, _, s) => *s, + Expr::RecordUpdate(_, _, s) => *s, + Expr::List(_, s) => *s, + Expr::Range(_, _, s) => *s, + Expr::BinOp(_, _, _, s) => *s, + Expr::Concat(_, _, s) => *s, + Expr::Pipe(_, _, s) => *s, + Expr::Dollar(_, _, s) => *s, + Expr::LogicalAnd(_, _, s) => *s, + Expr::LogicalOr(_, _, s) => *s, + Expr::Not(_, s) => *s, + Expr::Tuple(_, s) => *s, + Expr::Index(_, _, s) => *s, + Expr::For(_, _, _, s) => *s, + Expr::While(_, _, s) => *s, + Expr::Loop(_, s) => *s, + Expr::Break(_, s) => *s, + Expr::Block(_, s) => *s, + Expr::ClassDef { span, .. } => *span, + Expr::New(_, _, s) => *s, + Expr::MethodCall(_, _, _, s) => *s, + Expr::MapLiteral(_, s) => *s, + Expr::SetLiteral(_, s) => *s, + Expr::FString(_, s) => *s, + Expr::ListComp { span, .. } => *span, + } +} + +fn eval_literal(lit: &Literal) -> Result { + Ok(match lit { + Literal::Int(i) => Value::Num(Number::Int(*i)), + Literal::Float(f) => Value::Num(Number::Float(*f)), + Literal::Char(c) => Value::Char(*c), + Literal::String(s) => Value::String(s.clone()), + Literal::Bool(b) => Value::Bool(*b), + Literal::Unit => Value::Unit, + Literal::Uid(u) => Value::Uid(u.clone()), + Literal::ByteString(b) => Value::ByteString(b.clone()), + Literal::Duration(dur) => Value::Duration(*dur), + }) +} + +fn apply( + func: Value, + arg: Value, + _env: &mut Environment, + state: Arc>, + span: Span, +) -> Result { + match func { + Value::Closure(params, body, closure_env) => { + if params.is_empty() { + return Err(ChatError::with_span("Cannot apply nullary function", 1, span)); + } + let mut new_env = closure_env.clone(); + new_env.set(params[0].clone(), arg); + if params.len() == 1 { + eval_expr(&body, &mut new_env, state) + } else { + let remaining = params[1..].to_vec(); + Ok(Value::Closure(remaining, body, new_env)) + } + } + Value::BuiltinFunc(name, arity, f) => { + if arity == 0 { + f(vec![]) + } else if arity == 1 { + f(vec![arg]) + } else { + Ok(Value::Curry { + name, + arity, + f, + args: vec![arg], + }) + } + } + Value::Curry { name, arity, f, args } => { + let mut new_args = args; + new_args.push(arg); + if new_args.len() == arity { + f(new_args) + } else { + Ok(Value::Curry { + name, + arity, + f, + args: new_args, + }) + } + } + _ => Err(ChatError::with_span("Not a function", 1, span)), + } +} + +fn eval_binop(op: &BinOp, left: Value, right: Value, span: Span) -> Result { + match op { + BinOp::Add => { + if let (Value::String(_), _) = (&left, &right) { + return Err(ChatError::with_span("Use '++' to concatenate strings, not '+'", 1, span)); + } + if let (_, Value::String(_)) = (&left, &right) { + return Err(ChatError::with_span("Use '++' to concatenate strings, not '+'", 1, span)); + } + num_op(left, right, |a,b| a+b, |a,b| a+b, span) + } + BinOp::Sub => num_op(left, right, |a,b| a-b, |a,b| a-b, span), + BinOp::Mul => num_op(left, right, |a,b| a*b, |a,b| a*b, span), + BinOp::Div => match (left, right) { + (Value::Num(Number::Int(a)), Value::Num(Number::Int(b))) => { + if b == 0 { Err(ChatError::with_span("Division by zero", 1, span)) } + else { Ok(Value::Num(Number::Int(a / b))) } + } + (Value::Num(Number::Float(a)), Value::Num(Number::Float(b))) => { + if b == 0.0 { Err(ChatError::with_span("Division by zero", 1, span)) } + else { Ok(Value::Num(Number::Float(a / b))) } + } + (Value::Num(Number::Int(a)), Value::Num(Number::Float(b))) => { + if b == 0.0 { Err(ChatError::with_span("Division by zero", 1, span)) } + else { Ok(Value::Num(Number::Float(a as f64 / b))) } + } + (Value::Num(Number::Float(a)), Value::Num(Number::Int(b))) => { + if b == 0 { Err(ChatError::with_span("Division by zero", 1, span)) } + else { Ok(Value::Num(Number::Float(a / b as f64))) } + } + _ => Err(ChatError::with_span("Division requires Num", 1, span)), + }, + BinOp::Mod => match (left, right) { + (Value::Num(Number::Int(a)), Value::Num(Number::Int(b))) => { + if b == 0 { Err(ChatError::with_span("Modulo by zero", 1, span)) } + else { Ok(Value::Num(Number::Int(a % b))) } + } + _ => Err(ChatError::with_span("Modulo requires Int", 1, span)), + }, + BinOp::Eq => Ok(Value::Bool(left.display() == right.display())), + BinOp::Neq => Ok(Value::Bool(left.display() != right.display())), + BinOp::Lt => cmp_op(left, right, |a,b| a < b, span), + BinOp::Le => cmp_op(left, right, |a,b| a <= b, span), + BinOp::Gt => cmp_op(left, right, |a,b| a > b, span), + BinOp::Ge => cmp_op(left, right, |a,b| a >= b, span), + BinOp::Cons => match (left, right) { + (v, Value::List(mut list)) => { list.insert(0, v); Ok(Value::List(list)) } + _ => Err(ChatError::with_span("(:) requires element and list", 1, span)), + }, + BinOp::In => { + match right { + Value::List(list) => { + Ok(Value::Bool(list.iter().any(|x| x.display() == left.display()))) + } + Value::String(s) => { + if let Value::Char(c) = left { + Ok(Value::Bool(s.contains(c))) + } else { + Err(ChatError::with_span("in for string requires Char", 1, span)) + } + } + Value::ByteString(b) => { + if let Value::Num(Number::Int(i)) = left { + Ok(Value::Bool(b.contains(&(i as u8)))) + } else { + Err(ChatError::with_span("in for ByteString requires Int", 1, span)) + } + } + Value::Set(set) => { + Ok(Value::Bool(set.contains(&left.display()))) + } + Value::Map(map) => { + Ok(Value::Bool(map.contains_key(&left.display()))) + } + _ => Err(ChatError::with_span("in requires list, string, byte string, set, or map", 1, span)), + } + } + BinOp::NotIn => { + match eval_binop(&BinOp::In, left, right, span) { + Ok(Value::Bool(b)) => Ok(Value::Bool(!b)), + Err(e) => Err(e), + _ => Err(ChatError::with_span("not in failed", 1, span)), + } + } + } +} + +fn num_op(l: Value, r: Value, ifn: fn(i64, i64) -> i64, ffn: fn(f64, f64) -> f64, span: Span) -> Result { + match (l, r) { + (Value::Num(Number::Int(a)), Value::Num(Number::Int(b))) => Ok(Value::Num(Number::Int(ifn(a, b)))), + (Value::Num(Number::Float(a)), Value::Num(Number::Float(b))) => Ok(Value::Num(Number::Float(ffn(a, b)))), + (Value::Num(Number::Int(a)), Value::Num(Number::Float(b))) => Ok(Value::Num(Number::Float(ffn(a as f64, b)))), + (Value::Num(Number::Float(a)), Value::Num(Number::Int(b))) => Ok(Value::Num(Number::Float(ffn(a, b as f64)))), + _ => Err(ChatError::with_span("Arithmetic requires Num", 1, span)), + } +} + +fn cmp_op(l: Value, r: Value, f: fn(f64, f64) -> bool, _span: Span) -> Result { + let lf = to_f64(&l)?; + let rf = to_f64(&r)?; + Ok(Value::Bool(f(lf, rf))) +} + +fn to_f64(v: &Value) -> Result { + match v { + Value::Num(Number::Int(i)) => Ok(*i as f64), + Value::Num(Number::Float(x)) => Ok(*x), + _ => Err(ChatError::new("Comparison requires numeric value", 1)), + } +} + +pub fn match_pattern(pat: &Pattern, val: &Value) -> Option> { + match pat { + Pattern::Wildcard(_) => Some(BTreeMap::new()), + Pattern::Var(name, _) => { + let mut map = BTreeMap::new(); + map.insert(name.clone(), val.clone()); + Some(map) + } + Pattern::Literal(lit, _) => { + if let Ok(v) = eval_literal(lit) { + if v.display() == val.display() { + Some(BTreeMap::new()) + } else { None } + } else { None } + } + Pattern::Constructor(name, pats, _) => { + match val { + Value::Custom(cname, args) if cname == name => { + if pats.len() == args.len() { + let mut map = BTreeMap::new(); + for (p, a) in pats.iter().zip(args) { + if let Some(sub) = match_pattern(p, a) { + map.extend(sub); + } else { return None; } + } + Some(map) + } else { None } + } + _ => None, + } + } + Pattern::List(pats, _) => { + match val { + Value::List(vals) if pats.len() == vals.len() => { + let mut map = BTreeMap::new(); + for (p, v) in pats.iter().zip(vals) { + if let Some(sub) = match_pattern(p, v) { + map.extend(sub); + } else { return None; } + } + Some(map) + } + _ => None, + } + } + Pattern::Tuple(pats, _) => { + match val { + Value::Tuple(vals) if pats.len() == vals.len() => { + let mut map = BTreeMap::new(); + for (p, v) in pats.iter().zip(vals) { + if let Some(sub) = match_pattern(p, v) { + map.extend(sub); + } else { return None; } + } + Some(map) + } + _ => None, + } + } + Pattern::Record(field_pats, _) => { + match val { + Value::Record(map) => { + let mut bindings = BTreeMap::new(); + for (field, pat) in field_pats { + if let Some(v) = map.get(field) { + if let Some(sub) = match_pattern(pat, v) { + bindings.extend(sub); + } else { return None; } + } else { return None; } + } + Some(bindings) + } + Value::ClassInstance { fields, .. } => { + let mut bindings = BTreeMap::new(); + for (field, pat) in field_pats { + if let Some(v) = fields.get(field) { + if let Some(sub) = match_pattern(pat, v) { + bindings.extend(sub); + } else { return None; } + } else { return None; } + } + Some(bindings) + } + _ => None, + } + } + } +} + +pub fn spawn_process(closure: Value, state: Arc>) -> Result { + match closure { + Value::Closure(params, body, env) if params.is_empty() => { + let (sender, receiver) = mpsc::channel(); + let (exit_sender, exit_receiver) = mpsc::channel(); + let pid = { + let mut next = NEXT_PID.lock().unwrap(); + let id = *next; + *next += 1; + id + }; + + let exit_sender_clone = exit_sender.clone(); + let handle = thread::spawn(move || { + CURRENT_PID.with(|cell| { + *cell.borrow_mut() = Some(pid); + }); + CURRENT_RECEIVER.with(|cell| { + *cell.borrow_mut() = Some(receiver); + }); + + let mut new_env = env; + let result = eval_expr(&body, &mut new_env, state); + let exit_val = match result { + Ok(v) => v, + Err(e) => Value::String(e.message), + }; + let _ = exit_sender_clone.send(exit_val); + }); + + let proc = Process { + sender, + exit_sender, + exit_receiver, + thread: Some(handle), + }; + PROCESS_MANAGER.lock().unwrap().insert(pid, proc); + Ok(Value::Pid(pid)) + } + _ => Err(ChatError::new("spawn expects a nullary function", 1)), + } +} + +pub fn proc_self() -> Result { + CURRENT_PID.with(|cell| { + if let Some(pid) = *cell.borrow() { + Ok(Value::Pid(pid)) + } else { + Err(ChatError::new("Not inside a process", 1)) + } + }) +} + +pub fn proc_send(pid: usize, val: Value) -> Result { + let mut map = PROCESS_MANAGER.lock().unwrap(); + if let Some(proc) = map.get_mut(&pid) { + proc.sender.send(val).map_err(|_| ChatError::new("Process receiver dead", 1))?; + Ok(Value::Unit) + } else { + Err(ChatError::new("Process not found", 1)) + } +} + +pub fn proc_recv() -> Result { + CURRENT_RECEIVER.with(|cell| { + let receiver_opt = cell.borrow_mut().take(); + if let Some(receiver) = receiver_opt { + let result = receiver.recv().map_err(|_| ChatError::new("Process mailbox closed", 1)); + *cell.borrow_mut() = Some(receiver); + result + } else { + Err(ChatError::new("Not inside a process or no receiver", 1)) + } + }) +} + +pub fn proc_wait(pid: usize) -> Result { + let mut map = PROCESS_MANAGER.lock().unwrap(); + if let Some(proc) = map.remove(&pid) { + drop(map); + let exit_val = proc.exit_receiver.recv() + .map_err(|_| ChatError::new("Process exited without sending value", 1))?; + if let Some(handle) = proc.thread { + let _ = handle.join(); + } + Ok(exit_val) + } else { + Err(ChatError::new("Process not found", 1)) + } +} + +pub fn proc_exit(val: Value) -> Result { + let pid = CURRENT_PID.with(|cell| *cell.borrow()); + if let Some(pid) = pid { + let map = PROCESS_MANAGER.lock().unwrap(); + if let Some(proc) = map.get(&pid) { + let exit_sender = proc.exit_sender.clone(); + drop(map); + let _ = exit_sender.send(val); + return Ok(Value::Unit); + } + } + Err(ChatError::new("Not inside a process", 1)) +} + +pub fn sleep(dur: Duration) -> Result { + thread::sleep(dur); + Ok(Value::Unit) +} + +pub fn after(dur: Duration, closure: Value, state: Arc>) -> Result { + match closure { + Value::Closure(params, body, env) if params.is_empty() => { + thread::spawn(move || { + thread::sleep(dur); + let mut new_env = env; + let _ = eval_expr(&body, &mut new_env, state); + }); + Ok(Value::Unit) + } + _ => Err(ChatError::new("after expects a nullary function", 1)), + } +} + +pub fn type_name(val: &Value) -> String { + match val { + Value::Num(_) => "Num".to_string(), + Value::Char(_) => "Char".to_string(), + Value::String(_) => "String".to_string(), + Value::Bool(_) => "Bool".to_string(), + Value::Unit => "Unit".to_string(), + Value::Uid(_) => "Uid".to_string(), + Value::ByteString(_) => "ByteString".to_string(), + Value::List(_) => "List".to_string(), + Value::Tuple(_) => "Tuple".to_string(), + Value::Closure(_, _, _) => "Closure".to_string(), + Value::BuiltinFunc(_, _, _) => "BuiltinFunc".to_string(), + Value::Curry { .. } => "Curry".to_string(), + Value::Custom(_, _) => "Custom".to_string(), + Value::Record(_) => "Record".to_string(), + Value::Pid(_) => "Pid".to_string(), + Value::DateTime(_) => "DateTime".to_string(), + Value::Duration(_) => "Duration".to_string(), + Value::Json(_) => "Json".to_string(), + Value::Maybe(_) => "Maybe".to_string(), + Value::Either(_) => "Either".to_string(), + Value::ChatMsg { .. } => "ChatMsg".to_string(), + Value::FileInfo { .. } => "FileInfo".to_string(), + Value::FileTransfer { .. } => "FileTransfer".to_string(), + Value::FetchOptions(_) => "FetchOptions".to_string(), + Value::FetchResult(_) => "FetchResult".to_string(), + Value::Map(_) => "Map".to_string(), + Value::Set(_) => "Set".to_string(), + Value::ClassInstance { .. } => "ClassInstance".to_string(), + Value::Break(_) => "Break".to_string(), + } +} + +fn find_method(class_def: &Value, method: &str, env: &Environment) -> Result { + if let Value::Record(info) = class_def { + if let Some(Value::Record(methods)) = info.get("methods") { + if let Some(m) = methods.get(method) { + return Ok(m.clone()); + } + } + if let Some(Value::String(parent_name)) = info.get("extends") { + if !parent_name.is_empty() { + let parent_def = env.get(parent_name) + .ok_or_else(|| ChatError::new(&format!("Parent class '{}' not found", parent_name), 1))?; + return find_method(&parent_def, method, env); + } + } + } + Err(ChatError::new(&format!("Method '{}' not found in class hierarchy", method), 1)) +} diff --git a/src/lexer.rs b/src/lexer.rs new file mode 100644 index 0000000..ac4a08b --- /dev/null +++ b/src/lexer.rs @@ -0,0 +1,606 @@ +//! Lexer with position tracking. + +use crate::ast::Literal; +use std::time::Duration; + +#[derive(Debug, Clone, PartialEq)] +pub enum LexError { + UnexpectedChar(char, usize), + UnterminatedString(usize), + InvalidEscape(char, usize), + InvalidHexByteString(String, usize), + InvalidNumber(String, usize), + InvalidDuration(String, usize), + UnexpectedEof, +} + +impl std::fmt::Display for LexError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + LexError::UnexpectedChar(c, pos) => write!(f, "unexpected character '{}' at position {}", c, pos), + LexError::UnterminatedString(pos) => write!(f, "unterminated string literal at position {}", pos), + LexError::InvalidEscape(c, pos) => write!(f, "invalid escape sequence '\\{}' at position {}", c, pos), + LexError::InvalidHexByteString(s, pos) => write!(f, "invalid hex byte string '{}' at position {}", s, pos), + LexError::InvalidNumber(s, pos) => write!(f, "invalid number '{}' at position {}", s, pos), + LexError::InvalidDuration(s, pos) => write!(f, "invalid duration '{}' at position {}", s, pos), + LexError::UnexpectedEof => write!(f, "unexpected end of input"), + } + } +} + +impl std::error::Error for LexError {} + +#[derive(Debug, Clone, PartialEq)] +pub enum TokenKind { + INDENT, + DEDENT, + NEWLINE, + LParen, + RParen, + LBrace, + RBrace, + LBracket, + RBracket, + Comma, + Semicolon, + Backslash, + Pipe, + Arrow, + FatArrow, + Assign, + Colon, + DoubleColon, + Cons, + Concat, + Plus, + Minus, + Star, + Slash, + Percent, + Not, + Eq, + Neq, + Lt, + Le, + Gt, + Ge, + And, + Or, + Dollar, + Dot, + DotDot, + If, + Then, + Else, + Let, + In, + Case, + Of, + Data, + Struct, + Try, + Catch, + Error, + For, + While, + Lambda, + Class, + Extends, + New, + Loop, + Break, + Ident(String), + Literal(Literal), + FString(String), + Eof, +} + +#[derive(Debug, Clone)] +pub struct Token { + pub kind: TokenKind, + pub start: usize, + pub end: usize, +} + +impl Token { + pub fn span(&self) -> (usize, usize) { + (self.start, self.end) + } +} + +pub struct Lexer { + chars: Vec, + pos: usize, + indent_stack: Vec, + at_line_start: bool, + line_start_spaces: usize, + pending_tokens: Vec, +} + +impl Lexer { + pub fn new(input: &str) -> Self { + Lexer { + chars: input.chars().collect(), + pos: 0, + indent_stack: vec![0], + at_line_start: true, + line_start_spaces: 0, + pending_tokens: Vec::new(), + } + } + + fn current(&self) -> Option { + self.chars.get(self.pos).copied() + } + + fn advance(&mut self) { + self.pos += 1; + } + + pub fn tokenize(&mut self) -> Result, LexError> { + let mut tokens = Vec::new(); + loop { + match self.next_token()? { + Some(Token { kind: TokenKind::Eof, start, end }) => { + while self.indent_stack.len() > 1 { + self.indent_stack.pop(); + tokens.push(Token { kind: TokenKind::DEDENT, start, end }); + } + tokens.push(Token { kind: TokenKind::Eof, start, end }); + break; + } + Some(tok) => tokens.push(tok), + None => break, + } + } + Ok(tokens) + } + + fn next_token(&mut self) -> Result, LexError> { + if let Some(tok) = self.pending_tokens.pop() { + return Ok(Some(tok)); + } + + self.skip_whitespace()?; + + let c = match self.current() { + Some(ch) => ch, + None => return Ok(None), + }; + + let start = self.pos; + + if c == '\n' { + self.advance(); + let end = self.pos; + self.at_line_start = true; + return Ok(Some(Token { kind: TokenKind::NEWLINE, start, end })); + } + + if self.at_line_start { + self.line_start_spaces = 0; + while let Some(' ') = self.current() { + self.line_start_spaces += 1; + self.advance(); + } + self.at_line_start = false; + let current_indent = *self.indent_stack.last().unwrap(); + let end = self.pos; + if self.line_start_spaces > current_indent { + self.indent_stack.push(self.line_start_spaces); + return Ok(Some(Token { kind: TokenKind::INDENT, start, end })); + } else if self.line_start_spaces < current_indent { + let mut dedents = Vec::new(); + while self.indent_stack.last().map(|&x| x > self.line_start_spaces).unwrap_or(false) { + self.indent_stack.pop(); + dedents.push(Token { kind: TokenKind::DEDENT, start, end }); + } + self.pending_tokens.extend(dedents.into_iter().rev()); + if let Some(tok) = self.pending_tokens.pop() { + return Ok(Some(tok)); + } + } + } + + self.read_token(start) + } + + fn read_token(&mut self, start: usize) -> Result, LexError> { + let c = self.current().unwrap(); + let end = self.pos + 1; + match c { + '(' => { self.advance(); Ok(Some(Token { kind: TokenKind::LParen, start, end })) } + ')' => { self.advance(); Ok(Some(Token { kind: TokenKind::RParen, start, end })) } + '{' => { self.advance(); Ok(Some(Token { kind: TokenKind::LBrace, start, end })) } + '}' => { self.advance(); Ok(Some(Token { kind: TokenKind::RBrace, start, end })) } + '[' => { self.advance(); Ok(Some(Token { kind: TokenKind::LBracket, start, end })) } + ']' => { self.advance(); Ok(Some(Token { kind: TokenKind::RBracket, start, end })) } + ',' => { self.advance(); Ok(Some(Token { kind: TokenKind::Comma, start, end })) } + ';' => { self.advance(); Ok(Some(Token { kind: TokenKind::Semicolon, start, end })) } + '\\' => { self.advance(); Ok(Some(Token { kind: TokenKind::Backslash, start, end })) } + '|' => { + self.advance(); + if self.current() == Some('>') { + self.advance(); + Ok(Some(Token { kind: TokenKind::Pipe, start, end })) + } else { + Ok(Some(Token { kind: TokenKind::Pipe, start, end })) + } + } + '=' => { + self.advance(); + if self.current() == Some('>') { + self.advance(); + Ok(Some(Token { kind: TokenKind::FatArrow, start, end })) + } else if self.current() == Some('=') { + self.advance(); + Ok(Some(Token { kind: TokenKind::Eq, start, end })) + } else { + Ok(Some(Token { kind: TokenKind::Assign, start, end })) + } + } + ':' => { + self.advance(); + if self.current() == Some(':') { + self.advance(); + Ok(Some(Token { kind: TokenKind::DoubleColon, start, end })) + } else { + Ok(Some(Token { kind: TokenKind::Colon, start, end })) + } + } + '+' => { + self.advance(); + if self.current() == Some('+') { + self.advance(); + Ok(Some(Token { kind: TokenKind::Concat, start, end })) + } else { + Ok(Some(Token { kind: TokenKind::Plus, start, end })) + } + } + '-' => { + self.advance(); + if self.current() == Some('>') { + self.advance(); + Ok(Some(Token { kind: TokenKind::Arrow, start, end })) + } else { + Ok(Some(Token { kind: TokenKind::Minus, start, end })) + } + } + '*' => { self.advance(); Ok(Some(Token { kind: TokenKind::Star, start, end })) } + '/' => { + self.advance(); + if self.current() == Some('=') { + self.advance(); + Ok(Some(Token { kind: TokenKind::Neq, start, end })) + } else { + Ok(Some(Token { kind: TokenKind::Slash, start, end })) + } + } + '%' => { self.advance(); Ok(Some(Token { kind: TokenKind::Percent, start, end })) } + '!' => { + self.advance(); + if self.current() == Some('=') { + self.advance(); + Ok(Some(Token { kind: TokenKind::Neq, start, end })) + } else { + Ok(Some(Token { kind: TokenKind::Not, start, end })) + } + } + '<' => { + self.advance(); + if self.current() == Some('=') { + self.advance(); + Ok(Some(Token { kind: TokenKind::Le, start, end })) + } else { + Ok(Some(Token { kind: TokenKind::Lt, start, end })) + } + } + '>' => { + self.advance(); + if self.current() == Some('=') { + self.advance(); + Ok(Some(Token { kind: TokenKind::Ge, start, end })) + } else { + Ok(Some(Token { kind: TokenKind::Gt, start, end })) + } + } + '&' => { self.advance(); Ok(Some(Token { kind: TokenKind::And, start, end })) } + '$' => { self.advance(); Ok(Some(Token { kind: TokenKind::Dollar, start, end })) } + '.' => { + self.advance(); + if self.current() == Some('.') { + self.advance(); + Ok(Some(Token { kind: TokenKind::DotDot, start, end })) + } else { + Ok(Some(Token { kind: TokenKind::Dot, start, end })) + } + } + '"' => self.consume_string(start), + '\'' => self.consume_char(start), + '@' => self.consume_uid(start), + '#' => self.consume_byte_string(start), + c if c.is_ascii_digit() => self.consume_number_or_duration(start, c), + c if c.is_alphabetic() || c == '_' => { + let mut ident = String::new(); + ident.push(c); + self.advance(); + while let Some(ch) = self.current() { + if ch.is_alphanumeric() || ch == '_' { + ident.push(ch); + self.advance(); + } else { + break; + } + } + let end = self.pos; + if ident == "f" && self.current() == Some('"') { + self.advance(); + return self.consume_fstring(start); + } + match ident.as_str() { + "true" => Ok(Some(Token { kind: TokenKind::Literal(Literal::Bool(true)), start, end })), + "false" => Ok(Some(Token { kind: TokenKind::Literal(Literal::Bool(false)), start, end })), + "if" => Ok(Some(Token { kind: TokenKind::If, start, end })), + "then" => Ok(Some(Token { kind: TokenKind::Then, start, end })), + "else" => Ok(Some(Token { kind: TokenKind::Else, start, end })), + "let" => Ok(Some(Token { kind: TokenKind::Let, start, end })), + "in" => Ok(Some(Token { kind: TokenKind::In, start, end })), + "case" => Ok(Some(Token { kind: TokenKind::Case, start, end })), + "of" => Ok(Some(Token { kind: TokenKind::Of, start, end })), + "data" => Ok(Some(Token { kind: TokenKind::Data, start, end })), + "struct" => Ok(Some(Token { kind: TokenKind::Struct, start, end })), + "try" => Ok(Some(Token { kind: TokenKind::Try, start, end })), + "catch" => Ok(Some(Token { kind: TokenKind::Catch, start, end })), + "error" => Ok(Some(Token { kind: TokenKind::Error, start, end })), + "for" => Ok(Some(Token { kind: TokenKind::For, start, end })), + "while" => Ok(Some(Token { kind: TokenKind::While, start, end })), + "lambda" => Ok(Some(Token { kind: TokenKind::Lambda, start, end })), + "and" => Ok(Some(Token { kind: TokenKind::And, start, end })), + "or" => Ok(Some(Token { kind: TokenKind::Or, start, end })), + "not" => Ok(Some(Token { kind: TokenKind::Not, start, end })), + "class" => Ok(Some(Token { kind: TokenKind::Class, start, end })), + "extends" => Ok(Some(Token { kind: TokenKind::Extends, start, end })), + "new" => Ok(Some(Token { kind: TokenKind::New, start, end })), + "loop" => Ok(Some(Token { kind: TokenKind::Loop, start, end })), + "break" => Ok(Some(Token { kind: TokenKind::Break, start, end })), + _ => Ok(Some(Token { kind: TokenKind::Ident(ident), start, end })), + } + } + _ => Err(LexError::UnexpectedChar(c, self.pos)), + } + } + + fn consume_string(&mut self, start: usize) -> Result, LexError> { + self.advance(); + let mut s = String::new(); + while let Some(ch) = self.current() { + if ch == '"' { + self.advance(); + let end = self.pos; + return Ok(Some(Token { kind: TokenKind::Literal(Literal::String(s)), start, end })); + } + if ch == '\\' { + self.advance(); + if let Some(esc) = self.current() { + match esc { + 'n' => s.push('\n'), + 't' => s.push('\t'), + 'r' => s.push('\r'), + '\\' => s.push('\\'), + '"' => s.push('"'), + '\'' => s.push('\''), + _ => return Err(LexError::InvalidEscape(esc, self.pos)), + } + self.advance(); + } else { + return Err(LexError::UnexpectedEof); + } + } else { + s.push(ch); + self.advance(); + } + } + Err(LexError::UnterminatedString(start)) + } + + fn consume_fstring(&mut self, start: usize) -> Result, LexError> { + let mut content = String::new(); + while let Some(ch) = self.current() { + if ch == '"' { + if let Some(prev) = self.chars.get(self.pos - 1) { + if *prev == '\\' { + content.push('"'); + self.advance(); + continue; + } + } + self.advance(); + let end = self.pos; + return Ok(Some(Token { kind: TokenKind::FString(content), start, end })); + } + if ch == '\\' && self.current().and_then(|_| self.chars.get(self.pos + 1)).map(|&c| c == '"').unwrap_or(false) { + content.push('\\'); + self.advance(); + if let Some(ch2) = self.current() { + content.push(ch2); + self.advance(); + } + continue; + } + content.push(ch); + self.advance(); + } + Err(LexError::UnterminatedString(start)) + } + + fn consume_char(&mut self, start: usize) -> Result, LexError> { + self.advance(); + let ch = if self.current() == Some('\\') { + self.advance(); + if let Some(esc) = self.current() { + self.advance(); + match esc { + 'n' => '\n', + 't' => '\t', + 'r' => '\r', + '\\' => '\\', + '\'' => '\'', + _ => return Err(LexError::InvalidEscape(esc, self.pos)), + } + } else { + return Err(LexError::UnexpectedEof); + } + } else { + if let Some(c) = self.current() { + self.advance(); + c + } else { + return Err(LexError::UnexpectedEof); + } + }; + if self.current() == Some('\'') { + self.advance(); + let end = self.pos; + Ok(Some(Token { kind: TokenKind::Literal(Literal::Char(ch)), start, end })) + } else { + Err(LexError::UnterminatedString(start)) + } + } + + fn consume_uid(&mut self, start: usize) -> Result, LexError> { + self.advance(); + let mut uid = String::from("@"); + while let Some(ch) = self.current() { + if ch.is_alphanumeric() || ch == '_' { + uid.push(ch); + self.advance(); + } else { + break; + } + } + let end = self.pos; + Ok(Some(Token { kind: TokenKind::Literal(Literal::Uid(uid)), start, end })) + } + + fn consume_byte_string(&mut self, start: usize) -> Result, LexError> { + self.advance(); + if self.current() == Some('B') { + self.advance(); + if self.current() != Some('"') { + return Err(LexError::UnexpectedChar(self.current().unwrap_or('\0'), self.pos)); + } + self.advance(); + let mut hex = String::new(); + while let Some(ch) = self.current() { + if ch == '"' { + self.advance(); + let end = self.pos; + let bytes = hex::decode(&hex) + .map_err(|_| LexError::InvalidHexByteString(hex.clone(), self.pos))?; + return Ok(Some(Token { kind: TokenKind::Literal(Literal::ByteString(bytes)), start, end })); + } + hex.push(ch); + self.advance(); + } + Err(LexError::UnterminatedString(self.pos)) + } else { + Err(LexError::UnexpectedChar('#', self.pos)) + } + } + + fn consume_number_or_duration(&mut self, start: usize, first: char) -> Result, LexError> { + let mut num_str = String::new(); + num_str.push(first); + self.advance(); + let mut is_float = false; + loop { + match self.current() { + Some(ch) if ch.is_ascii_digit() => { + num_str.push(ch); + self.advance(); + } + Some('.') => { + if let Some(&next) = self.chars.get(self.pos + 1) { + if next == '.' { + break; + } + } + is_float = true; + num_str.push('.'); + self.advance(); + } + _ => break, + } + } + + let suffix = match self.current() { + Some('s') => { + self.advance(); + Some('s') + } + Some('m') => { + self.advance(); + if self.current() == Some('s') { + self.advance(); + Some('m') + } else { + Some('M') + } + } + Some('h') => { + self.advance(); + Some('h') + } + _ => None, + }; + let end = self.pos; + + if let Some(suf) = suffix { + let num: f64 = num_str.parse().map_err(|_| LexError::InvalidNumber(num_str.clone(), start))?; + let (secs, nanos) = match suf { + 's' => { + let secs = num as u64; + let nanos = ((num - secs as f64) * 1_000_000_000.0) as u32; + (secs, nanos) + } + 'm' => { + let millis = (num * 1000.0) as u64; + let secs = millis / 1000; + let nanos = ((millis % 1000) * 1_000_000) as u32; + (secs, nanos) + } + 'M' => { + let secs = (num * 60.0) as u64; + let nanos = ((num * 60.0 - secs as f64) * 1_000_000_000.0) as u32; + (secs, nanos) + } + 'h' => { + let secs = (num * 3600.0) as u64; + let nanos = ((num * 3600.0 - secs as f64) * 1_000_000_000.0) as u32; + (secs, nanos) + } + _ => return Err(LexError::InvalidDuration(num_str, start)), + }; + let dur = Duration::new(secs, nanos); + Ok(Some(Token { kind: TokenKind::Literal(Literal::Duration(dur)), start, end })) + } else { + if is_float { + let f: f64 = num_str.parse().map_err(|_| LexError::InvalidNumber(num_str.clone(), start))?; + Ok(Some(Token { kind: TokenKind::Literal(Literal::Float(f)), start, end })) + } else { + let i: i64 = num_str.parse().map_err(|_| LexError::InvalidNumber(num_str.clone(), start))?; + Ok(Some(Token { kind: TokenKind::Literal(Literal::Int(i)), start, end })) + } + } + } + + fn skip_whitespace(&mut self) -> Result<(), LexError> { + while let Some(ch) = self.current() { + match ch { + ' ' | '\t' | '\r' => self.advance(), + _ => break, + } + } + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..3c84f2f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,10 @@ +pub mod ast; +pub mod error; +pub mod lexer; +pub mod parser; +pub mod types; +pub mod eval; +pub mod builtins; +pub mod chat; +pub mod p2p; +pub mod server; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..1e7ff93 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,454 @@ +//! REPL with syntax highlighting and error reporting. + +use plic::chat::ChatState; +use plic::eval::eval_expr; +use plic::types::Environment; +use plic::parser::{parse_expression, parse_script, ParseError, strip_comments}; +use rustyline::Editor; +use rustyline::error::ReadlineError; +use rustyline::completion::Completer; +use rustyline::highlight::Highlighter; +use rustyline::hint::Hinter; +use rustyline::validate::{Validator, ValidationContext, ValidationResult}; +use rustyline::Helper; +use std::sync::{Arc, Mutex}; +use std::fs; +use std::env; +use std::collections::BTreeSet; +use std::borrow::Cow; +use codespan::Files; +use codespan_reporting::diagnostic::{Diagnostic, Label}; +use codespan_reporting::term::termcolor::{ColorChoice, StandardStream}; +use codespan_reporting::term as term_reporting; + +struct ChatLangHelper { + env: Arc>, + keywords: Vec, + builtins: Vec, + types: Vec, +} + +impl Completer for ChatLangHelper { + type Candidate = String; + + fn complete(&self, line: &str, pos: usize, _ctx: &rustyline::Context<'_>) -> rustyline::Result<(usize, Vec)> { + let mut candidates = BTreeSet::new(); + for kw in &self.keywords { + candidates.insert(kw.clone()); + } + for b in &self.builtins { + candidates.insert(b.clone()); + } + for t in &self.types { + candidates.insert(t.clone()); + } + let env_guard = self.env.lock().unwrap(); + for (name, _) in env_guard.vars.iter() { + candidates.insert(name.clone()); + } + drop(env_guard); + + let prefix = &line[..pos]; + let last_word = prefix.split_whitespace().last().unwrap_or(""); + let matches: Vec = candidates.into_iter() + .filter(|s| s.starts_with(last_word) && s != last_word) + .collect(); + Ok((pos - last_word.len(), matches)) + } +} + +impl Highlighter for ChatLangHelper { + fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> { + let mut result = String::new(); + let mut in_string = false; + let mut in_comment = false; + let mut multiline_comment_depth = 0; + let mut in_fstring = false; + let mut brace_depth = 0; + let keywords = [ + "let", "if", "then", "else", "case", "of", "lambda", "\\", + "data", "struct", "try", "catch", "error", "for", "while", + "true", "false", "in", "and", "or", "not", "class", "extends", "new", + "loop", "break" + ]; + let types = [ + "Num", "Char", "String", "Bool", "Unit", "Uid", + "ByteString", "List", "Tuple", "Record", "Pid", "DateTime", + "Duration", "Json", "Maybe", "Either", "ChatMsg", "FileInfo", + "FileTransfer", "FetchOptions", "FetchResult", "Map", "Set", + "ClassInstance" + ]; + let mut chars = line.chars().peekable(); + while let Some(ch) = chars.next() { + // Multiline comment + if ch == '#' && chars.peek() == Some(&'-') { + chars.next(); + multiline_comment_depth += 1; + if in_comment { + result.push_str("\x1b[3;90m#-"); + } else { + in_comment = true; + result.push_str("\x1b[3;90m#-"); + } + continue; + } + if ch == '-' && chars.peek() == Some(&'#') { + chars.next(); + if multiline_comment_depth > 0 { + multiline_comment_depth -= 1; + result.push_str("-#\x1b[0m"); + if multiline_comment_depth == 0 { + in_comment = false; + } else { + result.push_str("\x1b[3;90m"); + } + } else { + result.push_str("-#"); + } + continue; + } + if in_comment { + result.push(ch); + continue; + } + + // Single-line comment + if ch == '#' && chars.peek() != Some(&'B') && chars.peek() != Some(&'-') { + result.push_str("\x1b[3;90m#"); + while let Some(c) = chars.next() { + result.push(c); + } + result.push_str("\x1b[0m"); + break; + } + + // F-string + if ch == 'f' && chars.peek() == Some(&'"') { + in_fstring = true; + result.push_str("\x1b[32m"); + result.push(ch); + continue; + } + if in_fstring && ch == '"' { + result.push_str("\"\x1b[0m"); + in_fstring = false; + continue; + } + if in_fstring && ch == '{' { + brace_depth += 1; + result.push_str("\x1b[0m"); + result.push(ch); + continue; + } + if in_fstring && ch == '}' { + brace_depth -= 1; + result.push_str("\x1b[32m"); + result.push(ch); + continue; + } + if in_fstring && brace_depth > 0 { + result.push(ch); + continue; + } + + // Regular code highlighting + if ch.is_alphabetic() || ch == '_' { + let mut word = String::new(); + word.push(ch); + while let Some(&next) = chars.peek() { + if next.is_alphanumeric() || next == '_' { + word.push(next); + chars.next(); + } else { + break; + } + } + if keywords.contains(&word.as_str()) { + result.push_str(&format!("\x1b[34m{}\x1b[0m", word)); + } else if types.contains(&word.as_str()) { + result.push_str(&format!("\x1b[35m{}\x1b[0m", word)); + } else { + let env_guard = self.env.lock().unwrap(); + let defined = env_guard.vars.contains_key(&word); + drop(env_guard); + if defined { + result.push_str(&format!("\x1b[4;37m{}\x1b[0m", word)); + } else { + result.push_str(&format!("\x1b[37m{}\x1b[0m", word)); + } + } + } else if ch.is_digit(10) { + result.push_str(&format!("\x1b[33m{}\x1b[0m", ch)); + while let Some(&next) = chars.peek() { + if next.is_digit(10) || next == '.' { + result.push_str(&format!("\x1b[33m{}\x1b[0m", next)); + chars.next(); + } else { + break; + } + } + } else { + result.push(ch); + } + } + if in_string || in_fstring { + result.push_str("\x1b[0m"); + } + Cow::Owned(result) + } + + fn highlight_char(&self, _line: &str, _pos: usize) -> bool { + true + } +} + +impl Hinter for ChatLangHelper { + type Hint = String; + fn hint(&self, _line: &str, _pos: usize, _ctx: &rustyline::Context<'_>) -> Option { None } +} + +impl Validator for ChatLangHelper { + fn validate(&self, _ctx: &mut ValidationContext<'_>) -> rustyline::Result { + Ok(ValidationResult::Valid(None)) + } +} + +impl Helper for ChatLangHelper {} + +fn main() { + let args: Vec = env::args().collect(); + let state = Arc::new(Mutex::new(ChatState::new())); + let mut env = Environment::new(); + let env_arc = Arc::new(Mutex::new(env.clone())); + + plic::builtins::populate(&mut env, Arc::clone(&state), Arc::clone(&env_arc)); + + { + let mut guard = env_arc.lock().unwrap(); + *guard = env; + } + + let mut p2p_port = 19000; + for i in 0..args.len() { + if args[i] == "--p2p-port" && i + 1 < args.len() { + if let Ok(port) = args[i+1].parse() { + p2p_port = port; + } + } + } + { + let mut state_guard = state.lock().unwrap(); + state_guard.p2p_port = p2p_port; + } + + if args.len() > 1 && !args[1].starts_with("--") { + let filename = &args[1]; + match fs::read_to_string(filename) { + Ok(content) => { + match parse_script(&content) { + Ok(expr) => { + let mut env_guard = env_arc.lock().unwrap(); + if let Err(err) = eval_expr(&expr, &mut env_guard, Arc::clone(&state)) { + let mut files = Files::new(); + let file_id = files.add(filename, content.clone()); + let diagnostic = Diagnostic::error() + .with_message(err.message) + .with_labels(vec![ + Label::primary(file_id, err.span.unwrap_or(plic::ast::Span::dummy()).start..err.span.unwrap_or(plic::ast::Span::dummy()).end) + ]); + let writer = StandardStream::stderr(ColorChoice::Always); + let config = term_reporting::Config::default(); + let _ = term_reporting::emit(&mut writer.lock(), &config, &files, &diagnostic); + } + } + Err(e) => eprintln!("\x1b[31merror\x1b[0m: Parse error: {}", e), + } + } + Err(e) => eprintln!("\x1b[31merror\x1b[0m: Failed to read file {}: {}", filename, e), + } + return; + } + + let helper = ChatLangHelper { + env: Arc::clone(&env_arc), + keywords: vec![ + "let".into(), "if".into(), "then".into(), "else".into(), + "case".into(), "of".into(), "lambda".into(), "\\".into(), + "data".into(), "struct".into(), "try".into(), "catch".into(), + "error".into(), "for".into(), "while".into(), "in".into(), + "and".into(), "or".into(), "not".into(), + "class".into(), "extends".into(), "new".into(), + "loop".into(), "break".into(), + ], + builtins: vec![ + "sqrt".into(), "sin".into(), "cos".into(), "tan".into(), + "asin".into(), "acos".into(), "atan".into(), + "toFloat".into(), "toInt".into(), + "show".into(), "parseInt".into(), "parseFloat".into(), + "chr".into(), "ord".into(), + "null".into(), "length".into(), "map".into(), "filter".into(), + "foldl".into(), "foldr".into(), "take".into(), "drop".into(), + "reverse".into(), "all".into(), "any".into(), "find".into(), + "sort".into(), "sortBy".into(), "sum".into(), + "concat".into(), "flatten".into(), "zip".into(), "zipWith".into(), + "unzip".into(), "indexOf".into(), "lastIndexOf".into(), + "split".into(), "join".into(), "startsWith".into(), "endsWith".into(), + "trim".into(), "replace".into(), "substring".into(), + "parseJson".into(), "encodeJson".into(), "lookup".into(), + "formatTime".into(), "parseTime".into(), "addDuration".into(), + "diffDuration".into(), + "packBytes".into(), "unpackBytes".into(), + "putStrLn".into(), "getLine".into(), "getArgs".into(), + "readFile".into(), "readBinaryFile".into(), "writeFile".into(), + "appendFile".into(), "writeBinaryFile".into(), "fileExists".into(), + "fileSize".into(), + "fetch".into(), "fetchOpts".into(), + "login".into(), "newChat".into(), "addMember".into(), "removeMember".into(), + "open".into(), "send".into(), "sendFile".into(), + "sendChat".into(), "sendFileToChat".into(), + "inbox".into(), "history".into(), "downloads".into(), "saveFile".into(), + "serverStart".into(), "serverStop".into(), + "connect".into(), + "now".into(), + "spawn".into(), "procSelf".into(), "procSend".into(), + "procRecv".into(), "procWait".into(), "procExit".into(), + "sleep".into(), "after".into(), + "Nothing".into(), "Just".into(), "maybe".into(), + "mapGet".into(), "mapSet".into(), "mapRemove".into(), + "mapKeys".into(), "mapValues".into(), "mapEntries".into(), + "mapContains".into(), "mapSize".into(), "mapFilter".into(), "mapMerge".into(), + "setAdd".into(), "setRemove".into(), + "setContains".into(), "setUnion".into(), "setIntersection".into(), + "setDifference".into(), "setSize".into(), "setFilter".into(), "setMap".into(), + "listToSet".into(), "mapToList".into(), + "sha256".into(), "sha256String".into(), + "kyberKeyPair".into(), "kyberEncapsulate".into(), "kyberDecapsulate".into(), + "listDir".into(), "createDir".into(), "removeDir".into(), + "fileMove".into(), "filePermissions".into(), "setFilePermissions".into(), + "typeof".into(), "getPublicIP".into(), "setExternalIP".into(), + "exit".into(), "load".into(), "del".into(), + "logout".into(), "deleteUser".into(), "deleteChat".into(), + "listChats".into(), "members".into(), + "addContact".into(), "removeContact".into(), + ], + types: vec![ + "Num".into(), "Char".into(), "String".into(), + "Bool".into(), "Unit".into(), "Uid".into(), + "ByteString".into(), "List".into(), "Tuple".into(), + "Record".into(), "Pid".into(), "DateTime".into(), + "Duration".into(), "Json".into(), "Maybe".into(), + "Either".into(), "ChatMsg".into(), "FileInfo".into(), + "FileTransfer".into(), "FetchOptions".into(), "FetchResult".into(), + "Map".into(), "Set".into(), "ClassInstance".into(), + ], + }; + + let mut rl = Editor::new().unwrap(); + rl.set_helper(Some(helper)); + let _ = rl.load_history(".chatlang_history"); + + let mut buffer = String::new(); + let mut in_multiline = false; + let mut indent_level = 0; + + loop { + let prompt = if buffer.is_empty() { + ">>> " + } else { + "... " + }; + let line = rl.readline(prompt); + match line { + Ok(line) => { + rl.add_history_entry(line.as_str()); + let trimmed = line.trim(); + + // If we are in multiline and line is "end" or empty with indent 0, finish block + if in_multiline && (trimmed == "end" || (line.chars().take_while(|c| *c == ' ').count() == 0 && trimmed.is_empty())) { + let full_block = buffer.clone(); + match parse_script(&full_block) { + Ok(expr) => { + let mut env_guard = env_arc.lock().unwrap(); + match eval_expr(&expr, &mut env_guard, Arc::clone(&state)) { + Ok(_) => {}, + Err(err) => eprintln!("\x1b[31merror\x1b[0m: {}", err), + } + } + Err(e) => eprintln!("\x1b[31merror\x1b[0m: Parse error: {}", e), + } + buffer.clear(); + in_multiline = false; + indent_level = 0; + continue; + } + + let stripped = strip_comments(&line); + if stripped.trim().is_empty() && !in_multiline { + continue; + } + + if !in_multiline && trimmed.ends_with(':') { + buffer.push_str(&line); + in_multiline = true; + indent_level = line.chars().take_while(|c| *c == ' ').count(); + continue; + } + + if in_multiline { + buffer.push('\n'); + buffer.push_str(&line); + let current_indent = line.chars().take_while(|c| *c == ' ').count(); + // If the line has indent 0 and is not empty, it might be a new top-level statement – finish block + if current_indent == 0 && !trimmed.is_empty() && trimmed != "end" { + // finish block + let full_block = buffer.clone(); + match parse_script(&full_block) { + Ok(expr) => { + let mut env_guard = env_arc.lock().unwrap(); + match eval_expr(&expr, &mut env_guard, Arc::clone(&state)) { + Ok(_) => {}, + Err(err) => eprintln!("\x1b[31merror\x1b[0m: {}", err), + } + } + Err(e) => eprintln!("\x1b[31merror\x1b[0m: Parse error: {}", e), + } + buffer.clear(); + in_multiline = false; + indent_level = 0; + // Now we need to process the current line as a new top-level expression? + // But we already consumed it. We'll just continue. + continue; + } else { + // Still inside block + continue; + } + } + + // Single-line expression + match parse_expression(&stripped) { + Ok(expr) => { + let mut env_guard = env_arc.lock().unwrap(); + match eval_expr(&expr, &mut env_guard, Arc::clone(&state)) { + Ok(_) => {}, + Err(err) => eprintln!("\x1b[31merror\x1b[0m: {}", err), + } + } + Err(ParseError { message, span: _span }) => { + eprintln!("\x1b[31merror\x1b[0m: Parse error: {}", message); + } + } + } + Err(ReadlineError::Interrupted) => { + if in_multiline { + println!("Aborted multi-line block."); + buffer.clear(); + in_multiline = false; + indent_level = 0; + } + continue; + } + Err(_) => break, + } + } + let _ = rl.save_history(".chatlang_history"); +} diff --git a/src/p2p.rs b/src/p2p.rs new file mode 100644 index 0000000..397cc36 --- /dev/null +++ b/src/p2p.rs @@ -0,0 +1,158 @@ +//! P2P messaging with control messages. + +use crate::chat::{ChatState, P2pControl}; +use crate::types::Value; +use serde::{Deserialize, Serialize}; +use std::io::{BufRead, BufReader, Write}; +use std::net::TcpStream; +use std::sync::{Arc, Mutex}; +use std::thread; +use native_tls::{TlsConnector, TlsAcceptor, Identity}; +use std::fs; +use std::path::Path; +use once_cell::sync::Lazy; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct P2pMessage { + pub msg_type: String, + pub from: String, + pub to: String, + pub text: Option, + pub filename: Option, + pub data_base64: Option, + pub chat: Option, + pub timestamp: Option, + pub control: Option, +} + +static TLS_CONNECTOR: Lazy = Lazy::new(|| { + let identity = load_or_generate_identity().expect("Failed to load or generate TLS identity"); + TlsConnector::builder() + .identity(identity) + .danger_accept_invalid_certs(true) + .build() + .expect("Failed to build TLS connector") +}); + +static TLS_ACCEPTOR: Lazy = Lazy::new(|| { + let identity = load_or_generate_identity().expect("Failed to load or generate TLS identity"); + TlsAcceptor::new(identity).expect("Failed to build TLS acceptor") +}); + +fn load_or_generate_identity() -> Result> { + let cert_path = "chatlang_cert.pem"; + let key_path = "chatlang_key.pem"; + if !Path::new(cert_path).exists() || !Path::new(key_path).exists() { + let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()])?; + let cert_pem = cert.serialize_pem()?; + let key_pem = cert.serialize_private_key_pem(); + fs::write(cert_path, &cert_pem)?; + fs::write(key_path, &key_pem)?; + } + let cert = fs::read(cert_path)?; + let key = fs::read(key_path)?; + Identity::from_pkcs8(&cert, &key).map_err(|e| e.into()) +} + +pub fn get_tls_connector() -> TlsConnector { + TLS_CONNECTOR.clone() +} + +pub fn get_tls_acceptor() -> TlsAcceptor { + TLS_ACCEPTOR.clone() +} + +pub fn start_p2p_listener(port: u16, state: Arc>) { + let listener = match std::net::TcpListener::bind(format!("0.0.0.0:{}", port)) { + Ok(l) => l, + Err(e) => { + eprintln!("Failed to bind P2P port {}: {}", port, e); + return; + } + }; + let acceptor = get_tls_acceptor(); + thread::spawn(move || { + println!("P2P listener on port {} (TLS)", port); + for stream in listener.incoming() { + if let Ok(stream) = stream { + let acceptor = acceptor.clone(); + let state = Arc::clone(&state); + thread::spawn(move || { + if let Ok(tls_stream) = acceptor.accept(stream) { + handle_p2p_connection(tls_stream, state); + } + }); + } + } + }); +} + +fn handle_p2p_connection(stream: native_tls::TlsStream, state: Arc>) { + let reader = BufReader::new(stream); + for line in reader.lines() { + if let Ok(line) = line { + if let Ok(msg) = serde_json::from_str::(&line) { + let mut state = state.lock().unwrap(); + match msg.msg_type.as_str() { + "msg" => { + let message = crate::chat::Message { + from: msg.from, + text: msg.text.unwrap_or_default(), + chat: msg.chat.clone().unwrap_or_default(), + timestamp: msg.timestamp.unwrap_or(0), + }; + if let Some(chat) = msg.chat { + state.messages.entry(chat).or_default().push(message.clone()); + } + if !msg.to.is_empty() { + state.deliver_to_user(msg.to, message); + } + } + "file" => { + use base64::Engine; + let data = base64::engine::general_purpose::STANDARD + .decode(msg.data_base64.as_deref().unwrap_or("")) + .unwrap_or_default(); + state.downloads.push(Value::FileTransfer { + from: msg.from, + filename: msg.filename.unwrap_or_default(), + data, + }); + } + "control" => { + if let Some(control) = msg.control { + if let Err(e) = state.handle_control(&control) { + eprintln!("Control error: {}", e); + } + } + } + _ => {} + } + } + } + } +} + +pub fn send_message_to(addr: &str, msg: P2pMessage) -> Result<(), Box> { + let connector = get_tls_connector(); + let stream = TcpStream::connect(addr)?; + let mut tls_stream = connector.connect("localhost", stream)?; + let json = serde_json::to_string(&msg)?; + writeln!(tls_stream, "{}", json)?; + Ok(()) +} + +pub fn send_control(addr: &str, control: P2pControl, from: &str) -> Result<(), Box> { + let msg = P2pMessage { + msg_type: "control".to_string(), + from: from.to_string(), + to: String::new(), + text: None, + filename: None, + data_base64: None, + chat: None, + timestamp: None, + control: Some(control), + }; + send_message_to(addr, msg) +} diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..cc713f2 --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,1237 @@ +//! Parser with indentation‑sensitive blocks, list comprehensions, and spans. + +use crate::ast::*; +use crate::lexer::{LexError, Lexer, Token, TokenKind}; + +#[derive(Debug, Clone, PartialEq)] +pub struct ParseError { + pub message: String, + pub span: Span, +} + +impl std::fmt::Display for ParseError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.message) + } +} + +impl std::error::Error for ParseError {} + +impl From for ParseError { + fn from(e: LexError) -> Self { + ParseError { message: e.to_string(), span: Span::dummy() } + } +} + +pub type ParseResult = Result; + +pub fn strip_comments(input: &str) -> String { + let mut output = String::new(); + let mut chars = input.chars().peekable(); + while let Some(ch) = chars.next() { + if ch == '#' { + if chars.peek() == Some(&'-') { + chars.next(); + let mut depth = 1; + while depth > 0 { + match chars.next() { + Some('#') => { + if chars.peek() == Some(&'-') { + chars.next(); + depth += 1; + } + } + Some('-') => { + if chars.peek() == Some(&'#') { + chars.next(); + depth -= 1; + } + } + Some(_) => {} + None => break, + } + } + continue; + } else { + while let Some(c) = chars.next() { + if c == '\n' { + output.push(c); + break; + } + } + continue; + } + } + output.push(ch); + } + output +} + +pub fn parse_expression(input: &str) -> ParseResult { + let stripped = strip_comments(input); + let mut lexer = Lexer::new(&stripped); + let tokens = lexer.tokenize()?; + let mut pos = 0; + let expr = parse_expr(&tokens, &mut pos)?; + while pos < tokens.len() && (matches!(tokens[pos].kind, TokenKind::NEWLINE) || matches!(tokens[pos].kind, TokenKind::DEDENT)) { + pos += 1; + } + if pos < tokens.len() && tokens[pos].kind != TokenKind::Eof { + Err(ParseError { message: format!("Unexpected token: {:?}", tokens[pos].kind), span: Span::new(tokens[pos].start, tokens[pos].end) }) + } else { + Ok(expr) + } +} + +pub fn parse_script(input: &str) -> ParseResult { + let stripped = strip_comments(input); + let mut lexer = Lexer::new(&stripped); + let tokens = lexer.tokenize()?; + let mut pos = 0; + let mut exprs = Vec::new(); + while pos < tokens.len() && tokens[pos].kind != TokenKind::Eof { + let e = parse_expr(&tokens, &mut pos)?; + exprs.push(e); + while pos < tokens.len() && (matches!(tokens[pos].kind, TokenKind::NEWLINE) || matches!(tokens[pos].kind, TokenKind::Semicolon)) { + pos += 1; + } + } + if exprs.is_empty() { + Ok(Expr::Lit(Literal::Unit, Span::dummy())) + } else if exprs.len() == 1 { + Ok(exprs.remove(0)) + } else { + let start = exprs.first().map(|e| e_span(e).start).unwrap_or(0); + let end = exprs.last().map(|e| e_span(e).end).unwrap_or(0); + Ok(Expr::Block(exprs, Span::new(start, end))) + } +} + +fn e_span(e: &Expr) -> Span { + match e { + Expr::Lit(_, s) => *s, + Expr::Var(_, s) => *s, + Expr::Lambda(_, _, s) => *s, + Expr::App(_, _, s) => *s, + Expr::If(_, _, _, s) => *s, + Expr::Let { span, .. } => *span, + Expr::Assign(_, _, s) => *s, + Expr::Case(_, _, s) => *s, + Expr::Try(_, s) => *s, + Expr::Catch(_, _, _, s) => *s, + Expr::Throw(_, s) => *s, + Expr::DataDef(_, _, _, s) => *s, + Expr::StructDef(_, _, s) => *s, + Expr::StructNew(_, _, s) => *s, + Expr::Constructor(_, _, s) => *s, + Expr::Record(_, s) => *s, + Expr::FieldAccess(_, _, s) => *s, + Expr::RecordUpdate(_, _, s) => *s, + Expr::List(_, s) => *s, + Expr::Range(_, _, s) => *s, + Expr::BinOp(_, _, _, s) => *s, + Expr::Concat(_, _, s) => *s, + Expr::Pipe(_, _, s) => *s, + Expr::Dollar(_, _, s) => *s, + Expr::LogicalAnd(_, _, s) => *s, + Expr::LogicalOr(_, _, s) => *s, + Expr::Not(_, s) => *s, + Expr::Tuple(_, s) => *s, + Expr::Index(_, _, s) => *s, + Expr::For(_, _, _, s) => *s, + Expr::While(_, _, s) => *s, + Expr::Loop(_, s) => *s, + Expr::Break(_, s) => *s, + Expr::Block(_, s) => *s, + Expr::ClassDef { span, .. } => *span, + Expr::New(_, _, s) => *s, + Expr::MethodCall(_, _, _, s) => *s, + Expr::MapLiteral(_, s) => *s, + Expr::SetLiteral(_, s) => *s, + Expr::FString(_, s) => *s, + Expr::ListComp { span, .. } => *span, + } +} + +fn parse_expr(tokens: &[Token], pos: &mut usize) -> ParseResult { + parse_dollar(tokens, pos) +} + +fn parse_dollar(tokens: &[Token], pos: &mut usize) -> ParseResult { + let mut left = parse_assign(tokens, pos)?; + while *pos < tokens.len() && tokens[*pos].kind == TokenKind::Dollar { + let start = tokens[*pos].start; + *pos += 1; + let right = parse_assign(tokens, pos)?; + let end = e_span(&right).end; + left = Expr::Dollar(Box::new(left), Box::new(right), Span::new(start, end)); + } + Ok(left) +} + +fn parse_assign(tokens: &[Token], pos: &mut usize) -> ParseResult { + let left = parse_pipe(tokens, pos)?; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Assign { + if let Expr::Var(name, _span) = left { + let start = tokens[*pos].start; + *pos += 1; + let right = parse_assign(tokens, pos)?; + let end = e_span(&right).end; + Ok(Expr::Assign(name, Box::new(right), Span::new(start, end))) + } else { + Err(ParseError { message: "only variables can be assigned to".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }) + } + } else { + Ok(left) + } +} + +fn parse_pipe(tokens: &[Token], pos: &mut usize) -> ParseResult { + let mut left = parse_logical_or(tokens, pos)?; + while *pos < tokens.len() && tokens[*pos].kind == TokenKind::Pipe { + let start = tokens[*pos].start; + *pos += 1; + let right = parse_logical_or(tokens, pos)?; + let end = e_span(&right).end; + left = Expr::Pipe(Box::new(left), Box::new(right), Span::new(start, end)); + } + Ok(left) +} + +fn parse_logical_or(tokens: &[Token], pos: &mut usize) -> ParseResult { + let mut left = parse_logical_and(tokens, pos)?; + while *pos < tokens.len() && tokens[*pos].kind == TokenKind::Or { + let start = tokens[*pos].start; + *pos += 1; + let right = parse_logical_and(tokens, pos)?; + let end = e_span(&right).end; + left = Expr::LogicalOr(Box::new(left), Box::new(right), Span::new(start, end)); + } + Ok(left) +} + +fn parse_logical_and(tokens: &[Token], pos: &mut usize) -> ParseResult { + let mut left = parse_comparison(tokens, pos)?; + while *pos < tokens.len() && tokens[*pos].kind == TokenKind::And { + let start = tokens[*pos].start; + *pos += 1; + let right = parse_comparison(tokens, pos)?; + let end = e_span(&right).end; + left = Expr::LogicalAnd(Box::new(left), Box::new(right), Span::new(start, end)); + } + Ok(left) +} + +fn parse_comparison(tokens: &[Token], pos: &mut usize) -> ParseResult { + let mut left = parse_concat(tokens, pos)?; + while *pos < tokens.len() { + let op = match tokens[*pos].kind { + TokenKind::Eq => BinOp::Eq, + TokenKind::Neq => BinOp::Neq, + TokenKind::Lt => BinOp::Lt, + TokenKind::Le => BinOp::Le, + TokenKind::Gt => BinOp::Gt, + TokenKind::Ge => BinOp::Ge, + TokenKind::In => { + let start = tokens[*pos].start; + *pos += 1; + let right = parse_concat(tokens, pos)?; + let end = e_span(&right).end; + left = Expr::BinOp(BinOp::In, Box::new(left), Box::new(right), Span::new(start, end)); + continue; + } + _ => break, + }; + let start = tokens[*pos].start; + *pos += 1; + let right = parse_concat(tokens, pos)?; + let end = e_span(&right).end; + left = Expr::BinOp(op, Box::new(left), Box::new(right), Span::new(start, end)); + } + Ok(left) +} + +fn parse_concat(tokens: &[Token], pos: &mut usize) -> ParseResult { + let mut left = parse_cons(tokens, pos)?; + while *pos < tokens.len() && tokens[*pos].kind == TokenKind::Concat { + let start = tokens[*pos].start; + *pos += 1; + let right = parse_cons(tokens, pos)?; + let end = e_span(&right).end; + left = Expr::Concat(Box::new(left), Box::new(right), Span::new(start, end)); + } + Ok(left) +} + +fn parse_cons(tokens: &[Token], pos: &mut usize) -> ParseResult { + let mut left = parse_addsub(tokens, pos)?; + while *pos < tokens.len() && tokens[*pos].kind == TokenKind::Cons { + let start = tokens[*pos].start; + *pos += 1; + let right = parse_cons(tokens, pos)?; + let end = e_span(&right).end; + left = Expr::BinOp(BinOp::Cons, Box::new(left), Box::new(right), Span::new(start, end)); + } + Ok(left) +} + +fn parse_addsub(tokens: &[Token], pos: &mut usize) -> ParseResult { + let mut left = parse_muldiv(tokens, pos)?; + while *pos < tokens.len() { + let op = match tokens[*pos].kind { + TokenKind::Plus => BinOp::Add, + TokenKind::Minus => BinOp::Sub, + _ => break, + }; + let start = tokens[*pos].start; + *pos += 1; + let right = parse_muldiv(tokens, pos)?; + let end = e_span(&right).end; + left = Expr::BinOp(op, Box::new(left), Box::new(right), Span::new(start, end)); + } + Ok(left) +} + +fn parse_muldiv(tokens: &[Token], pos: &mut usize) -> ParseResult { + let mut left = parse_unary(tokens, pos)?; + while *pos < tokens.len() { + let op = match tokens[*pos].kind { + TokenKind::Star => BinOp::Mul, + TokenKind::Slash => BinOp::Div, + TokenKind::Percent => BinOp::Mod, + _ => break, + }; + let start = tokens[*pos].start; + *pos += 1; + let right = parse_unary(tokens, pos)?; + let end = e_span(&right).end; + left = Expr::BinOp(op, Box::new(left), Box::new(right), Span::new(start, end)); + } + Ok(left) +} + +fn parse_unary(tokens: &[Token], pos: &mut usize) -> ParseResult { + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Not { + let start = tokens[*pos].start; + *pos += 1; + let expr = parse_atom(tokens, pos)?; + let end = e_span(&expr).end; + Ok(Expr::Not(Box::new(expr), Span::new(start, end))) + } else if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Minus { + let start = tokens[*pos].start; + *pos += 1; + let expr = parse_atom(tokens, pos)?; + let end = e_span(&expr).end; + Ok(Expr::BinOp(BinOp::Sub, Box::new(Expr::Lit(Literal::Int(0), Span::new(start, start))), Box::new(expr), Span::new(start, end))) + } else { + parse_atom(tokens, pos) + } +} + +fn skip_newlines(tokens: &[Token], pos: &mut usize) { + while *pos < tokens.len() && tokens[*pos].kind == TokenKind::NEWLINE { + *pos += 1; + } +} + +fn parse_atom(tokens: &[Token], pos: &mut usize) -> ParseResult { + if *pos >= tokens.len() { + return Err(ParseError { message: "incomplete input".to_string(), span: Span::dummy() }); + } + let start = tokens[*pos].start; + match &tokens[*pos].kind { + TokenKind::Literal(lit) => { + *pos += 1; + let end = tokens[*pos - 1].end; + let expr = Expr::Lit(lit.clone(), Span::new(start, end)); + parse_postfix(expr, tokens, pos) + } + TokenKind::FString(content) => { + *pos += 1; + let end = tokens[*pos - 1].end; + let parts = parse_fstring(content)?; + Ok(Expr::FString(parts, Span::new(start, end))) + } + TokenKind::Ident(name) => { + *pos += 1; + let end = tokens[*pos - 1].end; + let mut expr = Expr::Var(name.clone(), Span::new(start, end)); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::LParen { + let temp_pos = *pos + 1; + if is_struct_constructor(tokens, temp_pos) { + *pos += 1; + let mut fields = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RParen { + let field_name = match &tokens[*pos].kind { + TokenKind::Ident(n) => n.clone(), + _ => return Err(ParseError { message: "expected field name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + }; + let _field_start = tokens[*pos].start; + *pos += 1; + expect_kind(tokens, pos, TokenKind::Assign)?; + let value = parse_expr(tokens, pos)?; + let _value_end = e_span(&value).end; + fields.push((field_name, value)); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } + } + let _end = expect_kind(tokens, pos, TokenKind::RParen)?; + expr = Expr::StructNew(name.clone(), fields, Span::new(start, _end)); + } else { + *pos += 1; + let mut args = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RParen { + let arg = parse_expr(tokens, pos)?; + args.push(arg); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } + } + let _end = expect_kind(tokens, pos, TokenKind::RParen)?; + for arg in args { + let end = e_span(&arg).end; + expr = Expr::App(Box::new(expr), Box::new(arg), Span::new(start, end)); + } + } + } else { + expr = parse_application(expr, tokens, pos)?; + } + parse_postfix(expr, tokens, pos) + } + TokenKind::If => { + *pos += 1; + let cond = parse_expr(tokens, pos)?; + skip_newlines(tokens, pos); + expect_kind(tokens, pos, TokenKind::Then)?; + let then_expr = parse_expr_or_block(tokens, pos)?; + skip_newlines(tokens, pos); + expect_kind(tokens, pos, TokenKind::Else)?; + let else_expr = parse_expr_or_block(tokens, pos)?; + let end = e_span(&else_expr).end; + Ok(Expr::If(Box::new(cond), Box::new(then_expr), Box::new(else_expr), Span::new(start, end))) + } + TokenKind::Let => { + *pos += 1; + let name = match &tokens[*pos].kind { + TokenKind::Ident(n) => n.clone(), + _ => return Err(ParseError { message: "expected identifier".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + }; + let _name_start = tokens[*pos].start; + *pos += 1; + + let mut type_ann = None; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::DoubleColon { + *pos += 1; + let type_str = parse_type(tokens, pos)?; + type_ann = Some(type_str); + } + + let mut params = Vec::new(); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::LParen { + *pos += 1; + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RParen { + if let TokenKind::Ident(p) = &tokens[*pos].kind { + params.push(p.clone()); + *pos += 1; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } + } else { + return Err(ParseError { message: "expected parameter name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }); + } + } + expect_kind(tokens, pos, TokenKind::RParen)?; + } else { + while *pos < tokens.len() && matches!(tokens[*pos].kind, TokenKind::Ident(_)) { + if let TokenKind::Ident(p) = &tokens[*pos].kind { + params.push(p.clone()); + *pos += 1; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } else { + break; + } + } else { break; } + } + } + + expect_kind(tokens, pos, TokenKind::Assign)?; + let body = parse_expr_or_block(tokens, pos)?; + let end = e_span(&body).end; + if params.is_empty() { + Ok(Expr::Let { + name, + type_ann, + def: Box::new(body), + body: None, + span: Span::new(start, end), + }) + } else { + let lambda = params.into_iter().rfold(body, |acc, p| { + Expr::Lambda(vec![p], Box::new(acc), Span::new(start, end)) + }); + Ok(Expr::Let { + name, + type_ann, + def: Box::new(lambda), + body: None, + span: Span::new(start, end), + }) + } + } + TokenKind::Case => { + *pos += 1; + let scrut = parse_expr(tokens, pos)?; + skip_newlines(tokens, pos); + expect_kind(tokens, pos, TokenKind::Of)?; + skip_newlines(tokens, pos); + let mut arms = Vec::new(); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::INDENT { + *pos += 1; + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::DEDENT { + let pat = parse_pattern(tokens, pos)?; + expect_kind(tokens, pos, TokenKind::Arrow)?; + let arm_body = parse_expr(tokens, pos)?; + arms.push((pat, Box::new(arm_body))); + if *pos < tokens.len() && (tokens[*pos].kind == TokenKind::Semicolon || tokens[*pos].kind == TokenKind::NEWLINE) { + *pos += 1; + } + } + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::DEDENT { + *pos += 1; + } + let end = if !arms.is_empty() { e_span(&arms.last().unwrap().1).end } else { tokens[*pos - 1].end }; + Ok(Expr::Case(Box::new(scrut), arms, Span::new(start, end))) + } else { + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::Eof && tokens[*pos].kind != TokenKind::NEWLINE { + let pat = parse_pattern(tokens, pos)?; + expect_kind(tokens, pos, TokenKind::Arrow)?; + let arm_body = parse_expr(tokens, pos)?; + arms.push((pat, Box::new(arm_body))); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Semicolon { + *pos += 1; + } else { + break; + } + } + let end = if !arms.is_empty() { e_span(&arms.last().unwrap().1).end } else { tokens[*pos - 1].end }; + Ok(Expr::Case(Box::new(scrut), arms, Span::new(start, end))) + } + } + TokenKind::Try => { + *pos += 1; + let try_expr = parse_expr(tokens, pos)?; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Catch { + *pos += 1; + let pat = parse_pattern(tokens, pos)?; + skip_newlines(tokens, pos); + expect_kind(tokens, pos, TokenKind::Arrow)?; + let handler = parse_expr(tokens, pos)?; + let end = e_span(&handler).end; + Ok(Expr::Catch(Box::new(try_expr), pat, Box::new(handler), Span::new(start, end))) + } else { + let end = e_span(&try_expr).end; + Ok(Expr::Try(Box::new(try_expr), Span::new(start, end))) + } + } + TokenKind::Error => { + *pos += 1; + let msg = parse_expr(tokens, pos)?; + let end = e_span(&msg).end; + Ok(Expr::Throw(Box::new(msg), Span::new(start, end))) + } + TokenKind::LBracket => { + *pos += 1; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::RBracket { + *pos += 1; + let end = tokens[*pos - 1].end; + return Ok(Expr::List(vec![], Span::new(start, end))); + } + let first = parse_expr(tokens, pos)?; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::For { + let expr = first; + let generators = Vec::new(); + let filters = Vec::new(); + let (gens, filters) = parse_list_comp_generators(tokens, pos, generators, filters)?; + let end = expect_kind(tokens, pos, TokenKind::RBracket)?; + Ok(Expr::ListComp { + expr: Box::new(expr), + generators: gens, + filters, + span: Span::new(start, end), + }) + } else if *pos < tokens.len() && tokens[*pos].kind == TokenKind::DotDot { + *pos += 1; + let end_expr = parse_expr(tokens, pos)?; + let end = expect_kind(tokens, pos, TokenKind::RBracket)?; + Ok(Expr::Range(Box::new(first), Box::new(end_expr), Span::new(start, end))) + } else { + let mut elems = vec![first]; + while *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + elems.push(parse_expr(tokens, pos)?); + } + let end = expect_kind(tokens, pos, TokenKind::RBracket)?; + Ok(Expr::List(elems, Span::new(start, end))) + } + } + TokenKind::LParen => { + *pos += 1; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::RParen { + *pos += 1; + let end = tokens[*pos - 1].end; + return Ok(Expr::Lit(Literal::Unit, Span::new(start, end))); + } + let expr = parse_expr(tokens, pos)?; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + let mut elems = vec![expr]; + while *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + elems.push(parse_expr(tokens, pos)?); + } + let end = expect_kind(tokens, pos, TokenKind::RParen)?; + Ok(Expr::Tuple(elems, Span::new(start, end))) + } else { + let end = expect_kind(tokens, pos, TokenKind::RParen)?; + Ok(expr) + } + } + TokenKind::Backslash | TokenKind::Lambda => { + *pos += 1; + let mut params = Vec::new(); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::LParen { + *pos += 1; + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RParen { + if let TokenKind::Ident(p) = &tokens[*pos].kind { + params.push(p.clone()); + *pos += 1; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } + } else { + return Err(ParseError { message: "expected parameter name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }); + } + } + expect_kind(tokens, pos, TokenKind::RParen)?; + } else { + while *pos < tokens.len() && matches!(tokens[*pos].kind, TokenKind::Ident(_)) { + if let TokenKind::Ident(p) = &tokens[*pos].kind { + params.push(p.clone()); + *pos += 1; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } else { + break; + } + } else { break; } + } + } + expect_kind(tokens, pos, TokenKind::Arrow)?; + let body = parse_expr_or_block(tokens, pos)?; + let end = e_span(&body).end; + Ok(Expr::Lambda(params, Box::new(body), Span::new(start, end))) + } + TokenKind::Struct => { + *pos += 1; + let name = match &tokens[*pos].kind { + TokenKind::Ident(n) => n.clone(), + _ => return Err(ParseError { message: "expected struct name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + }; + *pos += 1; + expect_kind(tokens, pos, TokenKind::Assign)?; + expect_kind(tokens, pos, TokenKind::LParen)?; + let mut fields = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RParen { + let field_name = match &tokens[*pos].kind { + TokenKind::Ident(n) => n.clone(), + _ => return Err(ParseError { message: "expected field name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + }; + *pos += 1; + expect_kind(tokens, pos, TokenKind::Assign)?; + let field_type = match &tokens[*pos].kind { + TokenKind::Ident(t) => t.clone(), + _ => return Err(ParseError { message: "expected type name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + }; + *pos += 1; + fields.push((field_name, field_type)); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } else { + break; + } + } + let end = expect_kind(tokens, pos, TokenKind::RParen)?; + Ok(Expr::StructDef(name, fields, Span::new(start, end))) + } + TokenKind::Data => { + *pos += 1; + let name = match &tokens[*pos].kind { + TokenKind::Ident(n) => n.clone(), + _ => return Err(ParseError { message: "expected data type name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + }; + *pos += 1; + let mut params = Vec::new(); + while *pos < tokens.len() && matches!(tokens[*pos].kind, TokenKind::Ident(_)) { + if let TokenKind::Ident(p) = &tokens[*pos].kind { + params.push(p.clone()); + *pos += 1; + } else { break; } + } + expect_kind(tokens, pos, TokenKind::Assign)?; + let mut constructors = Vec::new(); + loop { + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::RParen { + *pos += 1; + break; + } + let ctor_name = match &tokens[*pos].kind { + TokenKind::Ident(n) => n.clone(), + _ => return Err(ParseError { message: "expected constructor name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + }; + let ctor_start = tokens[*pos].start; + *pos += 1; + let mut fields = Vec::new(); + while *pos < tokens.len() && matches!(tokens[*pos].kind, TokenKind::Ident(_)) { + if let TokenKind::Ident(t) = &tokens[*pos].kind { + fields.push(t.clone()); + *pos += 1; + } else { break; } + } + let ctor_end = tokens[*pos - 1].end; + constructors.push(ConstructorDef { name: ctor_name, fields, span: Span::new(ctor_start, ctor_end) }); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Pipe { + *pos += 1; + } else { + break; + } + } + let end = tokens[*pos - 1].end; + Ok(Expr::DataDef(name, params, constructors, Span::new(start, end))) + } + TokenKind::For => { + *pos += 1; + let var = match &tokens[*pos].kind { + TokenKind::Ident(n) => n.clone(), + _ => return Err(ParseError { message: "expected loop variable".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + }; + *pos += 1; + expect_kind(tokens, pos, TokenKind::In)?; + let iterable = parse_expr(tokens, pos)?; + skip_newlines(tokens, pos); + expect_kind(tokens, pos, TokenKind::Colon)?; + let body = parse_expr_or_block(tokens, pos)?; + let end = e_span(&body).end; + Ok(Expr::For(var, Box::new(iterable), Box::new(body), Span::new(start, end))) + } + TokenKind::While => { + *pos += 1; + let cond = parse_expr(tokens, pos)?; + skip_newlines(tokens, pos); + expect_kind(tokens, pos, TokenKind::Colon)?; + let body = parse_expr_or_block(tokens, pos)?; + let end = e_span(&body).end; + Ok(Expr::While(Box::new(cond), Box::new(body), Span::new(start, end))) + } + TokenKind::Loop => { + *pos += 1; + expect_kind(tokens, pos, TokenKind::LBrace)?; + let body = parse_expr(tokens, pos)?; + let end = expect_kind(tokens, pos, TokenKind::RBrace)?; + Ok(Expr::Loop(Box::new(body), Span::new(start, end))) + } + TokenKind::Break => { + *pos += 1; + let opt = if *pos < tokens.len() && !matches!(tokens[*pos].kind, TokenKind::RBrace | TokenKind::NEWLINE | TokenKind::Semicolon | TokenKind::Eof) { + Some(Box::new(parse_expr(tokens, pos)?)) + } else { + None + }; + let end = if let Some(e) = &opt { e_span(e).end } else { tokens[*pos - 1].end }; + Ok(Expr::Break(opt, Span::new(start, end))) + } + TokenKind::Class => { + *pos += 1; + let name = match &tokens[*pos].kind { + TokenKind::Ident(n) => n.clone(), + _ => return Err(ParseError { message: "expected class name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + }; + *pos += 1; + let mut extends = None; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Extends { + *pos += 1; + if let TokenKind::Ident(parent) = &tokens[*pos].kind { + extends = Some(parent.clone()); + *pos += 1; + } else { + return Err(ParseError { message: "expected parent class name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }); + } + } + expect_kind(tokens, pos, TokenKind::Assign)?; + expect_kind(tokens, pos, TokenKind::LParen)?; + let mut fields = Vec::new(); + let mut methods = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RParen { + match &tokens[*pos].kind { + TokenKind::Ident(name) => { + let method_name = name.clone(); + let start = tokens[*pos].start; + *pos += 1; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Assign { + *pos += 1; + let field_type = if let TokenKind::Ident(t) = &tokens[*pos].kind { + t.clone() + } else { + return Err(ParseError { message: "expected type name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }); + }; + *pos += 1; + fields.push((method_name, Some(field_type))); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } + } else if *pos < tokens.len() && tokens[*pos].kind == TokenKind::LParen { + *pos += 1; + let mut params = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RParen { + if let TokenKind::Ident(p) = &tokens[*pos].kind { + params.push(p.clone()); + *pos += 1; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } + } else { + return Err(ParseError { message: "expected parameter name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }); + } + } + expect_kind(tokens, pos, TokenKind::RParen)?; + expect_kind(tokens, pos, TokenKind::Assign)?; + let body = parse_expr(tokens, pos)?; + let end = e_span(&body).end; + methods.push(MethodDef { + name: method_name, + params, + body: Box::new(body), + span: Span::new(start, end), + }); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Semicolon { + *pos += 1; + } + } else { + return Err(ParseError { message: "expected '=' or '(' after field/method name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }); + } + } + _ => return Err(ParseError { message: format!("unexpected token {:?}", tokens[*pos].kind), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + } + } + let _end = expect_kind(tokens, pos, TokenKind::RParen)?; + Ok(Expr::ClassDef { + name, + extends, + fields, + methods, + span: Span::new(start, _end), + }) + } + TokenKind::New => { + *pos += 1; + let class_name = match &tokens[*pos].kind { + TokenKind::Ident(n) => n.clone(), + _ => return Err(ParseError { message: "expected class name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + }; + *pos += 1; + expect_kind(tokens, pos, TokenKind::LParen)?; + let mut args = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RParen { + args.push(parse_expr(tokens, pos)?); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } + } + let _end = expect_kind(tokens, pos, TokenKind::RParen)?; + Ok(Expr::New(class_name, args, Span::new(start, _end))) + } + TokenKind::Percent => { + *pos += 1; + match &tokens[*pos].kind { + TokenKind::LParen => { + *pos += 1; + let mut entries = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RParen { + let key = parse_expr(tokens, pos)?; + expect_kind(tokens, pos, TokenKind::FatArrow)?; + let value = parse_expr(tokens, pos)?; + entries.push((key, value)); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } + } + let _end = expect_kind(tokens, pos, TokenKind::RParen)?; + Ok(Expr::MapLiteral(entries, Span::new(start, _end))) + } + TokenKind::LBracket => { + *pos += 1; + let mut elems = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RBracket { + elems.push(parse_expr(tokens, pos)?); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } + } + let end = expect_kind(tokens, pos, TokenKind::RBracket)?; + Ok(Expr::SetLiteral(elems, Span::new(start, end))) + } + _ => Err(ParseError { message: "expected '(' or '[' after '%'".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + } + } + _ => Err(ParseError { message: format!("unexpected token {:?}", tokens[*pos].kind), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + } +} + +fn is_struct_constructor(tokens: &[Token], mut pos: usize) -> bool { + let mut depth = 1; + while pos < tokens.len() && depth > 0 { + match tokens[pos].kind { + TokenKind::LParen => depth += 1, + TokenKind::RParen => depth -= 1, + TokenKind::Assign => { + if depth == 1 { + return true; + } + } + _ => {} + } + pos += 1; + } + false +} + +fn parse_postfix(mut expr: Expr, tokens: &[Token], pos: &mut usize) -> ParseResult { + loop { + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::LBracket { + let start = tokens[*pos].start; + *pos += 1; + let index = parse_expr(tokens, pos)?; + let end = expect_kind(tokens, pos, TokenKind::RBracket)?; + expr = Expr::Index(Box::new(expr), Box::new(index), Span::new(start, end)); + } else if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Dot { + let start = tokens[*pos].start; + *pos += 1; + if let TokenKind::Ident(field) = &tokens[*pos].kind { + let field = field.clone(); + *pos += 1; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::LParen { + *pos += 1; + let mut args = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RParen { + args.push(parse_expr(tokens, pos)?); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } + } + let end = expect_kind(tokens, pos, TokenKind::RParen)?; + expr = Expr::MethodCall(Box::new(expr), field, args, Span::new(start, end)); + } else { + let end = tokens[*pos - 1].end; + expr = Expr::FieldAccess(Box::new(expr), field, Span::new(start, end)); + } + } else { + return Err(ParseError { message: "expected field or method name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }); + } + } else { + break; + } + } + Ok(expr) +} + +fn parse_application(mut func: Expr, tokens: &[Token], pos: &mut usize) -> ParseResult { + loop { + if *pos < tokens.len() && is_argument_start(&tokens[*pos].kind) { + let arg = parse_expr(tokens, pos)?; + let end = e_span(&arg).end; + let start = e_span(&func).start; + func = Expr::App(Box::new(func), Box::new(arg), Span::new(start, end)); + } else { + break; + } + } + Ok(func) +} + +fn is_argument_start(kind: &TokenKind) -> bool { + matches!(kind, + TokenKind::Literal(_) | TokenKind::Ident(_) | TokenKind::LParen | TokenKind::LBracket | + TokenKind::Backslash | TokenKind::Lambda | TokenKind::If | TokenKind::Let | TokenKind::Case | + TokenKind::Try | TokenKind::Error | TokenKind::For | TokenKind::While | TokenKind::Not | + TokenKind::Minus | TokenKind::Percent | TokenKind::Class | TokenKind::New | TokenKind::Struct | TokenKind::Data | + TokenKind::FString(_) | TokenKind::Loop | TokenKind::Break + ) +} + +fn parse_expr_or_block(tokens: &[Token], pos: &mut usize) -> ParseResult { + skip_newlines(tokens, pos); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::INDENT { + let start = tokens[*pos].start; + *pos += 1; + let mut exprs = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::DEDENT { + let e = parse_expr(tokens, pos)?; + exprs.push(e); + if *pos < tokens.len() && (tokens[*pos].kind == TokenKind::NEWLINE || tokens[*pos].kind == TokenKind::Semicolon) { + *pos += 1; + } + } + let end = if *pos < tokens.len() && tokens[*pos].kind == TokenKind::DEDENT { + let dedent_end = tokens[*pos].end; + *pos += 1; + dedent_end + } else { + tokens[*pos - 1].end + }; + if exprs.len() == 1 { + Ok(exprs.remove(0)) + } else { + Ok(Expr::Block(exprs, Span::new(start, end))) + } + } else { + parse_expr(tokens, pos) + } +} + +fn parse_pattern(tokens: &[Token], pos: &mut usize) -> Result { + if *pos >= tokens.len() { + return Err(ParseError { message: "incomplete input".to_string(), span: Span::dummy() }); + } + let start = tokens[*pos].start; + match &tokens[*pos].kind { + TokenKind::Ident(name) => { + *pos += 1; + let end = tokens[*pos - 1].end; + if name == "_" { + Ok(Pattern::Wildcard(Span::new(start, end))) + } else { + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::LParen { + *pos += 1; + let mut sub_pats = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RParen { + let pat = parse_pattern(tokens, pos)?; + sub_pats.push(pat); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } + } + let end = expect_kind(tokens, pos, TokenKind::RParen)?; + Ok(Pattern::Constructor(name.clone(), sub_pats, Span::new(start, end))) + } else { + Ok(Pattern::Var(name.clone(), Span::new(start, end))) + } + } + } + TokenKind::Literal(lit) => { + *pos += 1; + let end = tokens[*pos - 1].end; + Ok(Pattern::Literal(lit.clone(), Span::new(start, end))) + } + TokenKind::LBracket => { + *pos += 1; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::RBracket { + *pos += 1; + let end = tokens[*pos - 1].end; + return Ok(Pattern::List(vec![], Span::new(start, end))); + } + let mut pats = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RBracket { + pats.push(parse_pattern(tokens, pos)?); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } + } + let end = expect_kind(tokens, pos, TokenKind::RBracket)?; + Ok(Pattern::List(pats, Span::new(start, end))) + } + TokenKind::LParen => { + *pos += 1; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::RParen { + *pos += 1; + let end = tokens[*pos - 1].end; + return Ok(Pattern::Literal(Literal::Unit, Span::new(start, end))); + } + let pat = parse_pattern(tokens, pos)?; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + let mut pats = vec![pat]; + while *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + pats.push(parse_pattern(tokens, pos)?); + } + let end = expect_kind(tokens, pos, TokenKind::RParen)?; + Ok(Pattern::Tuple(pats, Span::new(start, end))) + } else { + let end = expect_kind(tokens, pos, TokenKind::RParen)?; + Ok(pat) + } + } + TokenKind::LBrace => { + *pos += 1; + let mut fields = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RBrace { + let field_name = match &tokens[*pos].kind { + TokenKind::Ident(n) => n.clone(), + _ => return Err(ParseError { message: "expected field name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + }; + *pos += 1; + expect_kind(tokens, pos, TokenKind::Assign)?; + let pat = parse_pattern(tokens, pos)?; + fields.push((field_name, pat)); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } + } + let end = expect_kind(tokens, pos, TokenKind::RBrace)?; + Ok(Pattern::Record(fields, Span::new(start, end))) + } + _ => Err(ParseError { message: format!("unexpected token {:?}", tokens[*pos].kind), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + } +} + +fn parse_type(tokens: &[Token], pos: &mut usize) -> Result { + let mut type_str = String::new(); + while *pos < tokens.len() { + match &tokens[*pos].kind { + TokenKind::Ident(name) => { + if !type_str.is_empty() { type_str.push(' '); } + type_str.push_str(name); + *pos += 1; + } + TokenKind::Arrow => { + type_str.push_str(" -> "); + *pos += 1; + } + _ => break, + } + } + if type_str.is_empty() { + Err(ParseError { message: "expected type".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }) + } else { + Ok(type_str) + } +} + +fn expect_kind(tokens: &[Token], pos: &mut usize, expected: TokenKind) -> Result { + if *pos < tokens.len() && tokens[*pos].kind == expected { + let end = tokens[*pos].end; + *pos += 1; + Ok(end) + } else { + Err(ParseError { message: format!("expected {:?}", expected), span: Span::new(tokens[*pos].start, tokens[*pos].end) }) + } +} + +fn parse_fstring(raw: &str) -> Result, ParseError> { + let mut parts = Vec::new(); + let chars: Vec = raw.chars().collect(); + let mut i = 0; + while i < chars.len() { + let ch = chars[i]; + if ch == '{' { + if i + 1 < chars.len() && chars[i + 1] == '{' { + parts.push(FStringPart::Literal("{".to_string())); + i += 2; + continue; + } + i += 1; + let start = i; + let mut depth = 1; + let mut in_string = false; + while i < chars.len() && depth > 0 { + let c = chars[i]; + if c == '"' && (i == 0 || chars[i-1] != '\\') { + in_string = !in_string; + } + if !in_string { + if c == '{' { + depth += 1; + } else if c == '}' { + depth -= 1; + if depth == 0 { + break; + } + } + } + i += 1; + } + if depth != 0 { + return Err(ParseError { message: "unclosed expression in f-string".to_string(), span: Span::dummy() }); + } + let expr_str: String = chars[start..i].iter().collect(); + let expr = parse_expression(&expr_str)?; + parts.push(FStringPart::Expr(Box::new(expr))); + i += 1; + } else if ch == '}' && i + 1 < chars.len() && chars[i + 1] == '}' { + parts.push(FStringPart::Literal("}".to_string())); + i += 2; + } else { + let start = i; + while i < chars.len() { + let c = chars[i]; + if c == '{' || c == '}' { + break; + } + i += 1; + } + let lit: String = chars[start..i].iter().collect(); + let processed = process_escapes(&lit)?; + parts.push(FStringPart::Literal(processed)); + } + } + Ok(parts) +} + +fn process_escapes(s: &str) -> Result { + let mut result = String::new(); + let mut chars = s.chars().peekable(); + while let Some(ch) = chars.next() { + if ch == '\\' { + if let Some(esc) = chars.next() { + match esc { + 'n' => result.push('\n'), + 't' => result.push('\t'), + 'r' => result.push('\r'), + '\\' => result.push('\\'), + '"' => result.push('"'), + '\'' => result.push('\''), + _ => return Err(ParseError { message: format!("invalid escape sequence: \\{}", esc), span: Span::dummy() }), + } + } else { + return Err(ParseError { message: "unfinished escape sequence".to_string(), span: Span::dummy() }); + } + } else { + result.push(ch); + } + } + Ok(result) +} + +fn parse_list_comp_generators(tokens: &[Token], pos: &mut usize, mut gens: Vec<(String, Box)>, mut filters: Vec>) -> ParseResult<(Vec<(String, Box)>, Vec>)> { + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RBracket { + match tokens[*pos].kind { + TokenKind::For => { + *pos += 1; + let var = match &tokens[*pos].kind { + TokenKind::Ident(n) => n.clone(), + _ => return Err(ParseError { message: "expected loop variable".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + }; + *pos += 1; + expect_kind(tokens, pos, TokenKind::In)?; + let iter = parse_expr(tokens, pos)?; + gens.push((var, Box::new(iter))); + } + TokenKind::If => { + *pos += 1; + let cond = parse_expr(tokens, pos)?; + filters.push(Box::new(cond)); + } + _ => break, + } + } + Ok((gens, filters)) +} diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..23e44c8 --- /dev/null +++ b/src/server.rs @@ -0,0 +1,118 @@ +//! Contact server (unchanged except for error handling). + +use std::collections::BTreeMap; +use std::io::{BufRead, BufReader, Write}; +use std::sync::{Arc, Mutex}; +use std::thread; +use native_tls::TlsStream; +use std::net::TcpStream; +use std::sync::mpsc::Sender; + +pub type ContactsDb = Arc>>; + +pub fn start_contacts_server(addr: &str, password: Option) -> Result<(ContactsDb, thread::JoinHandle<()>, Sender<()>), Box> { + let db = Arc::new(Mutex::new(BTreeMap::new())); + let listener = std::net::TcpListener::bind(addr)?; + listener.set_nonblocking(true)?; + let acceptor = crate::p2p::get_tls_acceptor(); + let db_clone = db.clone(); + let addr_owned = addr.to_string(); + let password_arc = Arc::new(password); + + let (stop_tx, stop_rx) = std::sync::mpsc::channel(); + + let handle = thread::spawn(move || { + println!("Contact server (TLS) listening on {}", addr_owned); + loop { + if let Ok(()) = stop_rx.try_recv() { + println!("Contact server stopping."); + break; + } + + match listener.accept() { + Ok((stream, _)) => { + let db = db_clone.clone(); + let acceptor = acceptor.clone(); + let password = password_arc.clone(); + thread::spawn(move || { + if let Ok(tls_stream) = acceptor.accept(stream) { + handle_connection(tls_stream, db, password); + } + }); + } + Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => { + thread::sleep(std::time::Duration::from_millis(10)); + continue; + } + Err(e) => { + eprintln!("Contact server accept error: {}", e); + break; + } + } + } + }); + + Ok((db, handle, stop_tx)) +} + +fn handle_connection(stream: TlsStream, db: ContactsDb, password: Arc>) { + let mut reader = BufReader::new(stream); + let mut line = String::new(); + + let mut authenticated = password.is_none(); + if let Some(pass) = &*password { + if let Ok(n) = reader.read_line(&mut line) { + if n == 0 { return; } + let trimmed = line.trim(); + if trimmed.starts_with("PASSWORD ") { + let provided = trimmed[9..].trim(); + if provided == pass { + authenticated = true; + let _ = writeln!(reader.get_mut(), "OK"); + let _ = reader.get_mut().flush(); + } else { + let _ = writeln!(reader.get_mut(), "ERROR Invalid password"); + let _ = reader.get_mut().flush(); + return; + } + } else { + let _ = writeln!(reader.get_mut(), "ERROR Password required"); + let _ = reader.get_mut().flush(); + return; + } + } else { + return; + } + } + + line.clear(); + while let Ok(n) = reader.read_line(&mut line) { + if n == 0 { break; } + let trimmed = line.trim(); + let parts: Vec<&str> = trimmed.splitn(2, ' ').collect(); + match parts[0] { + "REGISTER" => { + if authenticated && parts.len() == 2 { + let mut split = parts[1].splitn(2, ' '); + if let (Some(uid), Some(addr)) = (split.next(), split.next()) { + db.lock().unwrap().insert(uid.to_string(), addr.to_string()); + let _ = writeln!(reader.get_mut(), "OK"); + let _ = reader.get_mut().flush(); + } + } + } + "GET" => { + if authenticated { + let contacts = db.lock().unwrap().iter() + .map(|(k, v)| format!("{} {}", k, v)) + .collect::>() + .join("\n"); + let _ = writeln!(reader.get_mut(), "{}", contacts); + let _ = reader.get_mut().flush(); + } + } + _ => {} + } + line.clear(); + } +} diff --git a/src/tests/integration.rs b/src/tests/integration.rs new file mode 100644 index 0000000..0c19bce --- /dev/null +++ b/src/tests/integration.rs @@ -0,0 +1,56 @@ +#[test] +fn test_loop_and_break() { + let state = Arc::new(Mutex::new(ChatState::new())); + let mut env = Environment::new(); + let env_arc = Arc::new(Mutex::new(env.clone())); + builtins::populate(&mut env, Arc::clone(&state), env_arc); + + // loop with break + let input = "let x = 0\nloop { x = x + 1; if x == 5 then break x else () }"; + let expr = parse_script(input).unwrap(); + let result = eval_expr(&expr, &mut env, state).unwrap(); + assert_eq!(result.display(), "5"); +} + +#[test] +fn test_list_comprehension() { + let state = Arc::new(Mutex::new(ChatState::new())); + let mut env = Environment::new(); + let env_arc = Arc::new(Mutex::new(env.clone())); + builtins::populate(&mut env, Arc::clone(&state), env_arc); + + let expr = parse_expression("[x * 2 for x in [1,2,3] if x > 1]").unwrap(); + let result = eval_expr(&expr, &mut env, state).unwrap(); + assert_eq!(result.display(), "[4, 6]"); +} + +#[test] +fn test_load_and_del() { + use std::fs; + use std::io::Write; + let state = Arc::new(Mutex::new(ChatState::new())); + let mut env = Environment::new(); + let env_arc = Arc::new(Mutex::new(env.clone())); + builtins::populate(&mut env, Arc::clone(&state), env_arc); + + // Create temp file + let temp_path = "test_load.plic"; + let content = "let x = 42"; + fs::write(temp_path, content).unwrap(); + + let expr = parse_script(&format!("load \"{}\"", temp_path)).unwrap(); + eval_expr(&expr, &mut env, Arc::clone(&state)).unwrap(); + // Now x should be defined + let expr2 = parse_expression("x").unwrap(); + let result = eval_expr(&expr2, &mut env, Arc::clone(&state)).unwrap(); + assert_eq!(result.display(), "42"); + + // Test del + let expr3 = parse_expression("del \"x\"").unwrap(); + eval_expr(&expr3, &mut env, Arc::clone(&state)).unwrap(); + let expr4 = parse_expression("x").unwrap(); + let result4 = eval_expr(&expr4, &mut env, state); + assert!(result4.is_err()); + + fs::remove_file(temp_path).ok(); +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..68fedc7 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,260 @@ +use crate::ast::Expr; +use crate::error::ChatError; +use serde::{Serialize, Deserialize}; +use std::collections::BTreeMap; +use std::collections::BTreeSet; +use std::fmt; +use std::sync::Arc; + +#[derive(Debug, Clone)] +pub struct FetchOptions { + pub url: String, + pub method: String, + pub headers: Vec<(String, String)>, + pub body: Option, +} + +#[derive(Debug, Clone)] +pub struct FetchResult { + pub status: i64, + pub body: String, + pub headers: Vec<(String, String)>, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Number { + Int(i64), + Float(f64), +} + +impl Number { + pub fn to_f64(&self) -> f64 { + match self { + Number::Int(i) => *i as f64, + Number::Float(f) => *f, + } + } + pub fn to_i64(&self) -> i64 { + match self { + Number::Int(i) => *i, + Number::Float(f) => *f as i64, + } + } +} + +#[derive(Clone)] +pub enum Value { + Num(Number), + Char(char), + String(String), + Bool(bool), + Unit, + Uid(String), + ByteString(Vec), + List(Vec), + Tuple(Vec), + Closure(Vec, Expr, Environment), + BuiltinFunc(String, usize, Arc) -> Result + Send + Sync>), + Curry { + name: String, + arity: usize, + f: Arc) -> Result + Send + Sync>, + args: Vec, + }, + Custom(String, Vec), + Record(BTreeMap), + Pid(usize), + DateTime(chrono::DateTime), + Duration(std::time::Duration), + Json(JsonValue), + Maybe(Option>), + Either(Box), + ChatMsg { + from: String, + text: String, + chat: String, + attachment: Option>, + }, + FileInfo { + name: String, + size: i64, + mime: String, + }, + FileTransfer { + from: String, + filename: String, + data: Vec, + }, + FetchOptions(FetchOptions), + FetchResult(FetchResult), + Map(BTreeMap), + Set(BTreeSet), + ClassInstance { + class: String, + fields: BTreeMap, + }, + Break(Option>), // internal use only +} + +#[derive(Clone, Serialize, Deserialize)] +pub enum JsonValue { + Null, + Bool(bool), + Number(f64), + String(String), + Array(Vec), + Object(BTreeMap), +} + +#[derive(Clone)] +pub struct Either { + pub left: Option>, + pub right: Option>, +} + +impl fmt::Debug for Value { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.display()) + } +} + +impl Value { + pub fn display(&self) -> String { + match self { + Value::Num(n) => match n { + Number::Int(i) => format!("{}", i), + Number::Float(x) => { + let s = format!("{:.10}", x); + if s == "-0.0000000000" { "0.0".to_string() } + else { + let trimmed = s.trim_end_matches('0').trim_end_matches('.'); + if trimmed.is_empty() { "0".to_string() } else { trimmed.to_string() } + } + } + } + Value::Char(c) => c.to_string(), + Value::String(s) => s.clone(), + Value::Bool(b) => b.to_string(), + Value::Unit => "()".to_string(), + Value::Uid(u) => u.clone(), + Value::ByteString(b) => format!("#B\"{}\"", hex::encode(b)), + Value::List(v) => { + let items: Vec = v.iter().map(|x| x.display()).collect(); + format!("[{}]", items.join(", ")) + } + Value::Tuple(v) => { + let items: Vec = v.iter().map(|x| x.display()).collect(); + format!("({})", items.join(", ")) + } + Value::Closure(_, _, _) => "".to_string(), + Value::BuiltinFunc(name, _, _) => format!("", name), + Value::Curry { name, .. } => format!("", name), + Value::Custom(name, args) => { + let args_str: Vec = args.iter().map(|x| x.display()).collect(); + if args.is_empty() { + name.clone() + } else { + format!("{} {}", name, args_str.join(" ")) + } + } + Value::Record(map) => { + let fields: Vec = map.iter() + .map(|(k, v)| format!("{} = {}", k, v.display())) + .collect(); + format!("{{ {} }}", fields.join(", ")) + } + Value::Pid(pid) => format!("", pid), + Value::DateTime(dt) => dt.to_rfc3339(), + Value::Duration(d) => format!("{}s", d.as_secs_f64()), + Value::Json(j) => serde_json::to_string_pretty(j).unwrap_or_default(), + Value::Maybe(m) => match m { + Some(v) => format!("Just {}", v.display()), + None => "Nothing".to_string(), + }, + Value::Either(e) => { + if let Some(left) = &e.left { + format!("Left {}", left.display()) + } else if let Some(right) = &e.right { + format!("Right {}", right.display()) + } else { + "Either".to_string() + } + } + Value::ChatMsg { from, text, chat, attachment } => { + let attach = if let Some(a) = attachment { + format!(" with {}", a.display()) + } else { "".to_string() }; + format!("[Message from {} in {}: \"{}\"{}]", from, chat, text, attach) + } + Value::FileInfo { name, size, mime } => { + format!("[FileInfo {} ({} bytes, {})]", name, size, mime) + } + Value::FileTransfer { from, filename, .. } => { + format!("[FileTransfer from {}: {}]", from, filename) + } + Value::FetchOptions(opts) => { + format!("FetchOptions {{ url: {}, method: {}, headers: {:?}, body: {:?} }}", + opts.url, opts.method, opts.headers, opts.body) + } + Value::FetchResult(res) => { + format!("FetchResult {{ status: {}, body: {}, headers: {:?} }}", + res.status, res.body, res.headers) + } + Value::Map(map) => { + let entries: Vec = map.iter() + .map(|(k, v)| format!("{}: {}", k, v.display())) + .collect(); + format!("%{{{}}}", entries.join(", ")) + } + Value::Set(set) => { + let elems: Vec = set.iter().cloned().collect(); + format!("%[{}]", elems.join(", ")) + } + Value::ClassInstance { class, fields } => { + let flds: Vec = fields.iter() + .map(|(k, v)| format!("{} = {}", k, v.display())) + .collect(); + format!("{} {{ {} }}", class, flds.join(", ")) + } + Value::Break(opt) => { + if let Some(v) = opt { + format!("break {}", v.display()) + } else { + "break".to_string() + } + } + } + } +} + +impl fmt::Display for Value { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.display()) + } +} + +#[derive(Debug, Clone)] +pub struct Environment { + pub vars: BTreeMap, + pub type_map: BTreeMap, +} + +impl Environment { + pub fn new() -> Self { + Environment { vars: BTreeMap::new(), type_map: BTreeMap::new() } + } + + pub fn get(&self, name: &str) -> Option { + self.vars.get(name).cloned() + } + + pub fn set(&mut self, name: String, val: Value) { + self.vars.insert(name, val); + } + + pub fn extend(&self, other: BTreeMap) -> Self { + let mut new = self.clone(); + new.vars.extend(other); + new + } +} From eb6cb992fe9213d9db5a93b955a6ec1f2a8b05ad Mon Sep 17 00:00:00 2001 From: one-of-all Date: Sun, 19 Jul 2026 10:06:10 +0300 Subject: [PATCH 05/18] Delete lang_example directory --- lang_example/README.md | 598 ------------------------------------ lang_example/compiler.gleam | 7 - 2 files changed, 605 deletions(-) delete mode 100644 lang_example/README.md delete mode 100644 lang_example/compiler.gleam diff --git a/lang_example/README.md b/lang_example/README.md deleted file mode 100644 index cd4d3fe..0000000 --- a/lang_example/README.md +++ /dev/null @@ -1,598 +0,0 @@ -# ChatLang 3.1 Documentation - -## Introduction -ChatLang is a dynamic programming language with a functional core and built‑in P2P chat support. It combines functional, imperative, and object‑oriented paradigms. - -This version supports: -- Arithmetic (Int, Float, mixed) with unary minus. -- Lambda functions: `lambda x, y -> ...` or `\ x, y -> ...` (parameters separated by commas). -- Function definitions: `let name arg1, arg2 = ...` (optional type annotations). -- Conditionals `if ... then ... else ...`. -- Lists, strings, byte strings, tuples, records, maps, sets. -- Classes with methods and single inheritance. -- Algebraic data types (union) with pattern matching. -- Built‑in functions (math, string, list, JSON, file I/O, network, chat, processes, cryptography). -- Processes (`spawn`, `procSend`, `procRecv`, `procWait`, `procExit`, `sleep`, `after`). -- Chat functions (`login`, `new`, `add`, `abort`, `open`, `send`, `sendFile`, `sendChat`, `sendFileToChat`, `inbox`, `history`, `downloads`, `saveFile`, `logout`, `deleteUser`, `deleteChat`, `listChats`, `members`). -- Contact server with optional password (`serverStart`, `serverStop`, `connect`). -- P2P messaging and file transfer over TLS. -- Post‑quantum cryptography (Kyber key encapsulation). -- SHA‑256 hashing. -- Extended file system operations. -- Type introspection (`typeof`). -- Optional type annotations with runtime checking. -- F‑strings: `f"text {expression} text"`. -- REPL commands: `exit()` and `load(filename)` only; no automatic output of expression results. - -All features described below are implemented and tested. - ---- - -## 1. REPL -Launch with `cargo run` or `chatlang`. - -- Enter single‑line expressions; results are **not** printed automatically. Use `show(expr)` to display a value. -- For multi‑line blocks, use `{ ... }`. Inside a block, expressions are separated by `;` or newlines. The block returns the value of the last expression. Empty block `{}` returns `()`. -- Built‑in functions for controlling the REPL: - - `exit()` – terminates the interpreter. - - `load(filename)` – loads and executes a ChatLang script in the current environment. Definitions persist. -- Errors are printed to stderr with a red `error:` prefix. - -Example: -``` ->>> let greet name = "Hello, " ++ name ++ "!" ->>> show(greet "World") -Hello, World! ->>> { - let square x = x * x - show(square 5) -} -25 -``` - ---- - -## 2. Lexical Elements and Syntax - -### Comments -- Single‑line: `# text` -- Multi‑line: `#- text -#` (nested supported) - -### Identifiers -- Letters, digits, `_` (starting with a letter or `_`). - -### Literals -- Integers: `42`, `-7` -- Floats: `3.14`, `-0.5`, `1.0e10` -- Characters: `'a'`, `'\n'` -- Strings: `"Hello"`, `"line1\nline2"` -- Booleans: `true`, `false` -- Unit: `()` -- UID: `@alice`, `@everyone` -- Lists: `[1, 2, 3]`, `[0..10]` (range, excludes upper bound) -- Tuples: `(1, "ok")`, `(true, @bob)` -- Byte strings: `#B"48656C6C6F"` -- Durations: `5s`, `500ms`, `2m`, `1h` -- Map literal: `%(key => value, ...)` (using `=>`) -- Set literal: `%[elem, ...]` -- F‑string: `f"text {expression} text"` (see section 4.8) - ---- - -## 3. Value Types -- Primitive: `Int`, `Float`, `Char`, `String`, `Bool`, `Unit`, `Uid`, `ByteString`, `Pid`, `DateTime`, `Duration`. -- Composite: lists, tuples, records. -- Custom: `data` definitions, `struct` definitions, classes. -- Collections: `Map` and `Set`. - ---- - -## 4. Expressions - -### 4.1. Basics -- Literals, variables. -- Function application: `f arg1, arg2` (arguments separated by commas). If one argument, no comma is needed. Parentheses can be used for grouping: `f (arg1, arg2)`. -- Lambda: `lambda x, y -> x + y` or `\ x -> x * 2`. -- Indexing: `expr[index]` works on lists, strings, byte strings, tuples, maps (returns value or `Unit`), sets (returns `Bool`). - -### 4.2. Conditional -``` -if condition then expr1 else expr2 -``` -Single‑line only. - -### 4.3. Pattern Matching -``` -case expr of - pattern1 -> expr1 - pattern2 -> expr2 - _ -> default -``` -One‑line variant with semicolons: -``` -case expr of pattern1 -> expr1; pattern2 -> expr2; _ -> default -``` - -### 4.4. Local Definitions -- `let x = 5` – defines a new variable (error if already defined). -- `let f x, y = x + y` – function definition. -- Type annotations: `let N :: Int = 5`, `let add :: Int -> Int -> Int = lambda a, b -> a + b`. -- `let ... in` is **not** supported; use blocks `{ ... }` for scoping. - -### 4.5. Operators (precedence, high to low) -1. `not` (unary) -2. `*`, `/`, `%` (left associative) -3. `+`, `-` (left associative) -4. `++` (string/list concatenation, right associative) -5. `:` (cons, right associative) -6. `in`, `not in` (membership) -7. `==`, `!=`, `<`, `>`, `<=`, `>=` -8. `and`, `or` (short‑circuit) -9. `|>` (pipe, left associative) - -Operator `$` is supported (low‑precedence application, right‑associative). - -### 4.6. Loops -- `for x in collection: body` – iterates over lists, sets, maps (yields key‑value tuples for maps). -- `while condition: body` - -Loops return `()`. - -### 4.7. Error Handling -- `error "message"` – throws an error. -- `try expr` – catches error (if any, passes it on). -- `try expr catch pattern -> handler` – handles error. - -Example: -``` -try error "oops" catch e -> "caught: " ++ e -``` - -### 4.8. F‑strings -Syntax: `f"text {expression} text"`. Inside braces, any ChatLang expression can appear; it is evaluated and converted to a string via `show`. Double braces `{{` and `}}` are used to insert literal `{` and `}` characters. - -Example: -``` ->>> let x = 5 ->>> f"x = {x}, x*2 = {x * 2}" -"x = 5, x*2 = 10" -``` - ---- - -## 5. Structs and Algebraic Data Types - -``` -struct Person = (name = String, age = Int) -data Result a = Ok a | Err String -``` - -Struct construction: `Person(name = "Alice", age = 30)`. Field access via `.` (e.g., `person.name`). Update via record update syntax (not directly; use functions or manual reconstruction). - ---- - -## 6. Classes (OOP) - -``` -class Counter = (val = Int; inc(self) = self { val = self.val + 1 }; get(self) = self.val) -``` - -- Constructor: `new Counter(0)` -- Inheritance: `class AdvancedCounter extends Counter = (...)` (with parentheses) -- Method call: `counter.inc()` - ---- - -## 7. Unions (ADT) - -``` -data Option a = None | Some a -``` - -Constructors: `Some 42`, `None`. Pattern matching works. - ---- - -## 8. Map and Set - -### Map -- Create: `%(key => value, ...)` -- Functions: `mapGet`, `mapSet`, `mapRemove`, `mapKeys`, `mapValues`, `mapEntries`, `mapContains`, `mapSize`, `mapFilter`, `mapMerge` -- Indexing: `map[key]` returns value or `Unit` - -### Set -- Create: `%[elem, ...]` -- Functions: `setAdd`, `setRemove`, `setContains`, `setUnion`, `setIntersection`, `setDifference`, `setSize`, `setFilter`, `setMap` -- Indexing: `set[elem]` returns `Bool` - ---- - -## 9. Built‑in Functions - -### 9.1. Mathematics -- `sqrt`, `sin`, `cos`, `tan`, `asin`, `acos`, `atan :: Float -> Float` -- `intToFloat :: Int -> Float`, `floatToInt :: Float -> Int` - -### 9.2. Conversions and Type Introspection -- `show :: a -> String` – converts a value to a string and prints it. -- `parseInt :: String -> Int`, `parseFloat :: String -> Float` -- `chr :: Int -> Char`, `ord :: Char -> Int` -- `typeof :: a -> String` - -### 9.3. List, String, ByteString, Map, Set -- `null :: [a] -> Bool` -- `length :: collection -> Int` (works on List, String, ByteString, Map, Set, Tuple) -- `map :: (a -> b) -> [a] -> [b]` -- `filter :: (a -> Bool) -> [a] -> [a]` -- `foldl :: (b -> a -> b) -> b -> [a] -> b` -- `foldr :: (a -> b -> b) -> b -> [a] -> b` -- `take :: Int -> collection -> collection` -- `drop :: Int -> collection -> collection` -- `reverse :: [a] -> [a]` -- `all :: (a -> Bool) -> [a] -> Bool` -- `any :: (a -> Bool) -> [a] -> Bool` -- `find :: (a -> Bool) -> [a] -> Maybe a` -- `sort :: [a] -> [a]` (by string representation) -- `sortBy :: (a -> a -> Int) -> [a] -> [a]` -- `sum :: [a] -> a` (numbers) -- `concat :: [[a]] -> [a]` -- `flatten :: [[a]] -> [a]` -- `zip :: [a] -> [b] -> [(a,b)]` -- `zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]` -- `unzip :: [(a,b)] -> ([a],[b])` -- `indexOf :: [a] -> a -> Maybe Int` -- `lastIndexOf :: [a] -> a -> Maybe Int` - -### 9.4. String Functions -- `split :: String -> String -> [String]` -- `join :: String -> [String] -> String` -- `startsWith :: String -> String -> Bool` -- `endsWith :: String -> String -> Bool` -- `trim :: String -> String` -- `replace :: String -> String -> String -> String` -- `substring :: Int -> Int -> String -> String` - -### 9.5. JSON -- `parseJson :: String -> JsonValue` -- `encodeJson :: JsonValue -> String` -- `lookup :: String -> JsonValue -> Maybe JsonValue` - -### 9.6. Time -- `formatTime :: String -> DateTime -> String` -- `parseTime :: String -> String -> DateTime` -- `addDuration :: DateTime -> Duration -> DateTime` -- `diffDuration :: DateTime -> DateTime -> Duration` -- `now :: DateTime` - -### 9.7. ByteString -- `packBytes :: [Int] -> ByteString` -- `unpackBytes :: ByteString -> [Int]` - -### 9.8. I/O and Files -- `putStrLn :: String -> ()` – prints a raw string. -- `getLine :: String` -- `getArgs :: [String]` -- `readFile :: String -> String` -- `readBinaryFile :: String -> ByteString` -- `writeFile :: String -> String -> ()` -- `appendFile :: String -> String -> ()` -- `writeBinaryFile :: String -> ByteString -> ()` -- `fileExists :: String -> Bool` -- `fileSize :: String -> Int` -- `listDir :: String -> [String]` -- `createDir :: String -> ()` -- `removeDir :: String -> ()` -- `fileMove :: String -> String -> ()` -- `filePermissions :: String -> Int` -- `setFilePermissions :: String -> Int -> ()` - -### 9.9. Network -- `fetch :: String -> FetchResult` -- `fetchOpts :: FetchOptions -> FetchResult` - -### 9.10. Chat and Contacts -- `login :: Uid -> ()` – logs in as the given UID. -- `logout :: () -> ()` – logs out the current user. -- `deleteUser :: Uid -> ()` – deletes a user (and removes them from all chats). -- `new :: String -> [Uid] -> String` – creates a new chat with the given name and members. -- `add :: Uid -> String -> ()` – adds a member to a chat. -- `abort :: Uid -> String -> ()` – removes a member from a chat. -- `deleteChat :: String -> ()` – deletes a chat and its local history. -- `open :: String -> ()` – opens a chat (marks as active). -- `send :: Uid -> String -> Bool` – sends a private message to a user. -- `sendFile :: Uid -> String -> Bool` – sends a file privately. -- `sendChat :: String -> String -> Bool` – sends a message to all members of a chat. -- `sendFileToChat :: String -> String -> Bool` – sends a file to all members of a chat. -- `inbox :: () -> [ChatMsg]` – returns the current user's inbox. -- `history :: String -> [ChatMsg]` – returns the history of a given chat. -- `downloads :: () -> [FileTransfer]` – lists received files. -- `saveFile :: Int -> String -> Bool` – saves a downloaded file to disk. -- `listChats :: () -> [String]` – lists all chats the user is a member of. -- `members :: String -> [Uid]` – lists all members of a chat. -- `serverStart :: String -> (String?) -> ()` – starts a contact server (address, optional password). -- `serverStop :: () -> ()` – stops the contact server. -- `connect :: String -> Uid -> (String?) -> Int` – connects to a contact server and fetches contacts. -- `getPublicIP :: () -> Maybe String` -- `setExternalIP :: String -> ()` - -### 9.11. Processes -- `spawn :: (() -> ()) -> Pid` -- `procSelf :: Pid` -- `procSend :: Pid -> a -> ()` -- `procRecv :: a` -- `procWait :: Pid -> a` -- `procExit :: a -> ()` -- `sleep :: Duration -> ()` -- `after :: Duration -> (() -> ()) -> ()` - -### 9.12. Maybe -- `Nothing :: Maybe a` -- `Just :: a -> Maybe a` -- `maybe :: (a -> b) -> b -> Maybe a -> b` - -### 9.13. Map & Set (additional) -- `mapGet :: Map -> key -> value` -- `mapSet :: Map -> key -> value -> Map` -- `mapRemove :: Map -> key -> Map` -- `mapKeys :: Map -> [key]` -- `mapValues :: Map -> [value]` -- `mapEntries :: Map -> [(key, value)]` -- `mapContains :: Map -> key -> Bool` -- `mapSize :: Map -> Int` -- `mapFilter :: (k -> v -> Bool) -> Map -> Map` -- `mapMerge :: Map -> Map -> Map` -- `setAdd :: Set -> elem -> Set` -- `setRemove :: Set -> elem -> Set` -- `setContains :: Set -> elem -> Bool` -- `setUnion :: Set -> Set -> Set` -- `setIntersection :: Set -> Set -> Set` -- `setDifference :: Set -> Set -> Set` -- `setSize :: Set -> Int` -- `setFilter :: (a -> Bool) -> Set -> Set` -- `setMap :: (a -> b) -> Set -> Set` -- `listToSet :: [a] -> Set` -- `mapToList :: Map -> [(key, value)]` - -### 9.14. Cryptography -- `sha256 :: ByteString -> ByteString` -- `sha256String :: String -> String` -- `kyberKeyPair :: () -> (PublicKey, SecretKey)` (both as ByteString) -- `kyberEncapsulate :: PublicKey -> (Ciphertext, SharedSecret)` -- `kyberDecapsulate :: SecretKey -> Ciphertext -> SharedSecret` - -### 9.15. Variables -- `del :: String -> ()` – deletes a variable from the environment. - ---- - -## 10. Examples - -### 10.1. Arithmetic and Functions -``` ->>> show(1 + 2 * 3) -7 ->>> let sq x = x * x ->>> show(sq 5) -25 ->>> show((lambda x -> x + 1) 5) -6 ->>> show(-5) --5 ->>> show(5 - 2) -3 ->>> show(5 - -2) -7 -``` - -### 10.2. Variables and Assignments -``` ->>> let x = 5 ->>> show(x) -5 ->>> let x = 6 -error: variable 'x' already defined ->>> x = 10 ->>> show(x) -10 -``` - -### 10.3. Characters and Strings -``` ->>> show('a') -a ->>> show('\n') - ->>> let msg = "Hello" ->>> show(msg) -Hello ->>> show(msg ++ " world") -Hello world -``` - -### 10.4. Type Annotations and typeof -``` ->>> let N :: Int = 5 ->>> show(typeof N) -Int ->>> let add :: Int -> Int -> Int = lambda a, b -> a + b ->>> show(typeof add) -Closure ->>> let msg :: String = "hello" ->>> show(typeof msg) -String ->>> N = 'a' # type mismatch -error: Type mismatch: expected 'Int', got 'Char' -``` - -### 10.5. Conditionals -``` ->>> show(if 5 > 3 then "yes" else "no") -yes -``` - -### 10.6. Lists, Strings, Tuples -``` ->>> show([1, 2, 3] |> map(lambda x -> x * 2)) -[2, 4, 6] ->>> show("Hello, " ++ "world!") -Hello, world! ->>> show(length("abc")) -3 ->>> show([0..5]) -[0, 1, 2, 3, 4] ->>> show((1, "two", 3.0)[1]) -two ->>> show(length((1,2,3))) -3 -``` - -### 10.7. Pattern Matching -``` ->>> show(case 2 of 1 -> "one"; 2 -> "two"; _ -> "other") -two -``` - -### 10.8. JSON -``` ->>> let data = parseJson("[1, 2, 3]") ->>> show(data) -[1, 2, 3] ->>> show(encodeJson(data)) -[1,2,3] -``` - -### 10.9. Files and I/O -``` ->>> writeFile("test.txt", "Hello") -() ->>> show(readFile("test.txt")) -Hello ->>> show(fileExists("test.txt")) -true ->>> show(listDir(".")) -["test.txt", ...] -``` - -### 10.10. Map and Set -``` ->>> let m = %(1 => "one", 2 => "two") ->>> show(m[1]) -one ->>> show(mapSet(m, 3, "three")) -%(1: one, 2: two, 3: three) ->>> show(mapKeys(m2)) -[1, 2, 3] ->>> let s = %[1,2,3] ->>> show(setAdd(s, 4)) -%[1,2,3,4] ->>> show(s[2]) -true ->>> show(setContains(s, 5)) -false -``` - -### 10.11. Classes -``` ->>> class Counter = (val = Int; inc(self) = self { val = self.val + 1 }; get(self) = self.val) ->>> let c = new Counter(0) ->>> show(c.inc().get()) -1 -``` - -### 10.12. Structures -``` ->>> struct Person = (name = String, age = Int) ->>> let p = Person(name = "Alice", age = 30) ->>> show(p.name) -Alice -``` - -### 10.13. Chat (Complete Scenario with External IP and Password) - -**Set external IP and start server with password:** -``` ->>> setExternalIP("203.0.113.5") -() ->>> serverStart("0.0.0.0:9000", "secret") -() -``` - -**Alice:** -``` ->>> login(@alice) -() ->>> connect("127.0.0.1:9000", @alice, "secret") -1 ->>> new("general", [@bob, @alice]) -general ->>> open("general") -() ->>> sendChat("general", "Hello everyone!") -true ->>> send(@bob, "Hello Bob!") -true -``` - -**Bob (another instance):** -``` ->>> login(@bob) -() ->>> connect("127.0.0.1:9000", @bob, "secret") -1 ->>> show(inbox()) -[[Message from @alice in general: "Hello everyone!"]] ->>> show(history("general")) -[[Message from @alice in general: "Hello everyone!"]] -``` - -**File transfer:** -``` ->>> writeFile("report.txt", "Sales data") -() ->>> sendFileToChat("general", "report.txt") -true ->>> show(downloads()) -[[FileTransfer from @alice: report.txt]] ->>> saveFile(0, "received_report.txt") -true -``` - -### 10.14. Processes -``` ->>> let p = spawn(lambda () -> (procRecv() |> show)) ->>> procSend(p, "Hi") -() ->>> sleep(1s) -() -``` - -### 10.15. Cryptography -``` ->>> let (pk, sk) = kyberKeyPair() ->>> let (ct, ss1) = kyberEncapsulate(pk) ->>> let ss2 = kyberDecapsulate(sk, ct) ->>> show(ss1 == ss2) -true ->>> show(sha256String("hello")) -2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 -``` - ---- - -## 11. P2P Technical Details -- Protocol: JSON lines over TLS. -- Certificates auto‑generated (`chatlang_cert.pem`, `chatlang_key.pem`). -- P2P port: 19000 (configurable via `--p2p-port`). -- Contact server port: configurable (e.g., 9000). -- External IP can be set manually or obtained via `getPublicIP()`. - ---- - -## 12. Known Limitations -- `let ... in` is not supported; use blocks `{ ... }`. -- Some built‑in functions may panic on wrong argument types (but most handle errors gracefully). - ---- - -## 13. Conclusion -This documentation accurately reflects the current implementation of ChatLang 3.1. All examples have been tested and work. For full chat usage, follow section 10.13. Future updates will add more features and improve error handling. diff --git a/lang_example/compiler.gleam b/lang_example/compiler.gleam deleted file mode 100644 index e1804c7..0000000 --- a/lang_example/compiler.gleam +++ /dev/null @@ -1,7 +0,0 @@ -import gleam/io - -pub fn main() { - io.println("computing...") -} - - From 1a38a12120825e9b8be34948d71a975ba13892ef Mon Sep 17 00:00:00 2001 From: one-of-all Date: Sun, 19 Jul 2026 11:18:16 +0300 Subject: [PATCH 06/18] Add files via upload --- Cargo.toml | 1 + doc.md | 492 ++++++------ exmpl/advanced.plic | 14 +- exmpl/basic.plic | 22 +- exmpl/chat.plic | 6 +- exmpl/collections.plic | 86 +-- exmpl/concurrency.plic | 16 +- exmpl/control.plic | 18 +- hl/vim/ftdetect/plic.vim | 1 + hl/vim/ftdetect/plic_header.vim | 1 + hl/vim/syntax/plic.vim | 47 ++ hl/vsc/plic.tmLanguage.json | 213 +++++ src/builtins.rs | 9 + src/eval.rs | 40 +- src/main.rs | 72 +- src/p2p.rs | 1 - src/parser.rs | 94 +-- src/server.rs | 4 +- src/tests/integration.rs | 1279 +++++++++++++++++++++++++++++-- src/types.rs | 2 +- 20 files changed, 1936 insertions(+), 482 deletions(-) create mode 100644 hl/vim/ftdetect/plic.vim create mode 100644 hl/vim/ftdetect/plic_header.vim create mode 100644 hl/vim/syntax/plic.vim create mode 100644 hl/vsc/plic.tmLanguage.json diff --git a/Cargo.toml b/Cargo.toml index 0a0bf50..4dbcebc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ thiserror = "1.0" aes-gcm = "0.10" codespan = "0.11" codespan-reporting = "0.11" +ctrlc = "3.4" [[bin]] name = "plic" diff --git a/doc.md b/doc.md index 33525bd..ce5b2ff 100644 --- a/doc.md +++ b/doc.md @@ -1,166 +1,166 @@ -# ChatLang 3.2 Documentation - -## Introduction -ChatLang is a dynamic programming language with a functional core and built‑in P2P chat support. It combines functional, imperative, and object‑oriented paradigms. - -This version supports: -- **Currying** – all functions are curried. -- **Num type** – unified numeric type (Int and Float). -- **Indentation‑based blocks** – `:` and indentation instead of `{ }`. -- **List comprehensions** – `[expr for var in iterable if condition]`. -- **Loop** – `loop { ... }` and `break` with optional value. -- **Chat** – client‑server contacts, local chats, control messages for synchronisation. -- **Mentions** – `@uid` in messages delivers a private copy. -- **Error reporting** – file, line, column, snippet. -- **F‑strings** – `f"text {expr} text"` with highlighting in REPL. -- **`del`** – deletes a variable. -- **`load`** – imports files with caching. -- **Panic safety** – no unwraps; errors are propagated. - -All features described below are implemented and tested. +# PLIC (ChatLang) 3.2 Документация + +## Введение +PLIC — это динамический язык программирования с функциональным ядром и встроенной поддержкой P2P-чата. Он сочетает функциональную, императивную и объектно-ориентированную парадигмы. + +Версия 3.2 поддерживает: +- **Каррирование** – все функции каррированы. +- **Тип Num** – объединяет Int и Float. +- **Блоки на основе отступов** – `:` и отступы вместо `{ }`. +- **Генераторы списков** – `[expr for var in iterable if condition]`. +- **Цикл** – `loop { ... }` и `break` с необязательным значением. +- **Чат** – клиент-серверные контакты, локальные чаты, управляющие сообщения для синхронизации. +- **Упоминания** – `@uid` в сообщениях доставляет личную копию. +- **Вывод ошибок** – файл, строка, столбец, фрагмент кода. +- **F-строки** – `f"текст {выражение} текст"` с подсветкой в REPL. +- **`del`** – удаление переменной. +- **`load`** – импорт файлов. +- **Устойчивость к панике** – нет `unwrap`; ошибки обрабатываются. + +Все описанные функции реализованы и протестированы. --- ## 1. REPL -Launch with `cargo run` or `chatlang`. +Запуск: `cargo run` или `plic`. -- Enter single‑line expressions; results are **not** printed automatically. Use `show(expr)` to display a value (it prints to stdout and returns `()`). -- For multi‑line blocks, use a colon followed by indented lines. To end the block, enter an empty line or type `end` on a new line. The block returns the value of the last expression. -- Built‑in functions for controlling the REPL: - - `exit()` – terminates the interpreter. - - `load(filename)` – loads and executes a ChatLang script in the current environment. Definitions persist. -- Errors are printed with location and code snippet. +- Ввод однострочных выражений; результаты **не выводятся автоматически**. Используйте `show $ expr` для печати значения (она выводит на экран и возвращает `()`). +- Для многострочных блоков используйте двоеточие, затем строки с увеличенным отступом. Завершите блок пустой строкой или словом `end`. Блок возвращает значение последнего выражения. +- Встроенные функции управления REPL: + - `exit()` – завершает интерпретатор. + - `load(filename)` – загружает и выполняет скрипт; определения сохраняются. +- Ошибки выводятся с позицией и фрагментом кода. -Example: +Пример: ``` >>> let greet name = "Hello, " ++ name ++ "!" ->>> show(greet "World") +>>> show $ greet "World" Hello, World! >>> let square x = x * x: -... show(square 5) +... show $ square 5 25 ``` --- -## 2. Lexical Elements and Syntax +## 2. Лексика и синтаксис -### Comments -- Single‑line: `# text` -- Multi‑line: `#- text -#` (nested supported) +### Комментарии +- Однострочные: `# текст` +- Многострочные: `#- текст -#` (вложенные поддерживаются) -### Identifiers -- Letters, digits, `_` (starting with a letter or `_`). +### Идентификаторы +- Буквы, цифры, `_` (начинаются с буквы или `_`). -### Literals -- Integers: `42`, `-7` -- Floats: `3.14`, `-0.5`, `1.0e10` -- Characters: `'a'`, `'\n'` -- Strings: `"Hello"`, `"line1\nline2"` -- Booleans: `true`, `false` -- Unit: `()` +### Литералы +- Целые: `42`, `-7` +- Вещественные: `3.14`, `-0.5`, `1.0e10` +- Символы: `'a'`, `'\n'` +- Строки: `"Hello"`, `"line1\nline2"` +- Булевы: `true`, `false` +- Единица: `()` - UID: `@alice`, `@everyone` -- Lists: `[1, 2, 3]`, `[0..10]` (range, excludes upper bound) -- Tuples: `(1, "ok")`, `(true, @bob)` -- Byte strings: `#B"48656C6C6F"` -- Durations: `5s`, `500ms`, `2m`, `1h` -- Map literal: `%(key => value, ...)` -- Set literal: `%[elem, ...]` -- F‑string: `f"text {expression} text"` +- Списки: `[1, 2, 3]`, `[0..10]` (диапазон, не включая верхнюю границу) +- Кортежи: `(1, "ok")`, `(true, @bob)` +- Байтовые строки: `#B"48656C6C6F"` +- Длительности: `5s`, `500ms`, `2m`, `1h` +- Map-литерал: `%(key => value, ...)` +- Set-литерал: `%[elem, ...]` +- F-строка: `f"текст {выражение} текст"` --- -## 3. Value Types -- Primitive: `Num` (unified Int/Float), `Char`, `String`, `Bool`, `Unit`, `Uid`, `ByteString`, `Pid`, `DateTime`, `Duration`. -- Composite: lists, tuples, records. -- Custom: `data` definitions, `struct` definitions, classes. -- Collections: `Map` and `Set`. +## 3. Типы значений +- Примитивные: `Num` (Int/Float), `Char`, `String`, `Bool`, `Unit`, `Uid`, `ByteString`, `Pid`, `DateTime`, `Duration`. +- Составные: списки, кортежи, записи. +- Пользовательские: `data`, `struct`, классы. +- Коллекции: `Map` и `Set`. --- -## 4. Expressions +## 4. Выражения -### 4.1. Basics -- Literals, variables. -- Function application is curried and left‑associative: `f a b c` is `(((f a) b) c)`. -- Lambda: `lambda x y -> x + y` or `\ x -> x * 2`. -- Indexing: `expr[index]` works on lists, strings, byte strings, tuples, maps (returns value or `Unit`), sets (returns `Bool`). +### 4.1. Основы +- Литералы, переменные. +- Применение функции каррировано и левоассоциативно: `f a b c` = `(((f a) b) c)`. +- Лямбда: `lambda x y -> x + y` или `\ x -> x * 2`. +- Индексация: `expr[index]` работает со списками, строками, байтовыми строками, кортежами, map (возвращает значение или `Unit`), set (возвращает `Bool`). -### 4.2. Conditional +### 4.2. Условный оператор ``` -if condition then expr1 else expr2 +if условие then выражение1 else выражение2 ``` -Single‑line only. +Только однострочный. -### 4.3. Pattern Matching +### 4.3. Сопоставление с образцом ``` -case expr of - pattern1 -> expr1 - pattern2 -> expr2 - _ -> default +case выражение of + образец1 -> выражение1 + образец2 -> выражение2 + _ -> по умолчанию ``` -One‑line variant with semicolons: +Однострочный вариант с точками с запятой: ``` -case expr of pattern1 -> expr1; pattern2 -> expr2; _ -> default +case выражение of образец1 -> выражение1; образец2 -> выражение2; _ -> по умолчанию ``` -### 4.4. Local Definitions -- `let x = 5` – defines a new variable (error if already defined). -- `let f x y = x + y` – function definition (curried). -- Type annotations: `let N :: Num = 5`, `let add :: Num -> Num -> Num = lambda a b -> a + b`. -- Blocks use indentation after `:`. +### 4.4. Локальные определения +- `let x = 5` – определяет новую переменную (ошибка, если уже определена). +- `let f x y = x + y` – определение функции (каррировано). +- Аннотации типов: `let N :: Num = 5`, `let add :: Num -> Num -> Num = lambda a b -> a + b`. +- Блоки используют отступы после `:`. -### 4.5. Operators (precedence, high to low) -1. `not` (unary) -2. `*`, `/`, `%` (left associative) -3. `+`, `-` (left associative) -4. `++` (string/list concatenation, right associative) -5. `:` (cons, right associative) -6. `in`, `not in` (membership) +### 4.5. Операторы (приоритет, от высокого к низкому) +1. `not` (унарный) +2. `*`, `/`, `%` (левоассоциативные) +3. `+`, `-` (левоассоциативные) +4. `++` (конкатенация строк/списков, правоассоциативная) +5. `:` (cons, правоассоциативная) +6. `in`, `not in` (проверка принадлежности) 7. `==`, `!=`, `<`, `>`, `<=`, `>=` -8. `and`, `or` (short‑circuit) -9. `|>` (pipe, left associative) -Operator `$` is supported (low‑precedence application, right‑associative). +8. `and`, `or` (короткое замыкание) +9. `|>` (pipe, левоассоциативный) +Оператор `$` поддерживается (низкоприоритетное применение, правоассоциативный). -### 4.6. Loops -- `for x in collection: body` – iterates over lists, sets, maps (yields key‑value tuples for maps). -- `while condition: body` -- `loop { body }` – infinite loop; use `break` with optional value to exit. +### 4.6. Циклы +- `for x in коллекция: тело` – итерация по спискам, set, map (для map – пары ключ-значение). +- `while условие: тело` +- `loop { тело }` – бесконечный цикл; используйте `break` с необязательным значением для выхода. -Example: +Пример: ``` >>> let x = 0 >>> loop { x = x + 1; if x == 5 then break x else () } 5 ``` -### 4.7. Error Handling -- `error "message"` – throws an error. -- `try expr` – catches error (if any, passes it on). -- `try expr catch pattern -> handler` – handles error. +### 4.7. Обработка ошибок +- `error "сообщение"` – выброс ошибки. +- `try выражение` – перехват ошибки (если есть, передаёт дальше). +- `try выражение catch образец -> обработчик` – обработка ошибки. -Example: +Пример: ``` try error "oops" catch e -> "caught: " ++ e ``` -### 4.8. F‑strings -Syntax: `f"text {expression} text"`. Inside braces, any ChatLang expression can appear; it is evaluated and converted to a string via `show`. Double braces `{{` and `}}` are used to insert literal `{` and `}` characters. +### 4.8. F-строки +Синтаксис: `f"текст {выражение} текст"`. Внутри фигурных скобок может быть любое выражение; оно вычисляется и преобразуется в строку через `show`. Двойные скобки `{{` и `}}` используются для вставки литеральных `{` и `}`. -Example: +Пример: ``` >>> let x = 5 >>> f"x = {x}, x*2 = {x * 2}" "x = 5, x*2 = 10" ``` -### 4.9. List Comprehensions +### 4.9. Генераторы списков ``` [expr for var in iterable if condition] ``` -Can have multiple `for` and `if` clauses. +Может содержать несколько `for` и `if`. -Example: +Пример: ``` >>> [x * 2 for x in [1,2,3] if x > 1] [4, 6] @@ -168,68 +168,68 @@ Example: --- -## 5. Structs and Algebraic Data Types +## 5. Структуры и алгебраические типы данных ``` struct Person = (name = String, age = Num) data Result a = Ok a | Err String ``` -Struct construction: `Person(name = "Alice", age = 30)`. Field access via `.` (e.g., `person.name`). Update via record update syntax (not directly; use functions or manual reconstruction). +Создание структуры: `Person(name = "Alice", age = 30)`. Доступ к полям через `.` (например, `person.name`). Обновление записи: `person { age = 31 }` (возвращает новую запись). --- -## 6. Classes (OOP) +## 6. Классы (ООП) ``` class Counter = (val = Num; inc(self) = self { val = self.val + 1 }; get(self) = self.val) ``` -- Constructor: `new Counter(0)` -- Inheritance: `class AdvancedCounter extends Counter = (...)` (with parentheses) -- Method call: `counter.inc()` +- Конструктор: `new Counter(0)` +- Наследование: `class AdvancedCounter extends Counter = (...)` (с круглыми скобками) +- Вызов метода: `counter.inc()` --- -## 7. Unions (ADT) +## 7. Union (ADT) ``` data Option a = None | Some a ``` -Constructors: `Some 42`, `None`. Pattern matching works. +Конструкторы: `Some 42`, `None`. Сопоставление с образцом работает. --- -## 8. Map and Set +## 8. Map и Set ### Map -- Create: `%(key => value, ...)` -- Functions: `mapGet`, `mapSet`, `mapRemove`, `mapKeys`, `mapValues`, `mapEntries`, `mapContains`, `mapSize`, `mapFilter`, `mapMerge` -- Indexing: `map[key]` returns value or `Unit` +- Создание: `%(key => value, ...)` +- Функции: `mapGet`, `mapSet`, `mapRemove`, `mapKeys`, `mapValues`, `mapEntries`, `mapContains`, `mapSize`, `mapFilter`, `mapMerge` +- Индексация: `map[key]` возвращает значение или `Unit` ### Set -- Create: `%[elem, ...]` -- Functions: `setAdd`, `setRemove`, `setContains`, `setUnion`, `setIntersection`, `setDifference`, `setSize`, `setFilter`, `setMap` -- Indexing: `set[elem]` returns `Bool` +- Создание: `%[elem, ...]` +- Функции: `setAdd`, `setRemove`, `setContains`, `setUnion`, `setIntersection`, `setDifference`, `setSize`, `setFilter`, `setMap` +- Индексация: `set[elem]` возвращает `Bool` --- -## 9. Built‑in Functions +## 9. Встроенные функции -### 9.1. Mathematics +### 9.1. Математика - `sqrt`, `sin`, `cos`, `tan`, `asin`, `acos`, `atan :: Num -> Num` - `toFloat :: Num -> Num`, `toInt :: Num -> Num` -### 9.2. Conversions and Type Introspection -- `show :: a -> ()` – prints the value to stdout and returns `()`. +### 9.2. Преобразования и интроспекция +- `show :: a -> ()` – печатает значение и возвращает `()`. - `parseInt :: String -> Num`, `parseFloat :: String -> Num` - `chr :: Num -> Char`, `ord :: Char -> Num` - `typeof :: a -> String` -### 9.3. List, String, ByteString, Map, Set +### 9.3. Списки, строки, байтовые строки, Map, Set - `null :: [a] -> Bool` -- `length :: collection -> Num` (works on List, String, ByteString, Map, Set, Tuple) +- `length :: collection -> Num` (работает со списками, строками, байтовыми строками, Map, Set, кортежами) - `map :: (a -> b) -> [a] -> [b]` - `filter :: (a -> Bool) -> [a] -> [a]` - `foldl :: (b -> a -> b) -> b -> [a] -> b` @@ -240,7 +240,7 @@ Constructors: `Some 42`, `None`. Pattern matching works. - `all :: (a -> Bool) -> [a] -> Bool` - `any :: (a -> Bool) -> [a] -> Bool` - `find :: (a -> Bool) -> [a] -> Maybe a` -- `sort :: [a] -> [a]` (by string representation) +- `sort :: [a] -> [a]` (по строковому представлению) - `sortBy :: (a -> a -> Num) -> [a] -> [a]` - `sum :: [Num] -> Num` - `concat :: [[a]] -> [a]` @@ -251,7 +251,7 @@ Constructors: `Some 42`, `None`. Pattern matching works. - `indexOf :: [a] -> a -> Maybe Num` - `lastIndexOf :: [a] -> a -> Maybe Num` -### 9.4. String Functions +### 9.4. Строковые функции - `split :: String -> String -> [String]` - `join :: String -> [String] -> String` - `startsWith :: String -> String -> Bool` @@ -265,19 +265,19 @@ Constructors: `Some 42`, `None`. Pattern matching works. - `encodeJson :: JsonValue -> String` - `lookup :: String -> JsonValue -> Maybe JsonValue` -### 9.6. Time +### 9.6. Время - `formatTime :: String -> DateTime -> String` - `parseTime :: String -> String -> DateTime` - `addDuration :: DateTime -> Duration -> DateTime` - `diffDuration :: DateTime -> DateTime -> Duration` - `now :: DateTime` -### 9.7. ByteString +### 9.7. Байтовые строки - `packBytes :: [Num] -> ByteString` - `unpackBytes :: ByteString -> [Num]` -### 9.8. I/O and Files -- `putStrLn :: String -> ()` – prints a raw string. +### 9.8. Ввод-вывод и файлы +- `putStrLn :: String -> ()` – печатает строку. - `getLine :: String` - `getArgs :: [String]` - `readFile :: String -> String` @@ -294,11 +294,11 @@ Constructors: `Some 42`, `None`. Pattern matching works. - `filePermissions :: String -> Num` - `setFilePermissions :: String -> Num -> ()` -### 9.9. Network +### 9.9. Сеть - `fetch :: String -> FetchResult` - `fetchOpts :: FetchOptions -> FetchResult` -### 9.10. Chat and Contacts +### 9.10. Чат и контакты - `login :: Uid -> ()` - `logout :: () -> ()` - `deleteUser :: Uid -> ()` @@ -307,9 +307,9 @@ Constructors: `Some 42`, `None`. Pattern matching works. - `removeMember :: Uid -> String -> ()` - `deleteChat :: String -> ()` - `open :: String -> ()` -- `send :: Uid -> String -> Bool` (private message) +- `send :: Uid -> String -> Bool` (личное сообщение) - `sendFile :: Uid -> String -> Bool` -- `sendChat :: String -> String -> Bool` – sends to chat, with mention support. +- `sendChat :: String -> String -> Bool` – отправка в чат с поддержкой упоминаний. - `sendFileToChat :: String -> String -> Bool` - `inbox :: () -> [ChatMsg]` - `history :: String -> [ChatMsg]` @@ -325,7 +325,7 @@ Constructors: `Some 42`, `None`. Pattern matching works. - `addContact :: Uid -> String -> ()` - `removeContact :: Uid -> ()` -### 9.11. Processes +### 9.11. Процессы - `spawn :: (() -> ()) -> Pid` - `procSelf :: Pid` - `procSend :: Pid -> a -> ()` @@ -340,287 +340,267 @@ Constructors: `Some 42`, `None`. Pattern matching works. - `Just :: a -> Maybe a` - `maybe :: (a -> b) -> b -> Maybe a -> b` -### 9.13. Map & Set (additional) +### 9.13. Map и Set (дополнительно) - `mapGet`, `mapSet`, `mapRemove`, `mapKeys`, `mapValues`, `mapEntries`, `mapContains`, `mapSize`, `mapFilter`, `mapMerge` - `setAdd`, `setRemove`, `setContains`, `setUnion`, `setIntersection`, `setDifference`, `setSize`, `setFilter`, `setMap` - `listToSet :: [a] -> Set` - `mapToList :: Map -> [(key, value)]` -### 9.14. Cryptography +### 9.14. Криптография - `sha256 :: ByteString -> ByteString` - `sha256String :: String -> String` -- `kyberKeyPair :: () -> (PublicKey, SecretKey)` (both as ByteString) +- `kyberKeyPair :: () -> (PublicKey, SecretKey)` (оба как ByteString) - `kyberEncapsulate :: PublicKey -> (Ciphertext, SharedSecret)` - `kyberDecapsulate :: SecretKey -> Ciphertext -> SharedSecret` -### 9.15. Variables -- `del :: String -> ()` – deletes a variable. +### 9.15. Управление переменными +- `del :: String -> ()` – удаляет переменную. +- `load :: String -> ()` – загружает и выполняет файл. + +### 9.16. P2P +- `p2pPort :: () -> Num` – возвращает текущий P2P-порт. --- -## 10. Examples +## 10. Примеры -### 10.1. Arithmetic and Functions +### 10.1. Арифметика и функции ``` ->>> show(1 + 2 * 3) +>>> show $ 1 + 2 * 3 7 >>> let sq x = x * x ->>> show(sq 5) +>>> show $ sq 5 25 ->>> show((lambda x -> x + 1) 5) +>>> show $ (lambda x -> x + 1) 5 6 ->>> show(-5) +>>> show $ -5 -5 ->>> show(5 - -2) +>>> show $ 5 - -2 7 ``` -### 10.2. Variables and Assignments +### 10.2. Переменные и присваивание ``` >>> let x = 5 ->>> show(x) +>>> show x 5 >>> let x = 6 error: variable 'x' already defined >>> x = 10 ->>> show(x) +>>> show x 10 ``` -### 10.3. Characters and Strings +### 10.3. Символы и строки ``` ->>> show('a') +>>> show 'a' a ->>> show('\n') +>>> show '\n' >>> let msg = "Hello" ->>> show(msg) +>>> show msg Hello ->>> show(msg ++ " world") +>>> show $ msg ++ " world" Hello world ``` -### 10.4. Type Annotations and typeof +### 10.4. Аннотации типов и typeof ``` >>> let N :: Num = 5 ->>> show(typeof N) +>>> show $ typeof N Num >>> let add :: Num -> Num -> Num = lambda a b -> a + b ->>> show(typeof add) +>>> show $ typeof add Closure >>> let msg :: String = "hello" ->>> show(typeof msg) +>>> show $ typeof msg String >>> N = 'a' # type mismatch error: Type mismatch: expected 'Num', got 'Char' ``` -### 10.5. Conditionals +### 10.5. Условный оператор ``` ->>> show(if 5 > 3 then "yes" else "no") +>>> show $ if 5 > 3 then "yes" else "no" yes ``` -### 10.6. Lists, Strings, Tuples +### 10.6. Списки, строки, кортежи ``` ->>> show([1, 2, 3] |> map(lambda x -> x * 2)) +>>> show $ [1, 2, 3] |> map(lambda x -> x * 2) [2, 4, 6] ->>> show("Hello, " ++ "world!") +>>> show $ "Hello, " ++ "world!" Hello, world! ->>> show(length("abc")) +>>> show $ length "abc" 3 ->>> show([0..5]) +>>> show $ [0..5] [0, 1, 2, 3, 4] ->>> show((1, "two", 3.0)[1]) +>>> show $ (1, "two", 3.0)[1] two ->>> show(length((1,2,3))) +>>> show $ length (1,2,3) 3 ``` -### 10.7. Pattern Matching +### 10.7. Сопоставление с образцом ``` ->>> show(case 2 of 1 -> "one"; 2 -> "two"; _ -> "other") +>>> show $ case 2 of 1 -> "one"; 2 -> "two"; _ -> "other" two ``` ### 10.8. JSON ``` ->>> let data = parseJson("[1, 2, 3]") ->>> show(data) +>>> let data = parseJson "[1, 2, 3]" +>>> show data [1, 2, 3] ->>> show(encodeJson(data)) +>>> show $ encodeJson data [1,2,3] ``` -### 10.9. Files and I/O +### 10.9. Файлы и ввод-вывод ``` ->>> writeFile("test.txt", "Hello") +>>> writeFile "test.txt" "Hello" () ->>> show(readFile("test.txt")) +>>> show $ readFile "test.txt" Hello ->>> show(fileExists("test.txt")) +>>> show $ fileExists "test.txt" true ->>> show(listDir(".")) +>>> show $ listDir "." ["test.txt", ...] ``` -### 10.10. Map and Set +### 10.10. Map и Set ``` >>> let m = %(1 => "one", 2 => "two") ->>> show(m[1]) +>>> show $ m[1] one ->>> show(mapSet(m, 3, "three")) +>>> show $ mapSet m 3 "three" %(1: one, 2: two, 3: three) ->>> show(mapKeys(m2)) +>>> show $ mapKeys m [1, 2, 3] >>> let s = %[1,2,3] ->>> show(setAdd(s, 4)) +>>> show $ setAdd s 4 %[1,2,3,4] ->>> show(s[2]) +>>> show $ s[2] true ->>> show(setContains(s, 5)) +>>> show $ setContains s 5 false ``` -### 10.11. Classes +### 10.11. Классы ``` >>> class Counter = (val = Num; inc(self) = self { val = self.val + 1 }; get(self) = self.val) >>> let c = new Counter(0) ->>> show(c.inc().get()) +>>> show $ c.inc().get() 1 ``` -### 10.12. Structures +### 10.12. Структуры ``` >>> struct Person = (name = String, age = Num) >>> let p = Person(name = "Alice", age = 30) ->>> show(p.name) +>>> show $ p.name Alice ``` -### 10.13. Chat (Complete Scenario with External IP and Password) +### 10.13. Чат (полный сценарий с внешним IP и паролем) -**Set external IP and start server with password:** +**Установка внешнего IP и запуск сервера с паролем:** ``` ->>> setExternalIP("203.0.113.5") +>>> setExternalIP "203.0.113.5" () ->>> serverStart("0.0.0.0:9000", "secret") +>>> serverStart "0.0.0.0:9000" "secret" () ``` -**Alice:** +**Алиса:** ``` ->>> login(@alice) +>>> login @alice () ->>> connect("127.0.0.1:9000", @alice, "secret") +>>> connect "127.0.0.1:9000" @alice "secret" 1 ->>> newChat("general", [@bob, @alice]) +>>> newChat "general" [@bob, @alice] general ->>> open("general") +>>> open "general" () ->>> sendChat("general", "Hello everyone!") +>>> sendChat "general" "Hello everyone!" true ->>> send(@bob, "Hello Bob!") +>>> send @bob "Hello Bob!" true ``` -**Bob (another instance):** +**Боб (другой экземпляр):** ``` ->>> login(@bob) +>>> login @bob () ->>> connect("127.0.0.1:9000", @bob, "secret") +>>> connect "127.0.0.1:9000" @bob "secret" 1 ->>> show(inbox()) +>>> show inbox [[Message from @alice in general: "Hello everyone!"], [Message from @alice: "Hello Bob!"]] ->>> show(history("general")) +>>> show $ history "general" [[Message from @alice in general: "Hello everyone!"]] ``` -**File transfer:** +**Передача файла:** ``` ->>> writeFile("report.txt", "Sales data") +>>> writeFile "report.txt" "Sales data" () ->>> sendFileToChat("general", "report.txt") +>>> sendFileToChat "general" "report.txt" true ->>> show(downloads()) +>>> show downloads [[FileTransfer from @alice: report.txt]] ->>> saveFile(0, "received_report.txt") +>>> saveFile 0 "received_report.txt" true ``` -### 10.14. Processes +### 10.14. Процессы ``` ->>> let p = spawn(lambda () -> (procRecv() |> show)) ->>> procSend(p, "Hi") +>>> let p = spawn lambda () -> (procRecv |> show) +>>> procSend p "Hi" () ->>> sleep(1s) +>>> sleep 1s () ``` -### 10.15. Cryptography +### 10.15. Криптография ``` >>> let (pk, sk) = kyberKeyPair() ->>> let (ct, ss1) = kyberEncapsulate(pk) ->>> let ss2 = kyberDecapsulate(sk, ct) ->>> show(ss1 == ss2) +>>> let (ct, ss1) = kyberEncapsulate pk +>>> let ss2 = kyberDecapsulate sk ct +>>> show $ ss1 == ss2 true ->>> show(sha256String("hello")) +>>> show $ sha256String "hello" 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 ``` --- -## 11. P2P Technical Details -- Protocol: JSON lines over TLS. -- Certificates auto‑generated (`chatlang_cert.pem`, `chatlang_key.pem`). -- P2P port: 19000 (configurable via `--p2p-port`). -- Contact server port: configurable (e.g., 9000). -- External IP can be set manually or obtained via `getPublicIP()`. +## 11. Технические детали P2P +- Протокол: JSON-строки поверх TLS. +- Сертификаты генерируются автоматически (`chatlang_cert.pem`, `chatlang_key.pem`). +- P2P-порт: 19000 (можно изменить с помощью `--p2p-port`). +- Порт сервера контактов: настраивается (например, 9000). +- Внешний IP можно задать вручную или получить через `getPublicIP()`. --- -## 12. Known Limitations -- `let ... in` is not supported; use blocks with indentation. -- Some built‑in functions may panic on wrong argument types (but most handle errors gracefully). +## 12. Известные ограничения +- `let ... in` не поддерживается; используйте блоки с отступами. +- Некоторые встроенные функции могут паниковать при неверных типах аргументов (большинство обрабатывают ошибки). --- -## 13. Vim and VSCode Syntax Highlighting +## 13. Подсветка синтаксиса для Vim и VSCode ### Vim -Place the following in `~/.vim/syntax/chatlang.vim`: -```vim -syn keyword chatlangKeyword let if then else case of lambda data struct try catch error for while in and or not class extends new loop break -syn keyword chatlangType Num Char String Bool Unit Uid ByteString List Tuple Record Pid DateTime Duration Json Maybe Either ChatMsg FileInfo FileTransfer FetchOptions FetchResult Map Set ClassInstance -syn match chatlangComment "#.*$" -syn region chatlangComment start="#-" end="-#" contains=chatlangComment -syn region chatlangString start=+"+ end=+"+ skip=+\\"+ -syn region chatlangFString start=+f"+ end=+"+ contains=chatlangExpr -syn region chatlangExpr start=+{+ end=+}+ contained -syn match chatlangNumber "\<[0-9.]\+\>" -highlight link chatlangKeyword Keyword -highlight link chatlangType Type -highlight link chatlangComment Comment -highlight link chatlangString String -highlight link chatlangFString String -highlight link chatlangNumber Number -``` - -Add to `.vimrc`: +Поместите содержимое `hl/vim/syntax/plic.vim` в `~/.vim/syntax/plic.vim`, а `hl/vim/ftdetect/plic.vim` – в `~/.vim/ftdetect/plic.vim`. Добавьте в `.vimrc`: ```vim -au BufRead,BufNewFile *.cl set filetype=chatlang +au BufRead,BufNewFile *.plic set filetype=plic ``` ### VSCode -Create a language extension or use the following in `settings.json`: +Скопируйте папку `hl/vsc` в `.vscode` вашего проекта или используйте расширение. В `settings.json` добавьте: ```json "files.associations": { - "*.cl": "chatlang" + "*.plic": "plic" } ``` -Then define a custom grammar in `chatlang.tmLanguage.json` (see online resources). - ---- - -## 14. Conclusion -This documentation accurately reflects the current implementation of ChatLang 3.2. All examples have been tested and work. For full chat usage, follow section 10.13. Future updates will add more features and improve error handling. diff --git a/exmpl/advanced.plic b/exmpl/advanced.plic index b93073a..53aea00 100644 --- a/exmpl/advanced.plic +++ b/exmpl/advanced.plic @@ -5,27 +5,27 @@ class Counter = ( get(self) = self.val ) let c = new Counter(0) -show(c.inc().inc().get()) # 2 +show $ c.inc().inc().get() # 2 # Inheritance class AdvancedCounter extends Counter = ( dec(self) = self { val = self.val - 1 } ) let ac = new AdvancedCounter(10) -show(ac.dec().get()) # 9 +show $ ac.dec().get() # 9 # Algebraic Data Types (union) data Option a = None | Some a let opt = Some 42 case opt of - Some x -> "value: " ++ show(x) + Some x -> "value: " ++ show $ x None -> "nothing" # returns "value: 42" # Structs (already shown) struct Point = (x = Num, y = Num) let p = Point(x = 3, y = 4) -show(p.x + p.y) # 7 +show $ p.x + p.y # 7 # Exceptions try @@ -35,14 +35,14 @@ catch e -> "caught: " ++ e # List comprehensions let nums = [1, 2, 3, 4, 5] -show([x * 2 for x in nums if x > 2]) # [6, 8, 10] +show $ [x * 2 for x in nums if x > 2] # [6, 8, 10] # Multiple generators -show([(x, y) for x in [1,2] for y in [a,b]]) # [(1,'a'), (1,'b'), (2,'a'), (2,'b')] +show $ [(x, y) for x in [1,2] for y in [a,b]] # [(1,'a'), (1,'b'), (2,'a'), (2,'b')] # F‑strings let name = "Alice" let age = 30 -show(f"Name: {name}, Age: {age}, Next year: {age + 1}") +show $ f"Name: {name}, Age: {age}, Next year: {age + 1}" # "Name: Alice, Age: 30, Next year: 31" # Del (delete variable) diff --git a/exmpl/basic.plic b/exmpl/basic.plic index 7f7a540..ec9cedc 100644 --- a/exmpl/basic.plic +++ b/exmpl/basic.plic @@ -1,31 +1,31 @@ # Simple arithmetic -show(1 + 2 * 3) # 7 -show((10 - 5) / 2) # 2.5 +show $ 1 + 2 * 3 # 7 +show $ (10 - 5) / 2 # 2.5 # Variables and assignments let x = 10 -show(x) # 10 +show $ x # 10 x = x + 5 -show(x) # 15 +show $ x # 15 # Functions (curried) let square x = x * x -show(square 4) # 16 +show $ square 4 # 16 let add a b = a + b -show(add 3 4) # 7 +show $ add 3 4 # 7 # Lambda let inc = lambda x -> x + 1 -show(inc 5) # 6 +show $ inc 5 # 6 # Type annotations let N :: Num = 42 -show(typeof N) # Num +show typeof N # Num let greet :: String = "Hello" -show(greet) # Hello +show $ greet # Hello # Numeric conversion let pi = 3.14159 -show(toInt pi) # 3 -show(toFloat 5) # 5.0 +show $ toInt pi # 3 +show $ toFloat 5 # 5.0 diff --git a/exmpl/chat.plic b/exmpl/chat.plic index c5d61e4..c70071b 100644 --- a/exmpl/chat.plic +++ b/exmpl/chat.plic @@ -23,11 +23,11 @@ writeFile("report.txt", "Sales data for Q1") sendFileToChat("general", "report.txt") # Check inbox and history -show(inbox()) # lists all private messages for Alice -show(history("general")) # shows chat history +show $ inbox() # lists all private messages for Alice +show $ history("general") # shows chat history # List downloads and save file -show(downloads()) # shows received file transfers +show $ downloads() # shows received file transfers saveFile(0, "received_report.txt") # Stop server diff --git a/exmpl/collections.plic b/exmpl/collections.plic index e248dd3..1861eb3 100644 --- a/exmpl/collections.plic +++ b/exmpl/collections.plic @@ -1,68 +1,68 @@ # Lists let nums = [1, 2, 3, 4, 5] -show(length nums) # 5 -show(null []) # true -show(nums[0]) # 1 +show $ length nums # 5 +show $ null [] # true +show $ nums[0] # 1 # List operations -show(map(lambda x -> x * 2, nums)) # [2, 4, 6, 8, 10] -show(filter(lambda x -> x > 2, nums)) # [3, 4, 5] -show(foldl(lambda acc x -> acc + x, nums, 0)) # 15 -show(take 3 nums) # [1, 2, 3] -show(drop 2 nums) # [3, 4, 5] -show(reverse nums) # [5, 4, 3, 2, 1] -show(concat [[1,2], [3,4]]) # [1, 2, 3, 4] -show(zip [1,2] [a,b]) # [(1, 'a'), (2, 'b')] +show $ map(lambda x -> x * 2, nums) # [2, 4, 6, 8, 10] +show $ filter(lambda x -> x > 2, nums) # [3, 4, 5] +show $ foldl(lambda acc x -> acc + x, nums, 0) # 15 +show $ take 3 nums # [1, 2, 3] +show $ drop 2 nums # [3, 4, 5] +show $ reverse nums # [5, 4, 3, 2, 1] +show $ concat [[1,2], [3,4]] # [1, 2, 3, 4] +show $ zip [1,2] [a,b] # [(1, 'a'), (2, 'b')] # Strings let hello = "Hello" let world = "World" -show(hello ++ " " ++ world) # "Hello World" -show(length hello) # 5 -show(hello[1]) # 'e' -show(split " " "Hello World") # ["Hello", "World"] -show(join ", " ["one", "two"]) # "one, two" -show(startsWith "He" hello) # true -show(endsWith "lo" hello) # true -show(trim " abc ") # "abc" -show(replace "l" "L" hello) # "HeLLo" -show(substring 1 3 hello) # "ell" +show $ hello ++ " " ++ world # "Hello World" +show $ length hello # 5 +show $ hello[1] # 'e' +show $ split " " "Hello World" # ["Hello", "World"] +show $ join ", " ["one", "two"] # "one, two" +show $ startsWith "He" hello # true +show $ endsWith "lo" hello # true +show $ trim " abc " # "abc" +show $ replace "l" "L" hello # "HeLLo" +show $ substring 1 3 hello # "ell" # ByteStrings -let bytes = #B"48656C6C6F" # "Hello" -show(bytes) # #B"48656C6C6F" -show(length bytes) # 5 -show(bytes[0]) # 72 (ASCII 'H') +let bytes = #B"48656C6C6F" # "Hello" +show $ bytes # #B"48656C6C6F" +show $ length bytes # 5 +show $ bytes[0] # 72 (ASCII 'H') let packed = packBytes [72, 101, 108, 108, 111] -show(packed) # #B"48656C6C6F" -show(unpackBytes packed) # [72, 101, 108, 108, 111] +show $ packed # #B"48656C6C6F" +show $ unpackBytes packed # [72, 101, 108, 108, 111] # Tuples let tup = (1, "two", 3.0) -show(tup[1]) # "two" -show(length tup) # 3 +show $ tup[1] # "two" +show $ length tup # 3 # Records (structs) struct Person = (name = String, age = Num) let alice = Person(name = "Alice", age = 30) -show(alice.name) # "Alice" -show(alice.age) # 30 +show $ alice.name # "Alice" +show $ alice.age # 30 # Maps let m = %(1 => "one", 2 => "two") -show(mapGet m 1) # "one" +show $ mapGet m 1 # "one" let m2 = mapSet m 3 "three" -show(m2) # %{1: one, 2: two, 3: three} -show(mapKeys m2) # [1, 2, 3] -show(mapValues m2) # ["one", "two", "three"] -show(mapContains m2 2) # true -show(mapSize m2) # 3 +show $ m2 # %{1: one, 2: two, 3: three} +show $ mapKeys m2 # [1, 2, 3] +show $ mapValues m2 # ["one", "two", "three"] +show $ mapContains m2 2 # true +show $ mapSize m2 # 3 # Sets let set = %[1, 2, 3] -show(setAdd set 4) # %[1,2,3,4] -show(setRemove set 2) # %[1,3] -show(setContains set 2) # true -show(setUnion set %[3,4,5]) # %[1,2,3,4,5] -show(setSize set) # 3 -show(listToSet [1,2,2,3]) # %[1,2,3] +show $ setAdd set 4 # %[1,2,3,4] +show $ setRemove set 2 # %[1,3] +show $ setContains set 2 # true +show $ setUnion set %[3,4,5] # %[1,2,3,4,5] +show $ setSize set # 3 +show $ listToSet [1,2,2,3] # %[1,2,3] diff --git a/exmpl/concurrency.plic b/exmpl/concurrency.plic index f28c55a..16e9b22 100644 --- a/exmpl/concurrency.plic +++ b/exmpl/concurrency.plic @@ -1,7 +1,7 @@ # Spawn a process that waits for a message and prints it let p = spawn(lambda () -> { let msg = procRecv() - show("Received: " ++ show(msg)) + show $ "Received: " ++ show $ msg procExit(msg) }) @@ -10,18 +10,18 @@ procSend(p, "Hello from main!") # Wait for the process to finish and get its exit value let result = procWait(p) -show("Process exited with: " ++ show(result)) +show $ "Process exited with: " ++ show $ result # Self PID let self = procSelf() -show(self) # prints +show $ self # prints # Sleep sleep(2s) -show("Slept for 2 seconds") +show $ "Slept for 2 seconds" # After – schedule a function to run after a delay -after(1s, lambda () -> show("Delayed message")) +after(1s, lambda () -> show $ "Delayed message") # More complex process example with multiple messages let p2 = spawn(lambda () -> { @@ -30,7 +30,7 @@ let p2 = spawn(lambda () -> { if msg == "stop" then break "stopped" else - show("Process 2 received: " ++ show(msg)) + show $ "Process 2 received: " ++ show $ msg } }) procSend(p2, "first") @@ -38,7 +38,7 @@ procSend(p2, "second") sleep(500ms) procSend(p2, "stop") let exit = procWait(p2) -show("p2 exit: " ++ show(exit)) +show $ "p2 exit: " ++ show $ exit # Show that processes can run concurrently let p3 = spawn(lambda () -> { @@ -48,4 +48,4 @@ let p3 = spawn(lambda () -> { }) procSend(p3, "ack") let res = procWait(p3) -show("p3 result: " ++ show(res)) +show $ "p3 result: " ++ show $ res diff --git a/exmpl/control.plic b/exmpl/control.plic index e74fa06..944c801 100644 --- a/exmpl/control.plic +++ b/exmpl/control.plic @@ -1,31 +1,31 @@ # If‑then‑else let age = 20 if age >= 18 then - show("Adult") + show $ "Adult" else - show("Minor") + show $ "Minor" # For loop over list let sum = 0 for x in [1, 2, 3, 4]: sum = sum + x -show(sum) # 10 +show $ sum # 10 # For over set let s = %[apple, banana, cherry] for item in s: - show(item) # prints each string + show $ item # prints each string # For over map let m = %(1 => "one", 2 => "two") for pair in m: - show(pair) # prints (1, "one") etc. + show $ pair # prints (1, "one") etc. # While loop let i = 0 while i < 5: i = i + 1 -show(i) # 5 +show $ i # 5 # Infinite loop with break let counter = 0 @@ -34,15 +34,15 @@ loop { if counter == 3 then break "done" } -show(counter) # 3 (last value before break) +show $ counter # 3 (last value before break) # Pattern matching (one‑line) -show(case 2 of 1 -> "one"; 2 -> "two"; _ -> "other") # two +show $ case 2 of 1 -> "one"; 2 -> "two"; _ -> "other" # two # Pattern matching (multiline) case 10 of 0 -> "zero" - n -> "positive: " ++ show(n) + n -> "positive: " ++ show $ n # returns "positive: 10" # Tuple pattern diff --git a/hl/vim/ftdetect/plic.vim b/hl/vim/ftdetect/plic.vim new file mode 100644 index 0000000..da7e122 --- /dev/null +++ b/hl/vim/ftdetect/plic.vim @@ -0,0 +1 @@ +au BufRead,BufNewFile *.plic set filetype=plic diff --git a/hl/vim/ftdetect/plic_header.vim b/hl/vim/ftdetect/plic_header.vim new file mode 100644 index 0000000..cc5e26e --- /dev/null +++ b/hl/vim/ftdetect/plic_header.vim @@ -0,0 +1 @@ +runtime! syntax/plic.vim diff --git a/hl/vim/syntax/plic.vim b/hl/vim/syntax/plic.vim new file mode 100644 index 0000000..d94b109 --- /dev/null +++ b/hl/vim/syntax/plic.vim @@ -0,0 +1,47 @@ +if exists("b:current_syntax") + finish +endif + +" Keywords +syn keyword chKeyword let if then else case of data struct try catch error for while lambda and or not class extends new loop break load del +syn keyword chKeyword addContact removeContact newChat addMember removeMember deleteChat sendChat sendFileToChat inbox history downloads saveFile +syn keyword chKeyword serverStart serverStop connect getPublicIP setExternalIP toFloat toInt typeof + +" DataType +syn region chString start=+"+ end=+"+ skip=+\\"+ contains=@Spell +syn region chString start=+'+ end=+'+ skip=+\\'+ contains=@Spell + +" f-string +syn region chFString start=+f"+ end=+"+ skip=+\\"+ contains=chFStringExpr +syn region chFStringExpr start=+{+ end=+}+ contained contains=chKeyword,chString,chNumber,chType,chOperator,chComment + +" Byte string +syn match chByteString "#B\"[0-9A-Fa-f]*\"" + +" Numbers +syn match chNumber "\<\d\(\d\|[a-zA-Z_]\)*\>" +syn keyword chNumber true false + +" Type +syn keyword chType Num Char String Bool Unit Uid ByteString Duration List Tuple Record Pid DateTime Json Maybe Either ChatMsg FileInfo FileTransfer FetchOptions FetchResult Map Set ClassInstance + +" Operators +syn match chOperator "[-+*%/@&$|^~=<>?:]" +syn match chOperator "[{}()\[\];,.]" + +" Comments +syn match chComment "#.*$" contains=@Spell +syn region chComment start="#-" end="-#" contains=@Spell fold + +" Markup +hi def link chComment Comment +hi def link chTodo Todo +hi def link chString String +hi def link chFString String +hi def link chByteString String +hi def link chNumber Number +hi def link chType Type +hi def link chKeyword Keyword +hi def link chOperator Operator + +let b:current_syntax = "plic" diff --git a/hl/vsc/plic.tmLanguage.json b/hl/vsc/plic.tmLanguage.json new file mode 100644 index 0000000..cc747d2 --- /dev/null +++ b/hl/vsc/plic.tmLanguage.json @@ -0,0 +1,213 @@ +{ + "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", + "name": "plic", + "scopeName": "source.plic", + "fileTypes": ["cl", "plic"], + "patterns": [ + { + "include": "#comments" + }, + { + "include": "#strings" + }, + { + "include": "#numbers" + }, + { + "include": "#uid" + }, + { + "include": "#keywords" + }, + { + "include": "#types" + }, + { + "include": "#operators" + }, + { + "include": "#identifiers" + } + ], + "repository": { + "comments": { + "patterns": [ + { + "name": "comment.line.number-sign.plic", + "match": "#[^B-].*$" + }, + { + "begin": "#-", + "end": "-#", + "name": "comment.block.plic", + "patterns": [ + { + "include": "#comments" + } + ] + } + ] + }, + "strings": { + "patterns": [ + { + "name": "string.quoted.double.plic", + "begin": "\"", + "end": "\"", + "patterns": [ + { + "name": "constant.character.escape.plic", + "match": "\\\\(n|t|r|\\\\|\"|')" + } + ] + }, + { + "name": "string.quoted.single.plic", + "begin": "'", + "end": "'", + "patterns": [ + { + "name": "constant.character.escape.plic", + "match": "\\\\(n|t|r|\\\\|'|\")" + } + ] + }, + { + "name": "string.quoted.double.fstring.plic", + "begin": "f\"", + "end": "\"", + "patterns": [ + { + "include": "#fstring_expression" + }, + { + "name": "constant.character.escape.plic", + "match": "\\\\(n|t|r|\\\\|\"|')" + } + ] + }, + { + "name": "string.quoted.double.bytestring.plic", + "begin": "#B\"", + "end": "\"", + "patterns": [ + { + "name": "constant.character.hex.plic", + "match": "[0-9A-Fa-f]{2}" + } + ] + } + ] + }, + "fstring_expression": { + "patterns": [ + { + "begin": "{", + "end": "}", + "name": "meta.embedded.expression.plic", + "patterns": [ + { + "include": "$self" + } + ] + } + ] + }, + "numbers": { + "patterns": [ + { + "name": "constant.numeric.integer.plic", + "match": "\\b\\d+\\b" + }, + { + "name": "constant.numeric.float.plic", + "match": "\\b\\d+\\.\\d+\\b" + }, + { + "name": "constant.numeric.duration.plic", + "match": "\\b\\d+(\\.\\d+)?[smMh]\\b" + } + ] + }, + "uid": { + "name": "constant.other.uid.plic", + "match": "@[A-Za-z_][A-Za-z0-9_]*" + }, + "keywords": { + "patterns": [ + { + "name": "keyword.control.plic", + "match": "\\b(if|then|else|case|of|try|catch|error|for|while|lambda|and|or|not|class|extends|new|let|in|data|struct|loop|break|load|del)\\b" + }, + { + "name": "keyword.other.plic", + "match": "\\b(addContact|removeContact|newChat|addMember|removeMember|deleteChat|sendChat|sendFileToChat|inbox|history|downloads|saveFile|serverStart|serverStop|connect|getPublicIP|setExternalIP|toFloat|toInt|typeof)\\b" + } + ] + }, + "types": { + "patterns": [ + { + "name": "storage.type.plic", + "match": "\\b(Int|Float|Char|String|Bool|Unit|Uid|ByteString|Duration|List|Tuple|Record|Pid|DateTime|Json|Maybe|Either|ChatMsg|FileInfo|FileTransfer|FetchOptions|FetchResult|Map|Set|ClassInstance)\\b" + } + ] + }, + "operators": { + "patterns": [ + { + "name": "keyword.operator.arithmetic.plic", + "match": "[-+*/%]" + }, + { + "name": "keyword.operator.comparison.plic", + "match": "==|!=|<=|>=|<|>" + }, + { + "name": "keyword.operator.logical.plic", + "match": "&&|\\|\\||!" + }, + { + "name": "keyword.operator.pipe.plic", + "match": "\\|>" + }, + { + "name": "keyword.operator.dollar.plic", + "match": "\\$" + }, + { + "name": "keyword.operator.cons.plic", + "match": ":" + }, + { + "name": "keyword.operator.concat.plic", + "match": "\\+\\+" + }, + { + "name": "keyword.operator.assignment.plic", + "match": "=" + }, + { + "name": "keyword.operator.member.plic", + "match": "\\." + }, + { + "name": "keyword.operator.other.plic", + "match": "->|=>|::|;|,|\\(|\\)|\\[|\\]|\\{|\\}" + } + ] + }, + "identifiers": { + "patterns": [ + { + "name": "entity.name.function.plic", + "match": "\\b[A-Za-z_][A-Za-z0-9_]*\\b(?=\\s*\\()" + }, + { + "name": "variable.other.plic", + "match": "\\b[A-Za-z_][A-Za-z0-9_]*\\b" + } + ] + } + } +} diff --git a/src/builtins.rs b/src/builtins.rs index f32772d..34e6309 100644 --- a/src/builtins.rs +++ b/src/builtins.rs @@ -2431,6 +2431,15 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: }), ); + let state_for_p2p_port = state.clone(); + env.set( + "p2pPort".to_string(), + builtin!("p2pPort", 0, move |_args| { + let port = state_for_p2p_port.lock().unwrap().p2p_port; + Ok(Value::Num(Number::Int(port as i64))) + }), + ); + { let mut state_guard = state.lock().unwrap(); if state_guard.p2p_port == 0 { diff --git a/src/eval.rs b/src/eval.rs index 7653e33..0056840 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -6,11 +6,12 @@ use crate::error::ChatError; use crate::types::{Environment, Value, Number}; use std::collections::BTreeMap; use std::collections::BTreeSet; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, atomic::Ordering}; use std::thread; use std::sync::mpsc::{self, Sender, Receiver}; use once_cell::sync::Lazy; use std::time::Duration; +use std::sync::atomic; pub struct Process { pub sender: Sender, @@ -27,6 +28,14 @@ thread_local! { static CURRENT_RECEIVER: std::cell::RefCell>> = const { std::cell::RefCell::new(None) }; } +static INTERRUPTED: atomic::AtomicBool = atomic::AtomicBool::new(false); + +/// Set the interruption flag (called from signal handler). +pub fn set_interrupted() { + INTERRUPTED.store(true, Ordering::SeqCst); +} + +/// Evaluate an expression in the given environment and chat state. pub fn eval_expr( expr: &Expr, env: &mut Environment, @@ -172,7 +181,13 @@ pub fn eval_expr( } Ok(Value::Record(map)) } - _ => Err(ChatError::with_span("Record update on non-record", 1, *span)), + Value::ClassInstance { mut fields, class } => { + for (k, v) in updates { + fields.insert(k.clone(), eval_expr(v, env, Arc::clone(&state))?); + } + Ok(Value::ClassInstance { class, fields }) + } + _ => Err(ChatError::with_span("Record update on non-record or non-class instance", 1, *span)), } } Expr::BinOp(op, l, r, span) => { @@ -256,7 +271,6 @@ pub fn eval_expr( Expr::Index(list, idx, span) => { let coll = eval_expr(list, env, Arc::clone(&state))?; let index = eval_expr(idx, env, Arc::clone(&state))?; - // Automatic float to int conversion for indexing let index_int = match index { Value::Num(Number::Int(i)) => i, Value::Num(Number::Float(f)) => f as i64, @@ -306,6 +320,9 @@ pub fn eval_expr( match iter_val { Value::List(list) => { for item in list { + if INTERRUPTED.load(Ordering::SeqCst) { + return Err(ChatError::with_span("Execution interrupted by user", 1, *span)); + } let mut new_env = env.clone(); new_env.set(var.clone(), item); eval_expr(body, &mut new_env, Arc::clone(&state))?; @@ -314,6 +331,9 @@ pub fn eval_expr( } Value::Set(set) => { for item in set { + if INTERRUPTED.load(Ordering::SeqCst) { + return Err(ChatError::with_span("Execution interrupted by user", 1, *span)); + } let mut new_env = env.clone(); new_env.set(var.clone(), Value::String(item)); eval_expr(body, &mut new_env, Arc::clone(&state))?; @@ -322,6 +342,9 @@ pub fn eval_expr( } Value::Map(map) => { for (k, v) in map { + if INTERRUPTED.load(Ordering::SeqCst) { + return Err(ChatError::with_span("Execution interrupted by user", 1, *span)); + } let mut new_env = env.clone(); new_env.set(var.clone(), Value::Tuple(vec![Value::String(k), v])); eval_expr(body, &mut new_env, Arc::clone(&state))?; @@ -333,6 +356,9 @@ pub fn eval_expr( } Expr::While(cond, body, span) => { loop { + if INTERRUPTED.load(Ordering::SeqCst) { + return Err(ChatError::with_span("Execution interrupted by user", 1, *span)); + } let c = eval_expr(cond, env, Arc::clone(&state))?; match c { Value::Bool(true) => { @@ -344,8 +370,11 @@ pub fn eval_expr( } Ok(Value::Unit) } - Expr::Loop(body, _span) => { + Expr::Loop(body, span) => { loop { + if INTERRUPTED.load(Ordering::SeqCst) { + return Err(ChatError::with_span("Execution interrupted by user", 1, *span)); + } match eval_expr(body, env, Arc::clone(&state)) { Ok(Value::Break(Some(val))) => return Ok(*val), Ok(Value::Break(None)) => return Ok(Value::Unit), @@ -485,7 +514,6 @@ pub fn eval_expr( acc: &mut Vec, ) -> Result<(), ChatError> { if gens.is_empty() { - // Check filters for filter in filters { let cond = eval_expr(filter, env, Arc::clone(&state))?; if let Value::Bool(b) = cond { @@ -533,6 +561,7 @@ pub fn eval_expr( } } +/// Helper to extract span from expression. fn e_span(e: &Expr) -> Span { match e { Expr::Lit(_, s) => *s, @@ -846,6 +875,7 @@ pub fn match_pattern(pat: &Pattern, val: &Value) -> Option>) -> Result { match closure { Value::Closure(params, body, env) if params.is_empty() => { diff --git a/src/main.rs b/src/main.rs index 1e7ff93..c702cae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ use rustyline::highlight::Highlighter; use rustyline::hint::Hinter; use rustyline::validate::{Validator, ValidationContext, ValidationResult}; use rustyline::Helper; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, atomic::{AtomicBool, Ordering}}; use std::fs; use std::env; use std::collections::BTreeSet; @@ -20,6 +20,9 @@ use codespan::Files; use codespan_reporting::diagnostic::{Diagnostic, Label}; use codespan_reporting::term::termcolor::{ColorChoice, StandardStream}; use codespan_reporting::term as term_reporting; +use std::sync::atomic; + +static INTERRUPTED: AtomicBool = AtomicBool::new(false); struct ChatLangHelper { env: Arc>, @@ -69,7 +72,7 @@ impl Highlighter for ChatLangHelper { "let", "if", "then", "else", "case", "of", "lambda", "\\", "data", "struct", "try", "catch", "error", "for", "while", "true", "false", "in", "and", "or", "not", "class", "extends", "new", - "loop", "break" + "loop", "break", "load", "del" ]; let types = [ "Num", "Char", "String", "Bool", "Unit", "Uid", @@ -80,7 +83,6 @@ impl Highlighter for ChatLangHelper { ]; let mut chars = line.chars().peekable(); while let Some(ch) = chars.next() { - // Multiline comment if ch == '#' && chars.peek() == Some(&'-') { chars.next(); multiline_comment_depth += 1; @@ -112,7 +114,6 @@ impl Highlighter for ChatLangHelper { continue; } - // Single-line comment if ch == '#' && chars.peek() != Some(&'B') && chars.peek() != Some(&'-') { result.push_str("\x1b[3;90m#"); while let Some(c) = chars.next() { @@ -122,7 +123,6 @@ impl Highlighter for ChatLangHelper { break; } - // F-string if ch == 'f' && chars.peek() == Some(&'"') { in_fstring = true; result.push_str("\x1b[32m"); @@ -151,7 +151,6 @@ impl Highlighter for ChatLangHelper { continue; } - // Regular code highlighting if ch.is_alphabetic() || ch == '_' { let mut word = String::new(); word.push(ch); @@ -168,14 +167,7 @@ impl Highlighter for ChatLangHelper { } else if types.contains(&word.as_str()) { result.push_str(&format!("\x1b[35m{}\x1b[0m", word)); } else { - let env_guard = self.env.lock().unwrap(); - let defined = env_guard.vars.contains_key(&word); - drop(env_guard); - if defined { - result.push_str(&format!("\x1b[4;37m{}\x1b[0m", word)); - } else { - result.push_str(&format!("\x1b[37m{}\x1b[0m", word)); - } + result.push_str(&format!("\x1b[37m{}\x1b[0m", word)); } } else if ch.is_digit(10) { result.push_str(&format!("\x1b[33m{}\x1b[0m", ch)); @@ -241,6 +233,11 @@ fn main() { state_guard.p2p_port = p2p_port; } + ctrlc::set_handler(|| { + INTERRUPTED.store(true, Ordering::SeqCst); + plic::eval::set_interrupted(); + }).expect("Error setting Ctrl-C handler"); + if args.len() > 1 && !args[1].starts_with("--") { let filename = &args[1]; match fs::read_to_string(filename) { @@ -278,7 +275,7 @@ fn main() { "error".into(), "for".into(), "while".into(), "in".into(), "and".into(), "or".into(), "not".into(), "class".into(), "extends".into(), "new".into(), - "loop".into(), "break".into(), + "loop".into(), "break".into(), "load".into(), "del".into(), ], builtins: vec![ "sqrt".into(), "sin".into(), "cos".into(), "tan".into(), @@ -330,6 +327,7 @@ fn main() { "logout".into(), "deleteUser".into(), "deleteChat".into(), "listChats".into(), "members".into(), "addContact".into(), "removeContact".into(), + "p2pPort".into(), ], types: vec![ "Num".into(), "Char".into(), "String".into(), @@ -363,7 +361,6 @@ fn main() { rl.add_history_entry(line.as_str()); let trimmed = line.trim(); - // If we are in multiline and line is "end" or empty with indent 0, finish block if in_multiline && (trimmed == "end" || (line.chars().take_while(|c| *c == ' ').count() == 0 && trimmed.is_empty())) { let full_block = buffer.clone(); match parse_script(&full_block) { @@ -398,9 +395,7 @@ fn main() { buffer.push('\n'); buffer.push_str(&line); let current_indent = line.chars().take_while(|c| *c == ' ').count(); - // If the line has indent 0 and is not empty, it might be a new top-level statement – finish block if current_indent == 0 && !trimmed.is_empty() && trimmed != "end" { - // finish block let full_block = buffer.clone(); match parse_script(&full_block) { Ok(expr) => { @@ -415,26 +410,49 @@ fn main() { buffer.clear(); in_multiline = false; indent_level = 0; - // Now we need to process the current line as a new top-level expression? - // But we already consumed it. We'll just continue. - continue; - } else { - // Still inside block continue; } + continue; } - // Single-line expression match parse_expression(&stripped) { Ok(expr) => { let mut env_guard = env_arc.lock().unwrap(); match eval_expr(&expr, &mut env_guard, Arc::clone(&state)) { Ok(_) => {}, - Err(err) => eprintln!("\x1b[31merror\x1b[0m: {}", err), + Err(err) => { + if err.span.is_some() { + let mut files = Files::new(); + let file_id = files.add("", line.clone()); + let diagnostic = Diagnostic::error() + .with_message(err.message) + .with_labels(vec![ + Label::primary(file_id, err.span.unwrap().start..err.span.unwrap().end) + ]); + let writer = StandardStream::stderr(ColorChoice::Always); + let config = term_reporting::Config::default(); + let _ = term_reporting::emit(&mut writer.lock(), &config, &files, &diagnostic); + } else { + eprintln!("\x1b[31merror\x1b[0m: {}", err); + } + } } } - Err(ParseError { message, span: _span }) => { - eprintln!("\x1b[31merror\x1b[0m: Parse error: {}", message); + Err(ParseError { message, span }) => { + if span != plic::ast::Span::dummy() { + let mut files = Files::new(); + let file_id = files.add("", line.clone()); + let diagnostic = Diagnostic::error() + .with_message(message) + .with_labels(vec![ + Label::primary(file_id, span.start..span.end) + ]); + let writer = StandardStream::stderr(ColorChoice::Always); + let config = term_reporting::Config::default(); + let _ = term_reporting::emit(&mut writer.lock(), &config, &files, &diagnostic); + } else { + eprintln!("\x1b[31merror\x1b[0m: Parse error: {}", message); + } } } } diff --git a/src/p2p.rs b/src/p2p.rs index 397cc36..9094937 100644 --- a/src/p2p.rs +++ b/src/p2p.rs @@ -72,7 +72,6 @@ pub fn start_p2p_listener(port: u16, state: Arc>) { }; let acceptor = get_tls_acceptor(); thread::spawn(move || { - println!("P2P listener on port {} (TLS)", port); for stream in listener.incoming() { if let Ok(stream) = stream { let acceptor = acceptor.clone(); diff --git a/src/parser.rs b/src/parser.rs index cc713f2..51dd4e4 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -72,6 +72,7 @@ pub fn parse_expression(input: &str) -> ParseResult { let mut lexer = Lexer::new(&stripped); let tokens = lexer.tokenize()?; let mut pos = 0; + skip_newlines(&tokens, &mut pos); let expr = parse_expr(&tokens, &mut pos)?; while pos < tokens.len() && (matches!(tokens[pos].kind, TokenKind::NEWLINE) || matches!(tokens[pos].kind, TokenKind::DEDENT)) { pos += 1; @@ -89,6 +90,7 @@ pub fn parse_script(input: &str) -> ParseResult { let tokens = lexer.tokenize()?; let mut pos = 0; let mut exprs = Vec::new(); + skip_newlines(&tokens, &mut pos); while pos < tokens.len() && tokens[pos].kind != TokenKind::Eof { let e = parse_expr(&tokens, &mut pos)?; exprs.push(e); @@ -428,31 +430,13 @@ fn parse_atom(tokens: &[Token], pos: &mut usize) -> ParseResult { } let mut params = Vec::new(); - if *pos < tokens.len() && tokens[*pos].kind == TokenKind::LParen { - *pos += 1; - while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RParen { - if let TokenKind::Ident(p) = &tokens[*pos].kind { - params.push(p.clone()); - *pos += 1; - if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { - *pos += 1; - } - } else { - return Err(ParseError { message: "expected parameter name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }); - } - } - expect_kind(tokens, pos, TokenKind::RParen)?; - } else { - while *pos < tokens.len() && matches!(tokens[*pos].kind, TokenKind::Ident(_)) { - if let TokenKind::Ident(p) = &tokens[*pos].kind { - params.push(p.clone()); - *pos += 1; - if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { - *pos += 1; - } else { - break; - } - } else { break; } + // No parentheses allowed; just a sequence of identifiers without commas + while *pos < tokens.len() && matches!(tokens[*pos].kind, TokenKind::Ident(_)) { + if let TokenKind::Ident(p) = &tokens[*pos].kind { + params.push(p.clone()); + *pos += 1; + } else { + break; } } @@ -593,38 +577,20 @@ fn parse_atom(tokens: &[Token], pos: &mut usize) -> ParseResult { let end = expect_kind(tokens, pos, TokenKind::RParen)?; Ok(Expr::Tuple(elems, Span::new(start, end))) } else { - let end = expect_kind(tokens, pos, TokenKind::RParen)?; + let _end = expect_kind(tokens, pos, TokenKind::RParen)?; Ok(expr) } } TokenKind::Backslash | TokenKind::Lambda => { *pos += 1; let mut params = Vec::new(); - if *pos < tokens.len() && tokens[*pos].kind == TokenKind::LParen { - *pos += 1; - while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RParen { - if let TokenKind::Ident(p) = &tokens[*pos].kind { - params.push(p.clone()); - *pos += 1; - if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { - *pos += 1; - } - } else { - return Err(ParseError { message: "expected parameter name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }); - } - } - expect_kind(tokens, pos, TokenKind::RParen)?; - } else { - while *pos < tokens.len() && matches!(tokens[*pos].kind, TokenKind::Ident(_)) { - if let TokenKind::Ident(p) = &tokens[*pos].kind { - params.push(p.clone()); - *pos += 1; - if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { - *pos += 1; - } else { - break; - } - } else { break; } + // No parentheses; just a sequence of identifiers without commas + while *pos < tokens.len() && matches!(tokens[*pos].kind, TokenKind::Ident(_)) { + if let TokenKind::Ident(p) = &tokens[*pos].kind { + params.push(p.clone()); + *pos += 1; + } else { + break; } } expect_kind(tokens, pos, TokenKind::Arrow)?; @@ -931,8 +897,30 @@ fn parse_postfix(mut expr: Expr, tokens: &[Token], pos: &mut usize) -> ParseResu let end = expect_kind(tokens, pos, TokenKind::RParen)?; expr = Expr::MethodCall(Box::new(expr), field, args, Span::new(start, end)); } else { - let end = tokens[*pos - 1].end; - expr = Expr::FieldAccess(Box::new(expr), field, Span::new(start, end)); + // Check for record update: expr { field = value } + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::LBrace { + let start2 = tokens[*pos].start; + *pos += 1; + let mut updates = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RBrace { + let f = match &tokens[*pos].kind { + TokenKind::Ident(n) => n.clone(), + _ => return Err(ParseError { message: "expected field name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + }; + *pos += 1; + expect_kind(tokens, pos, TokenKind::Assign)?; + let val = parse_expr(tokens, pos)?; + updates.push((f, val)); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } + } + let end2 = expect_kind(tokens, pos, TokenKind::RBrace)?; + expr = Expr::RecordUpdate(Box::new(expr), updates, Span::new(start2, end2)); + } else { + let end = tokens[*pos - 1].end; + expr = Expr::FieldAccess(Box::new(expr), field, Span::new(start, end)); + } } } else { return Err(ParseError { message: "expected field or method name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }); diff --git a/src/server.rs b/src/server.rs index 23e44c8..2f7f87c 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,4 +1,4 @@ -//! Contact server (unchanged except for error handling). +//! Contact server. use std::collections::BTreeMap; use std::io::{BufRead, BufReader, Write}; @@ -22,10 +22,8 @@ pub fn start_contacts_server(addr: &str, password: Option) -> Result<(Co let (stop_tx, stop_rx) = std::sync::mpsc::channel(); let handle = thread::spawn(move || { - println!("Contact server (TLS) listening on {}", addr_owned); loop { if let Ok(()) = stop_rx.try_recv() { - println!("Contact server stopping."); break; } diff --git a/src/tests/integration.rs b/src/tests/integration.rs index 0c19bce..51dd4e4 100644 --- a/src/tests/integration.rs +++ b/src/tests/integration.rs @@ -1,56 +1,1225 @@ -#[test] -fn test_loop_and_break() { - let state = Arc::new(Mutex::new(ChatState::new())); - let mut env = Environment::new(); - let env_arc = Arc::new(Mutex::new(env.clone())); - builtins::populate(&mut env, Arc::clone(&state), env_arc); - - // loop with break - let input = "let x = 0\nloop { x = x + 1; if x == 5 then break x else () }"; - let expr = parse_script(input).unwrap(); - let result = eval_expr(&expr, &mut env, state).unwrap(); - assert_eq!(result.display(), "5"); -} - -#[test] -fn test_list_comprehension() { - let state = Arc::new(Mutex::new(ChatState::new())); - let mut env = Environment::new(); - let env_arc = Arc::new(Mutex::new(env.clone())); - builtins::populate(&mut env, Arc::clone(&state), env_arc); - - let expr = parse_expression("[x * 2 for x in [1,2,3] if x > 1]").unwrap(); - let result = eval_expr(&expr, &mut env, state).unwrap(); - assert_eq!(result.display(), "[4, 6]"); -} - -#[test] -fn test_load_and_del() { - use std::fs; - use std::io::Write; - let state = Arc::new(Mutex::new(ChatState::new())); - let mut env = Environment::new(); - let env_arc = Arc::new(Mutex::new(env.clone())); - builtins::populate(&mut env, Arc::clone(&state), env_arc); - - // Create temp file - let temp_path = "test_load.plic"; - let content = "let x = 42"; - fs::write(temp_path, content).unwrap(); - - let expr = parse_script(&format!("load \"{}\"", temp_path)).unwrap(); - eval_expr(&expr, &mut env, Arc::clone(&state)).unwrap(); - // Now x should be defined - let expr2 = parse_expression("x").unwrap(); - let result = eval_expr(&expr2, &mut env, Arc::clone(&state)).unwrap(); - assert_eq!(result.display(), "42"); - - // Test del - let expr3 = parse_expression("del \"x\"").unwrap(); - eval_expr(&expr3, &mut env, Arc::clone(&state)).unwrap(); - let expr4 = parse_expression("x").unwrap(); - let result4 = eval_expr(&expr4, &mut env, state); - assert!(result4.is_err()); - - fs::remove_file(temp_path).ok(); +//! Parser with indentation‑sensitive blocks, list comprehensions, and spans. + +use crate::ast::*; +use crate::lexer::{LexError, Lexer, Token, TokenKind}; + +#[derive(Debug, Clone, PartialEq)] +pub struct ParseError { + pub message: String, + pub span: Span, +} + +impl std::fmt::Display for ParseError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.message) + } +} + +impl std::error::Error for ParseError {} + +impl From for ParseError { + fn from(e: LexError) -> Self { + ParseError { message: e.to_string(), span: Span::dummy() } + } +} + +pub type ParseResult = Result; + +pub fn strip_comments(input: &str) -> String { + let mut output = String::new(); + let mut chars = input.chars().peekable(); + while let Some(ch) = chars.next() { + if ch == '#' { + if chars.peek() == Some(&'-') { + chars.next(); + let mut depth = 1; + while depth > 0 { + match chars.next() { + Some('#') => { + if chars.peek() == Some(&'-') { + chars.next(); + depth += 1; + } + } + Some('-') => { + if chars.peek() == Some(&'#') { + chars.next(); + depth -= 1; + } + } + Some(_) => {} + None => break, + } + } + continue; + } else { + while let Some(c) = chars.next() { + if c == '\n' { + output.push(c); + break; + } + } + continue; + } + } + output.push(ch); + } + output +} + +pub fn parse_expression(input: &str) -> ParseResult { + let stripped = strip_comments(input); + let mut lexer = Lexer::new(&stripped); + let tokens = lexer.tokenize()?; + let mut pos = 0; + skip_newlines(&tokens, &mut pos); + let expr = parse_expr(&tokens, &mut pos)?; + while pos < tokens.len() && (matches!(tokens[pos].kind, TokenKind::NEWLINE) || matches!(tokens[pos].kind, TokenKind::DEDENT)) { + pos += 1; + } + if pos < tokens.len() && tokens[pos].kind != TokenKind::Eof { + Err(ParseError { message: format!("Unexpected token: {:?}", tokens[pos].kind), span: Span::new(tokens[pos].start, tokens[pos].end) }) + } else { + Ok(expr) + } +} + +pub fn parse_script(input: &str) -> ParseResult { + let stripped = strip_comments(input); + let mut lexer = Lexer::new(&stripped); + let tokens = lexer.tokenize()?; + let mut pos = 0; + let mut exprs = Vec::new(); + skip_newlines(&tokens, &mut pos); + while pos < tokens.len() && tokens[pos].kind != TokenKind::Eof { + let e = parse_expr(&tokens, &mut pos)?; + exprs.push(e); + while pos < tokens.len() && (matches!(tokens[pos].kind, TokenKind::NEWLINE) || matches!(tokens[pos].kind, TokenKind::Semicolon)) { + pos += 1; + } + } + if exprs.is_empty() { + Ok(Expr::Lit(Literal::Unit, Span::dummy())) + } else if exprs.len() == 1 { + Ok(exprs.remove(0)) + } else { + let start = exprs.first().map(|e| e_span(e).start).unwrap_or(0); + let end = exprs.last().map(|e| e_span(e).end).unwrap_or(0); + Ok(Expr::Block(exprs, Span::new(start, end))) + } +} + +fn e_span(e: &Expr) -> Span { + match e { + Expr::Lit(_, s) => *s, + Expr::Var(_, s) => *s, + Expr::Lambda(_, _, s) => *s, + Expr::App(_, _, s) => *s, + Expr::If(_, _, _, s) => *s, + Expr::Let { span, .. } => *span, + Expr::Assign(_, _, s) => *s, + Expr::Case(_, _, s) => *s, + Expr::Try(_, s) => *s, + Expr::Catch(_, _, _, s) => *s, + Expr::Throw(_, s) => *s, + Expr::DataDef(_, _, _, s) => *s, + Expr::StructDef(_, _, s) => *s, + Expr::StructNew(_, _, s) => *s, + Expr::Constructor(_, _, s) => *s, + Expr::Record(_, s) => *s, + Expr::FieldAccess(_, _, s) => *s, + Expr::RecordUpdate(_, _, s) => *s, + Expr::List(_, s) => *s, + Expr::Range(_, _, s) => *s, + Expr::BinOp(_, _, _, s) => *s, + Expr::Concat(_, _, s) => *s, + Expr::Pipe(_, _, s) => *s, + Expr::Dollar(_, _, s) => *s, + Expr::LogicalAnd(_, _, s) => *s, + Expr::LogicalOr(_, _, s) => *s, + Expr::Not(_, s) => *s, + Expr::Tuple(_, s) => *s, + Expr::Index(_, _, s) => *s, + Expr::For(_, _, _, s) => *s, + Expr::While(_, _, s) => *s, + Expr::Loop(_, s) => *s, + Expr::Break(_, s) => *s, + Expr::Block(_, s) => *s, + Expr::ClassDef { span, .. } => *span, + Expr::New(_, _, s) => *s, + Expr::MethodCall(_, _, _, s) => *s, + Expr::MapLiteral(_, s) => *s, + Expr::SetLiteral(_, s) => *s, + Expr::FString(_, s) => *s, + Expr::ListComp { span, .. } => *span, + } +} + +fn parse_expr(tokens: &[Token], pos: &mut usize) -> ParseResult { + parse_dollar(tokens, pos) +} + +fn parse_dollar(tokens: &[Token], pos: &mut usize) -> ParseResult { + let mut left = parse_assign(tokens, pos)?; + while *pos < tokens.len() && tokens[*pos].kind == TokenKind::Dollar { + let start = tokens[*pos].start; + *pos += 1; + let right = parse_assign(tokens, pos)?; + let end = e_span(&right).end; + left = Expr::Dollar(Box::new(left), Box::new(right), Span::new(start, end)); + } + Ok(left) +} + +fn parse_assign(tokens: &[Token], pos: &mut usize) -> ParseResult { + let left = parse_pipe(tokens, pos)?; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Assign { + if let Expr::Var(name, _span) = left { + let start = tokens[*pos].start; + *pos += 1; + let right = parse_assign(tokens, pos)?; + let end = e_span(&right).end; + Ok(Expr::Assign(name, Box::new(right), Span::new(start, end))) + } else { + Err(ParseError { message: "only variables can be assigned to".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }) + } + } else { + Ok(left) + } +} + +fn parse_pipe(tokens: &[Token], pos: &mut usize) -> ParseResult { + let mut left = parse_logical_or(tokens, pos)?; + while *pos < tokens.len() && tokens[*pos].kind == TokenKind::Pipe { + let start = tokens[*pos].start; + *pos += 1; + let right = parse_logical_or(tokens, pos)?; + let end = e_span(&right).end; + left = Expr::Pipe(Box::new(left), Box::new(right), Span::new(start, end)); + } + Ok(left) +} + +fn parse_logical_or(tokens: &[Token], pos: &mut usize) -> ParseResult { + let mut left = parse_logical_and(tokens, pos)?; + while *pos < tokens.len() && tokens[*pos].kind == TokenKind::Or { + let start = tokens[*pos].start; + *pos += 1; + let right = parse_logical_and(tokens, pos)?; + let end = e_span(&right).end; + left = Expr::LogicalOr(Box::new(left), Box::new(right), Span::new(start, end)); + } + Ok(left) +} + +fn parse_logical_and(tokens: &[Token], pos: &mut usize) -> ParseResult { + let mut left = parse_comparison(tokens, pos)?; + while *pos < tokens.len() && tokens[*pos].kind == TokenKind::And { + let start = tokens[*pos].start; + *pos += 1; + let right = parse_comparison(tokens, pos)?; + let end = e_span(&right).end; + left = Expr::LogicalAnd(Box::new(left), Box::new(right), Span::new(start, end)); + } + Ok(left) +} + +fn parse_comparison(tokens: &[Token], pos: &mut usize) -> ParseResult { + let mut left = parse_concat(tokens, pos)?; + while *pos < tokens.len() { + let op = match tokens[*pos].kind { + TokenKind::Eq => BinOp::Eq, + TokenKind::Neq => BinOp::Neq, + TokenKind::Lt => BinOp::Lt, + TokenKind::Le => BinOp::Le, + TokenKind::Gt => BinOp::Gt, + TokenKind::Ge => BinOp::Ge, + TokenKind::In => { + let start = tokens[*pos].start; + *pos += 1; + let right = parse_concat(tokens, pos)?; + let end = e_span(&right).end; + left = Expr::BinOp(BinOp::In, Box::new(left), Box::new(right), Span::new(start, end)); + continue; + } + _ => break, + }; + let start = tokens[*pos].start; + *pos += 1; + let right = parse_concat(tokens, pos)?; + let end = e_span(&right).end; + left = Expr::BinOp(op, Box::new(left), Box::new(right), Span::new(start, end)); + } + Ok(left) +} + +fn parse_concat(tokens: &[Token], pos: &mut usize) -> ParseResult { + let mut left = parse_cons(tokens, pos)?; + while *pos < tokens.len() && tokens[*pos].kind == TokenKind::Concat { + let start = tokens[*pos].start; + *pos += 1; + let right = parse_cons(tokens, pos)?; + let end = e_span(&right).end; + left = Expr::Concat(Box::new(left), Box::new(right), Span::new(start, end)); + } + Ok(left) +} + +fn parse_cons(tokens: &[Token], pos: &mut usize) -> ParseResult { + let mut left = parse_addsub(tokens, pos)?; + while *pos < tokens.len() && tokens[*pos].kind == TokenKind::Cons { + let start = tokens[*pos].start; + *pos += 1; + let right = parse_cons(tokens, pos)?; + let end = e_span(&right).end; + left = Expr::BinOp(BinOp::Cons, Box::new(left), Box::new(right), Span::new(start, end)); + } + Ok(left) +} + +fn parse_addsub(tokens: &[Token], pos: &mut usize) -> ParseResult { + let mut left = parse_muldiv(tokens, pos)?; + while *pos < tokens.len() { + let op = match tokens[*pos].kind { + TokenKind::Plus => BinOp::Add, + TokenKind::Minus => BinOp::Sub, + _ => break, + }; + let start = tokens[*pos].start; + *pos += 1; + let right = parse_muldiv(tokens, pos)?; + let end = e_span(&right).end; + left = Expr::BinOp(op, Box::new(left), Box::new(right), Span::new(start, end)); + } + Ok(left) +} + +fn parse_muldiv(tokens: &[Token], pos: &mut usize) -> ParseResult { + let mut left = parse_unary(tokens, pos)?; + while *pos < tokens.len() { + let op = match tokens[*pos].kind { + TokenKind::Star => BinOp::Mul, + TokenKind::Slash => BinOp::Div, + TokenKind::Percent => BinOp::Mod, + _ => break, + }; + let start = tokens[*pos].start; + *pos += 1; + let right = parse_unary(tokens, pos)?; + let end = e_span(&right).end; + left = Expr::BinOp(op, Box::new(left), Box::new(right), Span::new(start, end)); + } + Ok(left) +} + +fn parse_unary(tokens: &[Token], pos: &mut usize) -> ParseResult { + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Not { + let start = tokens[*pos].start; + *pos += 1; + let expr = parse_atom(tokens, pos)?; + let end = e_span(&expr).end; + Ok(Expr::Not(Box::new(expr), Span::new(start, end))) + } else if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Minus { + let start = tokens[*pos].start; + *pos += 1; + let expr = parse_atom(tokens, pos)?; + let end = e_span(&expr).end; + Ok(Expr::BinOp(BinOp::Sub, Box::new(Expr::Lit(Literal::Int(0), Span::new(start, start))), Box::new(expr), Span::new(start, end))) + } else { + parse_atom(tokens, pos) + } +} + +fn skip_newlines(tokens: &[Token], pos: &mut usize) { + while *pos < tokens.len() && tokens[*pos].kind == TokenKind::NEWLINE { + *pos += 1; + } +} + +fn parse_atom(tokens: &[Token], pos: &mut usize) -> ParseResult { + if *pos >= tokens.len() { + return Err(ParseError { message: "incomplete input".to_string(), span: Span::dummy() }); + } + let start = tokens[*pos].start; + match &tokens[*pos].kind { + TokenKind::Literal(lit) => { + *pos += 1; + let end = tokens[*pos - 1].end; + let expr = Expr::Lit(lit.clone(), Span::new(start, end)); + parse_postfix(expr, tokens, pos) + } + TokenKind::FString(content) => { + *pos += 1; + let end = tokens[*pos - 1].end; + let parts = parse_fstring(content)?; + Ok(Expr::FString(parts, Span::new(start, end))) + } + TokenKind::Ident(name) => { + *pos += 1; + let end = tokens[*pos - 1].end; + let mut expr = Expr::Var(name.clone(), Span::new(start, end)); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::LParen { + let temp_pos = *pos + 1; + if is_struct_constructor(tokens, temp_pos) { + *pos += 1; + let mut fields = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RParen { + let field_name = match &tokens[*pos].kind { + TokenKind::Ident(n) => n.clone(), + _ => return Err(ParseError { message: "expected field name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + }; + let _field_start = tokens[*pos].start; + *pos += 1; + expect_kind(tokens, pos, TokenKind::Assign)?; + let value = parse_expr(tokens, pos)?; + let _value_end = e_span(&value).end; + fields.push((field_name, value)); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } + } + let _end = expect_kind(tokens, pos, TokenKind::RParen)?; + expr = Expr::StructNew(name.clone(), fields, Span::new(start, _end)); + } else { + *pos += 1; + let mut args = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RParen { + let arg = parse_expr(tokens, pos)?; + args.push(arg); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } + } + let _end = expect_kind(tokens, pos, TokenKind::RParen)?; + for arg in args { + let end = e_span(&arg).end; + expr = Expr::App(Box::new(expr), Box::new(arg), Span::new(start, end)); + } + } + } else { + expr = parse_application(expr, tokens, pos)?; + } + parse_postfix(expr, tokens, pos) + } + TokenKind::If => { + *pos += 1; + let cond = parse_expr(tokens, pos)?; + skip_newlines(tokens, pos); + expect_kind(tokens, pos, TokenKind::Then)?; + let then_expr = parse_expr_or_block(tokens, pos)?; + skip_newlines(tokens, pos); + expect_kind(tokens, pos, TokenKind::Else)?; + let else_expr = parse_expr_or_block(tokens, pos)?; + let end = e_span(&else_expr).end; + Ok(Expr::If(Box::new(cond), Box::new(then_expr), Box::new(else_expr), Span::new(start, end))) + } + TokenKind::Let => { + *pos += 1; + let name = match &tokens[*pos].kind { + TokenKind::Ident(n) => n.clone(), + _ => return Err(ParseError { message: "expected identifier".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + }; + let _name_start = tokens[*pos].start; + *pos += 1; + + let mut type_ann = None; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::DoubleColon { + *pos += 1; + let type_str = parse_type(tokens, pos)?; + type_ann = Some(type_str); + } + + let mut params = Vec::new(); + // No parentheses allowed; just a sequence of identifiers without commas + while *pos < tokens.len() && matches!(tokens[*pos].kind, TokenKind::Ident(_)) { + if let TokenKind::Ident(p) = &tokens[*pos].kind { + params.push(p.clone()); + *pos += 1; + } else { + break; + } + } + + expect_kind(tokens, pos, TokenKind::Assign)?; + let body = parse_expr_or_block(tokens, pos)?; + let end = e_span(&body).end; + if params.is_empty() { + Ok(Expr::Let { + name, + type_ann, + def: Box::new(body), + body: None, + span: Span::new(start, end), + }) + } else { + let lambda = params.into_iter().rfold(body, |acc, p| { + Expr::Lambda(vec![p], Box::new(acc), Span::new(start, end)) + }); + Ok(Expr::Let { + name, + type_ann, + def: Box::new(lambda), + body: None, + span: Span::new(start, end), + }) + } + } + TokenKind::Case => { + *pos += 1; + let scrut = parse_expr(tokens, pos)?; + skip_newlines(tokens, pos); + expect_kind(tokens, pos, TokenKind::Of)?; + skip_newlines(tokens, pos); + let mut arms = Vec::new(); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::INDENT { + *pos += 1; + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::DEDENT { + let pat = parse_pattern(tokens, pos)?; + expect_kind(tokens, pos, TokenKind::Arrow)?; + let arm_body = parse_expr(tokens, pos)?; + arms.push((pat, Box::new(arm_body))); + if *pos < tokens.len() && (tokens[*pos].kind == TokenKind::Semicolon || tokens[*pos].kind == TokenKind::NEWLINE) { + *pos += 1; + } + } + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::DEDENT { + *pos += 1; + } + let end = if !arms.is_empty() { e_span(&arms.last().unwrap().1).end } else { tokens[*pos - 1].end }; + Ok(Expr::Case(Box::new(scrut), arms, Span::new(start, end))) + } else { + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::Eof && tokens[*pos].kind != TokenKind::NEWLINE { + let pat = parse_pattern(tokens, pos)?; + expect_kind(tokens, pos, TokenKind::Arrow)?; + let arm_body = parse_expr(tokens, pos)?; + arms.push((pat, Box::new(arm_body))); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Semicolon { + *pos += 1; + } else { + break; + } + } + let end = if !arms.is_empty() { e_span(&arms.last().unwrap().1).end } else { tokens[*pos - 1].end }; + Ok(Expr::Case(Box::new(scrut), arms, Span::new(start, end))) + } + } + TokenKind::Try => { + *pos += 1; + let try_expr = parse_expr(tokens, pos)?; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Catch { + *pos += 1; + let pat = parse_pattern(tokens, pos)?; + skip_newlines(tokens, pos); + expect_kind(tokens, pos, TokenKind::Arrow)?; + let handler = parse_expr(tokens, pos)?; + let end = e_span(&handler).end; + Ok(Expr::Catch(Box::new(try_expr), pat, Box::new(handler), Span::new(start, end))) + } else { + let end = e_span(&try_expr).end; + Ok(Expr::Try(Box::new(try_expr), Span::new(start, end))) + } + } + TokenKind::Error => { + *pos += 1; + let msg = parse_expr(tokens, pos)?; + let end = e_span(&msg).end; + Ok(Expr::Throw(Box::new(msg), Span::new(start, end))) + } + TokenKind::LBracket => { + *pos += 1; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::RBracket { + *pos += 1; + let end = tokens[*pos - 1].end; + return Ok(Expr::List(vec![], Span::new(start, end))); + } + let first = parse_expr(tokens, pos)?; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::For { + let expr = first; + let generators = Vec::new(); + let filters = Vec::new(); + let (gens, filters) = parse_list_comp_generators(tokens, pos, generators, filters)?; + let end = expect_kind(tokens, pos, TokenKind::RBracket)?; + Ok(Expr::ListComp { + expr: Box::new(expr), + generators: gens, + filters, + span: Span::new(start, end), + }) + } else if *pos < tokens.len() && tokens[*pos].kind == TokenKind::DotDot { + *pos += 1; + let end_expr = parse_expr(tokens, pos)?; + let end = expect_kind(tokens, pos, TokenKind::RBracket)?; + Ok(Expr::Range(Box::new(first), Box::new(end_expr), Span::new(start, end))) + } else { + let mut elems = vec![first]; + while *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + elems.push(parse_expr(tokens, pos)?); + } + let end = expect_kind(tokens, pos, TokenKind::RBracket)?; + Ok(Expr::List(elems, Span::new(start, end))) + } + } + TokenKind::LParen => { + *pos += 1; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::RParen { + *pos += 1; + let end = tokens[*pos - 1].end; + return Ok(Expr::Lit(Literal::Unit, Span::new(start, end))); + } + let expr = parse_expr(tokens, pos)?; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + let mut elems = vec![expr]; + while *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + elems.push(parse_expr(tokens, pos)?); + } + let end = expect_kind(tokens, pos, TokenKind::RParen)?; + Ok(Expr::Tuple(elems, Span::new(start, end))) + } else { + let _end = expect_kind(tokens, pos, TokenKind::RParen)?; + Ok(expr) + } + } + TokenKind::Backslash | TokenKind::Lambda => { + *pos += 1; + let mut params = Vec::new(); + // No parentheses; just a sequence of identifiers without commas + while *pos < tokens.len() && matches!(tokens[*pos].kind, TokenKind::Ident(_)) { + if let TokenKind::Ident(p) = &tokens[*pos].kind { + params.push(p.clone()); + *pos += 1; + } else { + break; + } + } + expect_kind(tokens, pos, TokenKind::Arrow)?; + let body = parse_expr_or_block(tokens, pos)?; + let end = e_span(&body).end; + Ok(Expr::Lambda(params, Box::new(body), Span::new(start, end))) + } + TokenKind::Struct => { + *pos += 1; + let name = match &tokens[*pos].kind { + TokenKind::Ident(n) => n.clone(), + _ => return Err(ParseError { message: "expected struct name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + }; + *pos += 1; + expect_kind(tokens, pos, TokenKind::Assign)?; + expect_kind(tokens, pos, TokenKind::LParen)?; + let mut fields = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RParen { + let field_name = match &tokens[*pos].kind { + TokenKind::Ident(n) => n.clone(), + _ => return Err(ParseError { message: "expected field name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + }; + *pos += 1; + expect_kind(tokens, pos, TokenKind::Assign)?; + let field_type = match &tokens[*pos].kind { + TokenKind::Ident(t) => t.clone(), + _ => return Err(ParseError { message: "expected type name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + }; + *pos += 1; + fields.push((field_name, field_type)); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } else { + break; + } + } + let end = expect_kind(tokens, pos, TokenKind::RParen)?; + Ok(Expr::StructDef(name, fields, Span::new(start, end))) + } + TokenKind::Data => { + *pos += 1; + let name = match &tokens[*pos].kind { + TokenKind::Ident(n) => n.clone(), + _ => return Err(ParseError { message: "expected data type name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + }; + *pos += 1; + let mut params = Vec::new(); + while *pos < tokens.len() && matches!(tokens[*pos].kind, TokenKind::Ident(_)) { + if let TokenKind::Ident(p) = &tokens[*pos].kind { + params.push(p.clone()); + *pos += 1; + } else { break; } + } + expect_kind(tokens, pos, TokenKind::Assign)?; + let mut constructors = Vec::new(); + loop { + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::RParen { + *pos += 1; + break; + } + let ctor_name = match &tokens[*pos].kind { + TokenKind::Ident(n) => n.clone(), + _ => return Err(ParseError { message: "expected constructor name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + }; + let ctor_start = tokens[*pos].start; + *pos += 1; + let mut fields = Vec::new(); + while *pos < tokens.len() && matches!(tokens[*pos].kind, TokenKind::Ident(_)) { + if let TokenKind::Ident(t) = &tokens[*pos].kind { + fields.push(t.clone()); + *pos += 1; + } else { break; } + } + let ctor_end = tokens[*pos - 1].end; + constructors.push(ConstructorDef { name: ctor_name, fields, span: Span::new(ctor_start, ctor_end) }); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Pipe { + *pos += 1; + } else { + break; + } + } + let end = tokens[*pos - 1].end; + Ok(Expr::DataDef(name, params, constructors, Span::new(start, end))) + } + TokenKind::For => { + *pos += 1; + let var = match &tokens[*pos].kind { + TokenKind::Ident(n) => n.clone(), + _ => return Err(ParseError { message: "expected loop variable".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + }; + *pos += 1; + expect_kind(tokens, pos, TokenKind::In)?; + let iterable = parse_expr(tokens, pos)?; + skip_newlines(tokens, pos); + expect_kind(tokens, pos, TokenKind::Colon)?; + let body = parse_expr_or_block(tokens, pos)?; + let end = e_span(&body).end; + Ok(Expr::For(var, Box::new(iterable), Box::new(body), Span::new(start, end))) + } + TokenKind::While => { + *pos += 1; + let cond = parse_expr(tokens, pos)?; + skip_newlines(tokens, pos); + expect_kind(tokens, pos, TokenKind::Colon)?; + let body = parse_expr_or_block(tokens, pos)?; + let end = e_span(&body).end; + Ok(Expr::While(Box::new(cond), Box::new(body), Span::new(start, end))) + } + TokenKind::Loop => { + *pos += 1; + expect_kind(tokens, pos, TokenKind::LBrace)?; + let body = parse_expr(tokens, pos)?; + let end = expect_kind(tokens, pos, TokenKind::RBrace)?; + Ok(Expr::Loop(Box::new(body), Span::new(start, end))) + } + TokenKind::Break => { + *pos += 1; + let opt = if *pos < tokens.len() && !matches!(tokens[*pos].kind, TokenKind::RBrace | TokenKind::NEWLINE | TokenKind::Semicolon | TokenKind::Eof) { + Some(Box::new(parse_expr(tokens, pos)?)) + } else { + None + }; + let end = if let Some(e) = &opt { e_span(e).end } else { tokens[*pos - 1].end }; + Ok(Expr::Break(opt, Span::new(start, end))) + } + TokenKind::Class => { + *pos += 1; + let name = match &tokens[*pos].kind { + TokenKind::Ident(n) => n.clone(), + _ => return Err(ParseError { message: "expected class name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + }; + *pos += 1; + let mut extends = None; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Extends { + *pos += 1; + if let TokenKind::Ident(parent) = &tokens[*pos].kind { + extends = Some(parent.clone()); + *pos += 1; + } else { + return Err(ParseError { message: "expected parent class name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }); + } + } + expect_kind(tokens, pos, TokenKind::Assign)?; + expect_kind(tokens, pos, TokenKind::LParen)?; + let mut fields = Vec::new(); + let mut methods = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RParen { + match &tokens[*pos].kind { + TokenKind::Ident(name) => { + let method_name = name.clone(); + let start = tokens[*pos].start; + *pos += 1; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Assign { + *pos += 1; + let field_type = if let TokenKind::Ident(t) = &tokens[*pos].kind { + t.clone() + } else { + return Err(ParseError { message: "expected type name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }); + }; + *pos += 1; + fields.push((method_name, Some(field_type))); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } + } else if *pos < tokens.len() && tokens[*pos].kind == TokenKind::LParen { + *pos += 1; + let mut params = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RParen { + if let TokenKind::Ident(p) = &tokens[*pos].kind { + params.push(p.clone()); + *pos += 1; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } + } else { + return Err(ParseError { message: "expected parameter name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }); + } + } + expect_kind(tokens, pos, TokenKind::RParen)?; + expect_kind(tokens, pos, TokenKind::Assign)?; + let body = parse_expr(tokens, pos)?; + let end = e_span(&body).end; + methods.push(MethodDef { + name: method_name, + params, + body: Box::new(body), + span: Span::new(start, end), + }); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Semicolon { + *pos += 1; + } + } else { + return Err(ParseError { message: "expected '=' or '(' after field/method name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }); + } + } + _ => return Err(ParseError { message: format!("unexpected token {:?}", tokens[*pos].kind), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + } + } + let _end = expect_kind(tokens, pos, TokenKind::RParen)?; + Ok(Expr::ClassDef { + name, + extends, + fields, + methods, + span: Span::new(start, _end), + }) + } + TokenKind::New => { + *pos += 1; + let class_name = match &tokens[*pos].kind { + TokenKind::Ident(n) => n.clone(), + _ => return Err(ParseError { message: "expected class name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + }; + *pos += 1; + expect_kind(tokens, pos, TokenKind::LParen)?; + let mut args = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RParen { + args.push(parse_expr(tokens, pos)?); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } + } + let _end = expect_kind(tokens, pos, TokenKind::RParen)?; + Ok(Expr::New(class_name, args, Span::new(start, _end))) + } + TokenKind::Percent => { + *pos += 1; + match &tokens[*pos].kind { + TokenKind::LParen => { + *pos += 1; + let mut entries = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RParen { + let key = parse_expr(tokens, pos)?; + expect_kind(tokens, pos, TokenKind::FatArrow)?; + let value = parse_expr(tokens, pos)?; + entries.push((key, value)); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } + } + let _end = expect_kind(tokens, pos, TokenKind::RParen)?; + Ok(Expr::MapLiteral(entries, Span::new(start, _end))) + } + TokenKind::LBracket => { + *pos += 1; + let mut elems = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RBracket { + elems.push(parse_expr(tokens, pos)?); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } + } + let end = expect_kind(tokens, pos, TokenKind::RBracket)?; + Ok(Expr::SetLiteral(elems, Span::new(start, end))) + } + _ => Err(ParseError { message: "expected '(' or '[' after '%'".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + } + } + _ => Err(ParseError { message: format!("unexpected token {:?}", tokens[*pos].kind), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + } +} + +fn is_struct_constructor(tokens: &[Token], mut pos: usize) -> bool { + let mut depth = 1; + while pos < tokens.len() && depth > 0 { + match tokens[pos].kind { + TokenKind::LParen => depth += 1, + TokenKind::RParen => depth -= 1, + TokenKind::Assign => { + if depth == 1 { + return true; + } + } + _ => {} + } + pos += 1; + } + false +} + +fn parse_postfix(mut expr: Expr, tokens: &[Token], pos: &mut usize) -> ParseResult { + loop { + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::LBracket { + let start = tokens[*pos].start; + *pos += 1; + let index = parse_expr(tokens, pos)?; + let end = expect_kind(tokens, pos, TokenKind::RBracket)?; + expr = Expr::Index(Box::new(expr), Box::new(index), Span::new(start, end)); + } else if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Dot { + let start = tokens[*pos].start; + *pos += 1; + if let TokenKind::Ident(field) = &tokens[*pos].kind { + let field = field.clone(); + *pos += 1; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::LParen { + *pos += 1; + let mut args = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RParen { + args.push(parse_expr(tokens, pos)?); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } + } + let end = expect_kind(tokens, pos, TokenKind::RParen)?; + expr = Expr::MethodCall(Box::new(expr), field, args, Span::new(start, end)); + } else { + // Check for record update: expr { field = value } + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::LBrace { + let start2 = tokens[*pos].start; + *pos += 1; + let mut updates = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RBrace { + let f = match &tokens[*pos].kind { + TokenKind::Ident(n) => n.clone(), + _ => return Err(ParseError { message: "expected field name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + }; + *pos += 1; + expect_kind(tokens, pos, TokenKind::Assign)?; + let val = parse_expr(tokens, pos)?; + updates.push((f, val)); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } + } + let end2 = expect_kind(tokens, pos, TokenKind::RBrace)?; + expr = Expr::RecordUpdate(Box::new(expr), updates, Span::new(start2, end2)); + } else { + let end = tokens[*pos - 1].end; + expr = Expr::FieldAccess(Box::new(expr), field, Span::new(start, end)); + } + } + } else { + return Err(ParseError { message: "expected field or method name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }); + } + } else { + break; + } + } + Ok(expr) +} + +fn parse_application(mut func: Expr, tokens: &[Token], pos: &mut usize) -> ParseResult { + loop { + if *pos < tokens.len() && is_argument_start(&tokens[*pos].kind) { + let arg = parse_expr(tokens, pos)?; + let end = e_span(&arg).end; + let start = e_span(&func).start; + func = Expr::App(Box::new(func), Box::new(arg), Span::new(start, end)); + } else { + break; + } + } + Ok(func) +} + +fn is_argument_start(kind: &TokenKind) -> bool { + matches!(kind, + TokenKind::Literal(_) | TokenKind::Ident(_) | TokenKind::LParen | TokenKind::LBracket | + TokenKind::Backslash | TokenKind::Lambda | TokenKind::If | TokenKind::Let | TokenKind::Case | + TokenKind::Try | TokenKind::Error | TokenKind::For | TokenKind::While | TokenKind::Not | + TokenKind::Minus | TokenKind::Percent | TokenKind::Class | TokenKind::New | TokenKind::Struct | TokenKind::Data | + TokenKind::FString(_) | TokenKind::Loop | TokenKind::Break + ) +} + +fn parse_expr_or_block(tokens: &[Token], pos: &mut usize) -> ParseResult { + skip_newlines(tokens, pos); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::INDENT { + let start = tokens[*pos].start; + *pos += 1; + let mut exprs = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::DEDENT { + let e = parse_expr(tokens, pos)?; + exprs.push(e); + if *pos < tokens.len() && (tokens[*pos].kind == TokenKind::NEWLINE || tokens[*pos].kind == TokenKind::Semicolon) { + *pos += 1; + } + } + let end = if *pos < tokens.len() && tokens[*pos].kind == TokenKind::DEDENT { + let dedent_end = tokens[*pos].end; + *pos += 1; + dedent_end + } else { + tokens[*pos - 1].end + }; + if exprs.len() == 1 { + Ok(exprs.remove(0)) + } else { + Ok(Expr::Block(exprs, Span::new(start, end))) + } + } else { + parse_expr(tokens, pos) + } +} + +fn parse_pattern(tokens: &[Token], pos: &mut usize) -> Result { + if *pos >= tokens.len() { + return Err(ParseError { message: "incomplete input".to_string(), span: Span::dummy() }); + } + let start = tokens[*pos].start; + match &tokens[*pos].kind { + TokenKind::Ident(name) => { + *pos += 1; + let end = tokens[*pos - 1].end; + if name == "_" { + Ok(Pattern::Wildcard(Span::new(start, end))) + } else { + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::LParen { + *pos += 1; + let mut sub_pats = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RParen { + let pat = parse_pattern(tokens, pos)?; + sub_pats.push(pat); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } + } + let end = expect_kind(tokens, pos, TokenKind::RParen)?; + Ok(Pattern::Constructor(name.clone(), sub_pats, Span::new(start, end))) + } else { + Ok(Pattern::Var(name.clone(), Span::new(start, end))) + } + } + } + TokenKind::Literal(lit) => { + *pos += 1; + let end = tokens[*pos - 1].end; + Ok(Pattern::Literal(lit.clone(), Span::new(start, end))) + } + TokenKind::LBracket => { + *pos += 1; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::RBracket { + *pos += 1; + let end = tokens[*pos - 1].end; + return Ok(Pattern::List(vec![], Span::new(start, end))); + } + let mut pats = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RBracket { + pats.push(parse_pattern(tokens, pos)?); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } + } + let end = expect_kind(tokens, pos, TokenKind::RBracket)?; + Ok(Pattern::List(pats, Span::new(start, end))) + } + TokenKind::LParen => { + *pos += 1; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::RParen { + *pos += 1; + let end = tokens[*pos - 1].end; + return Ok(Pattern::Literal(Literal::Unit, Span::new(start, end))); + } + let pat = parse_pattern(tokens, pos)?; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + let mut pats = vec![pat]; + while *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + pats.push(parse_pattern(tokens, pos)?); + } + let end = expect_kind(tokens, pos, TokenKind::RParen)?; + Ok(Pattern::Tuple(pats, Span::new(start, end))) + } else { + let end = expect_kind(tokens, pos, TokenKind::RParen)?; + Ok(pat) + } + } + TokenKind::LBrace => { + *pos += 1; + let mut fields = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RBrace { + let field_name = match &tokens[*pos].kind { + TokenKind::Ident(n) => n.clone(), + _ => return Err(ParseError { message: "expected field name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + }; + *pos += 1; + expect_kind(tokens, pos, TokenKind::Assign)?; + let pat = parse_pattern(tokens, pos)?; + fields.push((field_name, pat)); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } + } + let end = expect_kind(tokens, pos, TokenKind::RBrace)?; + Ok(Pattern::Record(fields, Span::new(start, end))) + } + _ => Err(ParseError { message: format!("unexpected token {:?}", tokens[*pos].kind), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + } +} + +fn parse_type(tokens: &[Token], pos: &mut usize) -> Result { + let mut type_str = String::new(); + while *pos < tokens.len() { + match &tokens[*pos].kind { + TokenKind::Ident(name) => { + if !type_str.is_empty() { type_str.push(' '); } + type_str.push_str(name); + *pos += 1; + } + TokenKind::Arrow => { + type_str.push_str(" -> "); + *pos += 1; + } + _ => break, + } + } + if type_str.is_empty() { + Err(ParseError { message: "expected type".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }) + } else { + Ok(type_str) + } +} + +fn expect_kind(tokens: &[Token], pos: &mut usize, expected: TokenKind) -> Result { + if *pos < tokens.len() && tokens[*pos].kind == expected { + let end = tokens[*pos].end; + *pos += 1; + Ok(end) + } else { + Err(ParseError { message: format!("expected {:?}", expected), span: Span::new(tokens[*pos].start, tokens[*pos].end) }) + } +} + +fn parse_fstring(raw: &str) -> Result, ParseError> { + let mut parts = Vec::new(); + let chars: Vec = raw.chars().collect(); + let mut i = 0; + while i < chars.len() { + let ch = chars[i]; + if ch == '{' { + if i + 1 < chars.len() && chars[i + 1] == '{' { + parts.push(FStringPart::Literal("{".to_string())); + i += 2; + continue; + } + i += 1; + let start = i; + let mut depth = 1; + let mut in_string = false; + while i < chars.len() && depth > 0 { + let c = chars[i]; + if c == '"' && (i == 0 || chars[i-1] != '\\') { + in_string = !in_string; + } + if !in_string { + if c == '{' { + depth += 1; + } else if c == '}' { + depth -= 1; + if depth == 0 { + break; + } + } + } + i += 1; + } + if depth != 0 { + return Err(ParseError { message: "unclosed expression in f-string".to_string(), span: Span::dummy() }); + } + let expr_str: String = chars[start..i].iter().collect(); + let expr = parse_expression(&expr_str)?; + parts.push(FStringPart::Expr(Box::new(expr))); + i += 1; + } else if ch == '}' && i + 1 < chars.len() && chars[i + 1] == '}' { + parts.push(FStringPart::Literal("}".to_string())); + i += 2; + } else { + let start = i; + while i < chars.len() { + let c = chars[i]; + if c == '{' || c == '}' { + break; + } + i += 1; + } + let lit: String = chars[start..i].iter().collect(); + let processed = process_escapes(&lit)?; + parts.push(FStringPart::Literal(processed)); + } + } + Ok(parts) +} + +fn process_escapes(s: &str) -> Result { + let mut result = String::new(); + let mut chars = s.chars().peekable(); + while let Some(ch) = chars.next() { + if ch == '\\' { + if let Some(esc) = chars.next() { + match esc { + 'n' => result.push('\n'), + 't' => result.push('\t'), + 'r' => result.push('\r'), + '\\' => result.push('\\'), + '"' => result.push('"'), + '\'' => result.push('\''), + _ => return Err(ParseError { message: format!("invalid escape sequence: \\{}", esc), span: Span::dummy() }), + } + } else { + return Err(ParseError { message: "unfinished escape sequence".to_string(), span: Span::dummy() }); + } + } else { + result.push(ch); + } + } + Ok(result) +} + +fn parse_list_comp_generators(tokens: &[Token], pos: &mut usize, mut gens: Vec<(String, Box)>, mut filters: Vec>) -> ParseResult<(Vec<(String, Box)>, Vec>)> { + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RBracket { + match tokens[*pos].kind { + TokenKind::For => { + *pos += 1; + let var = match &tokens[*pos].kind { + TokenKind::Ident(n) => n.clone(), + _ => return Err(ParseError { message: "expected loop variable".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + }; + *pos += 1; + expect_kind(tokens, pos, TokenKind::In)?; + let iter = parse_expr(tokens, pos)?; + gens.push((var, Box::new(iter))); + } + TokenKind::If => { + *pos += 1; + let cond = parse_expr(tokens, pos)?; + filters.push(Box::new(cond)); + } + _ => break, + } + } + Ok((gens, filters)) } diff --git a/src/types.rs b/src/types.rs index 68fedc7..9b4dbc6 100644 --- a/src/types.rs +++ b/src/types.rs @@ -93,7 +93,7 @@ pub enum Value { class: String, fields: BTreeMap, }, - Break(Option>), // internal use only + Break(Option>), } #[derive(Clone, Serialize, Deserialize)] From 20c7216719ddb1177f7923daf586245e28ad2f44 Mon Sep 17 00:00:00 2001 From: one-of-all Date: Sun, 19 Jul 2026 11:53:56 +0300 Subject: [PATCH 07/18] Add files via upload --- README.md | 42 +++++++++++++++---------------- doc.md | 14 +++++------ src/builtins.rs | 2 ++ src/error.rs | 2 -- src/main.rs | 32 ++++++++++++------------ src/p2p.rs | 4 +-- src/parser.rs | 66 +++++++++++++++++++++++++++++-------------------- src/server.rs | 1 - 8 files changed, 86 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 17af9db..f0d159c 100644 --- a/README.md +++ b/README.md @@ -49,13 +49,13 @@ sudo cp target/release/plic /bin/plic plic ``` -Появится приглашение `>>>`. Вводите выражения, результаты выводятся с помощью встроенной функции `show()`. +Появится приглашение `>>>`. Вводите выражения, результаты выводятся с помощью встроенной функции `show`. Пример: ``` >>> let square x = x * x ->>> show(square 5) +>>> show $ square 5 25 >>> f"2 + 2 = {2 + 2}" "2 + 2 = 4" @@ -65,17 +65,17 @@ plic ``` >>> if x > 0: -... show("positive") +... show $ "positive" ... else: -... show("non-positive") +... show $ "non-positive" ``` ### Запуск скриптов -Передайте имя файла с расширением `.cl`: +Передайте имя файла с расширением `.plic`: ```bash -plic script.cl +plic script.plic ``` Интерпретатор выполнит скрипт и завершит работу. @@ -83,46 +83,46 @@ plic script.cl ### Встроенные команды в REPL - `exit()` – завершить сеанс. -- `load "файл.cl"` – выполнить скрипт в текущем окружении (определения сохраняются). +- `load "файл.plic"` – выполнить скрипт в текущем окружении (определения сохраняются). ## Примеры ### Математика и функции -```haskell +```plic let add a b = a + b let inc = add 1 -show(inc 10) -- 11 -show(sqrt 16) -- 4.0 +show $ inc 10 # 11 +show $sqrt 16 # 4.0 ``` ### Работа со списками и множествами -```haskell +```plic let numbers = [1, 2, 3, 4, 5] -show(map (lambda x -> x * 2) numbers) -- [2, 4, 6, 8, 10] -show(filter (lambda x -> x > 2) numbers) -- [3, 4, 5] +show $ map (lambda x -> x * 2) numbers # [2, 4, 6, 8, 10] +show $ filter (lambda x -> x > 2) numbers # [3, 4, 5] let s = %[1, 2, 3] -show(setContains s 2) -- true +show $ setContains s 2 # true ``` ### Списковые включения -```haskell -show([x * x for x in [1..10] if x % 2 == 0]) -- [4, 16, 36, 64, 100] +```plic +show $ [x * x for x in [1..10] if x % 2 == 0] # [4, 16, 36, 64, 100] ``` ### Классы и объекты -```haskell +```plic class Counter = (val = Num; inc(self) = self { val = self.val + 1 }; get(self) = self.val) let c = new Counter(0) -show(c.inc().get()) -- 1 +show $ c.inc().get() # 1 ``` ### Чат: запуск сервера и подключение -```haskell +```plic # терминал 1 (сервер) serverStart("127.0.0.1:9000", "secret") setExternalIP("203.0.113.5") # если нужен внешний IP @@ -136,12 +136,12 @@ sendChat("general", "Hello everyone!") # терминал 3 (Боб) login(@bob) connect("127.0.0.1:9000", @bob, "secret") -inbox() -- покажет сообщение от Алисы +inbox() # покажет сообщение от Алисы ``` ### Отправка файлов -```haskell +```plic sendFile(@bob, "report.txt") downloads() saveFile(0, "received.txt") diff --git a/doc.md b/doc.md index ce5b2ff..9354883 100644 --- a/doc.md +++ b/doc.md @@ -1,5 +1,3 @@ -# PLIC (ChatLang) 3.2 Документация - ## Введение PLIC — это динамический язык программирования с функциональным ядром и встроенной поддержкой P2P-чата. Он сочетает функциональную, императивную и объектно-ориентированную парадигмы. @@ -106,7 +104,7 @@ case выражение of образец1 -> выражение1; образе ### 4.4. Локальные определения - `let x = 5` – определяет новую переменную (ошибка, если уже определена). -- `let f x y = x + y` – определение функции (каррировано). +- `let f x y = x + y` – определение функции (каррировано). Запятые между параметрами **не допускаются**. - Аннотации типов: `let N :: Num = 5`, `let add :: Num -> Num -> Num = lambda a b -> a + b`. - Блоки используют отступы после `:`. @@ -270,7 +268,7 @@ data Option a = None | Some a - `parseTime :: String -> String -> DateTime` - `addDuration :: DateTime -> Duration -> DateTime` - `diffDuration :: DateTime -> DateTime -> Duration` -- `now :: DateTime` +- `now :: () -> DateTime` ### 9.7. Байтовые строки - `packBytes :: [Num] -> ByteString` @@ -278,8 +276,8 @@ data Option a = None | Some a ### 9.8. Ввод-вывод и файлы - `putStrLn :: String -> ()` – печатает строку. -- `getLine :: String` -- `getArgs :: [String]` +- `getLine :: () -> String` +- `getArgs :: () -> [String]` - `readFile :: String -> String` - `readBinaryFile :: String -> ByteString` - `writeFile :: String -> String -> ()` @@ -327,9 +325,9 @@ data Option a = None | Some a ### 9.11. Процессы - `spawn :: (() -> ()) -> Pid` -- `procSelf :: Pid` +- `procSelf :: () -> Pid` - `procSend :: Pid -> a -> ()` -- `procRecv :: a` +- `procRecv :: () -> a` - `procWait :: Pid -> a` - `procExit :: a -> ()` - `sleep :: Duration -> ()` diff --git a/src/builtins.rs b/src/builtins.rs index 34e6309..4d0952e 100644 --- a/src/builtins.rs +++ b/src/builtins.rs @@ -2431,6 +2431,7 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: }), ); + // p2pPort builtin let state_for_p2p_port = state.clone(); env.set( "p2pPort".to_string(), @@ -2440,6 +2441,7 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: }), ); + // Start P2P listener, but without printing anything. { let mut state_guard = state.lock().unwrap(); if state_guard.p2p_port == 0 { diff --git a/src/error.rs b/src/error.rs index 19bb3a6..a3a5b5d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,3 @@ -//! Error types with location support. - use crate::ast::Span; use std::fmt; diff --git a/src/main.rs b/src/main.rs index c702cae..0120d9e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,18 +20,17 @@ use codespan::Files; use codespan_reporting::diagnostic::{Diagnostic, Label}; use codespan_reporting::term::termcolor::{ColorChoice, StandardStream}; use codespan_reporting::term as term_reporting; -use std::sync::atomic; static INTERRUPTED: AtomicBool = AtomicBool::new(false); -struct ChatLangHelper { +struct PlicHelper { env: Arc>, keywords: Vec, builtins: Vec, types: Vec, } -impl Completer for ChatLangHelper { +impl Completer for PlicHelper { type Candidate = String; fn complete(&self, line: &str, pos: usize, _ctx: &rustyline::Context<'_>) -> rustyline::Result<(usize, Vec)> { @@ -60,10 +59,10 @@ impl Completer for ChatLangHelper { } } -impl Highlighter for ChatLangHelper { +impl Highlighter for PlicHelper { fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> { let mut result = String::new(); - let mut in_string = false; + let mut in_string = false; // больше не mutable let mut in_comment = false; let mut multiline_comment_depth = 0; let mut in_fstring = false; @@ -194,20 +193,26 @@ impl Highlighter for ChatLangHelper { } } -impl Hinter for ChatLangHelper { +impl Hinter for PlicHelper { type Hint = String; fn hint(&self, _line: &str, _pos: usize, _ctx: &rustyline::Context<'_>) -> Option { None } } -impl Validator for ChatLangHelper { +impl Validator for PlicHelper { fn validate(&self, _ctx: &mut ValidationContext<'_>) -> rustyline::Result { Ok(ValidationResult::Valid(None)) } } -impl Helper for ChatLangHelper {} +impl Helper for PlicHelper {} fn main() { + // Global panic handler to avoid unwrap messages. + std::panic::set_hook(Box::new(|panic_info| { + eprintln!("\x1b[31mFatal error\x1b[0m: {}", panic_info); + std::process::exit(1); + })); + let args: Vec = env::args().collect(); let state = Arc::new(Mutex::new(ChatState::new())); let mut env = Environment::new(); @@ -266,7 +271,7 @@ fn main() { return; } - let helper = ChatLangHelper { + let helper = PlicHelper { env: Arc::clone(&env_arc), keywords: vec![ "let".into(), "if".into(), "then".into(), "else".into(), @@ -343,11 +348,10 @@ fn main() { let mut rl = Editor::new().unwrap(); rl.set_helper(Some(helper)); - let _ = rl.load_history(".chatlang_history"); + let _ = rl.load_history(".plic_history"); let mut buffer = String::new(); let mut in_multiline = false; - let mut indent_level = 0; loop { let prompt = if buffer.is_empty() { @@ -375,7 +379,6 @@ fn main() { } buffer.clear(); in_multiline = false; - indent_level = 0; continue; } @@ -387,7 +390,6 @@ fn main() { if !in_multiline && trimmed.ends_with(':') { buffer.push_str(&line); in_multiline = true; - indent_level = line.chars().take_while(|c| *c == ' ').count(); continue; } @@ -409,7 +411,6 @@ fn main() { } buffer.clear(); in_multiline = false; - indent_level = 0; continue; } continue; @@ -461,12 +462,11 @@ fn main() { println!("Aborted multi-line block."); buffer.clear(); in_multiline = false; - indent_level = 0; } continue; } Err(_) => break, } } - let _ = rl.save_history(".chatlang_history"); + let _ = rl.save_history(".plic_history"); } diff --git a/src/p2p.rs b/src/p2p.rs index 9094937..6788614 100644 --- a/src/p2p.rs +++ b/src/p2p.rs @@ -40,8 +40,8 @@ static TLS_ACCEPTOR: Lazy = Lazy::new(|| { }); fn load_or_generate_identity() -> Result> { - let cert_path = "chatlang_cert.pem"; - let key_path = "chatlang_key.pem"; + let cert_path = ".plic_cert.pem"; + let key_path = ".plic_key.pem"; if !Path::new(cert_path).exists() || !Path::new(key_path).exists() { let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()])?; let cert_pem = cert.serialize_pem()?; diff --git a/src/parser.rs b/src/parser.rs index 51dd4e4..18528cc 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -25,6 +25,7 @@ impl From for ParseError { pub type ParseResult = Result; +/// Remove comments from input, handling nested `#- ... -#` blocks. pub fn strip_comments(input: &str) -> String { let mut output = String::new(); let mut chars = input.chars().peekable(); @@ -53,6 +54,7 @@ pub fn strip_comments(input: &str) -> String { } continue; } else { + // Line comment while let Some(c) = chars.next() { if c == '\n' { output.push(c); @@ -429,8 +431,8 @@ fn parse_atom(tokens: &[Token], pos: &mut usize) -> ParseResult { type_ann = Some(type_str); } + // Read parameters as a sequence of identifiers WITHOUT commas. let mut params = Vec::new(); - // No parentheses allowed; just a sequence of identifiers without commas while *pos < tokens.len() && matches!(tokens[*pos].kind, TokenKind::Ident(_)) { if let TokenKind::Ident(p) = &tokens[*pos].kind { params.push(p.clone()); @@ -584,7 +586,6 @@ fn parse_atom(tokens: &[Token], pos: &mut usize) -> ParseResult { TokenKind::Backslash | TokenKind::Lambda => { *pos += 1; let mut params = Vec::new(); - // No parentheses; just a sequence of identifiers without commas while *pos < tokens.len() && matches!(tokens[*pos].kind, TokenKind::Ident(_)) { if let TokenKind::Ident(p) = &tokens[*pos].kind { params.push(p.clone()); @@ -702,8 +703,21 @@ fn parse_atom(tokens: &[Token], pos: &mut usize) -> ParseResult { TokenKind::Loop => { *pos += 1; expect_kind(tokens, pos, TokenKind::LBrace)?; - let body = parse_expr(tokens, pos)?; + // Parse a block of expressions inside braces, separated by semicolons or newlines. + let mut exprs = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RBrace { + let e = parse_expr(tokens, pos)?; + exprs.push(e); + if *pos < tokens.len() && (tokens[*pos].kind == TokenKind::Semicolon || tokens[*pos].kind == TokenKind::NEWLINE) { + *pos += 1; + } + } let end = expect_kind(tokens, pos, TokenKind::RBrace)?; + let body = if exprs.len() == 1 { + exprs.remove(0) + } else { + Expr::Block(exprs, Span::new(start, end)) + }; Ok(Expr::Loop(Box::new(body), Span::new(start, end))) } TokenKind::Break => { @@ -879,6 +893,26 @@ fn parse_postfix(mut expr: Expr, tokens: &[Token], pos: &mut usize) -> ParseResu let index = parse_expr(tokens, pos)?; let end = expect_kind(tokens, pos, TokenKind::RBracket)?; expr = Expr::Index(Box::new(expr), Box::new(index), Span::new(start, end)); + } else if *pos < tokens.len() && tokens[*pos].kind == TokenKind::LBrace { + // Record update: expr { field = value, ... } + let start = tokens[*pos].start; + *pos += 1; + let mut updates = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RBrace { + let field = match &tokens[*pos].kind { + TokenKind::Ident(n) => n.clone(), + _ => return Err(ParseError { message: "expected field name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + }; + *pos += 1; + expect_kind(tokens, pos, TokenKind::Assign)?; + let val = parse_expr(tokens, pos)?; + updates.push((field, val)); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } + } + let end = expect_kind(tokens, pos, TokenKind::RBrace)?; + expr = Expr::RecordUpdate(Box::new(expr), updates, Span::new(start, end)); } else if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Dot { let start = tokens[*pos].start; *pos += 1; @@ -897,30 +931,8 @@ fn parse_postfix(mut expr: Expr, tokens: &[Token], pos: &mut usize) -> ParseResu let end = expect_kind(tokens, pos, TokenKind::RParen)?; expr = Expr::MethodCall(Box::new(expr), field, args, Span::new(start, end)); } else { - // Check for record update: expr { field = value } - if *pos < tokens.len() && tokens[*pos].kind == TokenKind::LBrace { - let start2 = tokens[*pos].start; - *pos += 1; - let mut updates = Vec::new(); - while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RBrace { - let f = match &tokens[*pos].kind { - TokenKind::Ident(n) => n.clone(), - _ => return Err(ParseError { message: "expected field name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), - }; - *pos += 1; - expect_kind(tokens, pos, TokenKind::Assign)?; - let val = parse_expr(tokens, pos)?; - updates.push((f, val)); - if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { - *pos += 1; - } - } - let end2 = expect_kind(tokens, pos, TokenKind::RBrace)?; - expr = Expr::RecordUpdate(Box::new(expr), updates, Span::new(start2, end2)); - } else { - let end = tokens[*pos - 1].end; - expr = Expr::FieldAccess(Box::new(expr), field, Span::new(start, end)); - } + let end = tokens[*pos - 1].end; + expr = Expr::FieldAccess(Box::new(expr), field, Span::new(start, end)); } } else { return Err(ParseError { message: "expected field or method name".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }); diff --git a/src/server.rs b/src/server.rs index 2f7f87c..5ac2ae3 100644 --- a/src/server.rs +++ b/src/server.rs @@ -16,7 +16,6 @@ pub fn start_contacts_server(addr: &str, password: Option) -> Result<(Co listener.set_nonblocking(true)?; let acceptor = crate::p2p::get_tls_acceptor(); let db_clone = db.clone(); - let addr_owned = addr.to_string(); let password_arc = Arc::new(password); let (stop_tx, stop_rx) = std::sync::mpsc::channel(); From 88054ff2b87070bdb1baee4b210f093bbf1030b4 Mon Sep 17 00:00:00 2001 From: one-of-all Date: Sun, 19 Jul 2026 11:54:08 +0300 Subject: [PATCH 08/18] Delete exmpl directory --- exmpl/advanced.plic | 54 --------------------------------- exmpl/basic.plic | 31 ------------------- exmpl/chat.plic | 35 ---------------------- exmpl/collections.plic | 68 ------------------------------------------ exmpl/concurrency.plic | 51 ------------------------------- exmpl/control.plic | 59 ------------------------------------ 6 files changed, 298 deletions(-) delete mode 100644 exmpl/advanced.plic delete mode 100644 exmpl/basic.plic delete mode 100644 exmpl/chat.plic delete mode 100644 exmpl/collections.plic delete mode 100644 exmpl/concurrency.plic delete mode 100644 exmpl/control.plic diff --git a/exmpl/advanced.plic b/exmpl/advanced.plic deleted file mode 100644 index 53aea00..0000000 --- a/exmpl/advanced.plic +++ /dev/null @@ -1,54 +0,0 @@ -# Classes -class Counter = ( - val = Num; - inc(self) = self { val = self.val + 1 }; - get(self) = self.val -) -let c = new Counter(0) -show $ c.inc().inc().get() # 2 - -# Inheritance -class AdvancedCounter extends Counter = ( - dec(self) = self { val = self.val - 1 } -) -let ac = new AdvancedCounter(10) -show $ ac.dec().get() # 9 - -# Algebraic Data Types (union) -data Option a = None | Some a -let opt = Some 42 -case opt of - Some x -> "value: " ++ show $ x - None -> "nothing" -# returns "value: 42" - -# Structs (already shown) -struct Point = (x = Num, y = Num) -let p = Point(x = 3, y = 4) -show $ p.x + p.y # 7 - -# Exceptions -try - error "oops" -catch e -> "caught: " ++ e -# returns "caught: oops" - -# List comprehensions -let nums = [1, 2, 3, 4, 5] -show $ [x * 2 for x in nums if x > 2] # [6, 8, 10] -# Multiple generators -show $ [(x, y) for x in [1,2] for y in [a,b]] # [(1,'a'), (1,'b'), (2,'a'), (2,'b')] - -# F‑strings -let name = "Alice" -let age = 30 -show $ f"Name: {name}, Age: {age}, Next year: {age + 1}" -# "Name: Alice, Age: 30, Next year: 31" - -# Del (delete variable) -let temp = 5 -del("temp") -# temp is now undefined - -# Load (import another file) -# load("other.plic") diff --git a/exmpl/basic.plic b/exmpl/basic.plic deleted file mode 100644 index ec9cedc..0000000 --- a/exmpl/basic.plic +++ /dev/null @@ -1,31 +0,0 @@ -# Simple arithmetic -show $ 1 + 2 * 3 # 7 -show $ (10 - 5) / 2 # 2.5 - -# Variables and assignments -let x = 10 -show $ x # 10 -x = x + 5 -show $ x # 15 - -# Functions (curried) -let square x = x * x -show $ square 4 # 16 - -let add a b = a + b -show $ add 3 4 # 7 - -# Lambda -let inc = lambda x -> x + 1 -show $ inc 5 # 6 - -# Type annotations -let N :: Num = 42 -show typeof N # Num -let greet :: String = "Hello" -show $ greet # Hello - -# Numeric conversion -let pi = 3.14159 -show $ toInt pi # 3 -show $ toFloat 5 # 5.0 diff --git a/exmpl/chat.plic b/exmpl/chat.plic deleted file mode 100644 index c70071b..0000000 --- a/exmpl/chat.plic +++ /dev/null @@ -1,35 +0,0 @@ -# Set external IP (change to your public IP if needed) -setExternalIP("192.168.1.100") - -# Start contact server on port 9000 with password "secret" -serverStart("0.0.0.0:9000", "secret") - -# Alice logs in and connects to the server -login(@alice) -connect("127.0.0.1:9000", @alice, "secret") - -# Create a new chat with Bob and Alice -newChat("general", [@bob, @alice]) -open("general") - -# Send a message to the chat (mention Bob) -sendChat("general", "Hello @bob, welcome to the chat!") - -# Send a private message to Bob -send(@bob, "Hi Bob, this is private.") - -# Send a file to the chat -writeFile("report.txt", "Sales data for Q1") -sendFileToChat("general", "report.txt") - -# Check inbox and history -show $ inbox() # lists all private messages for Alice -show $ history("general") # shows chat history - -# List downloads and save file -show $ downloads() # shows received file transfers -saveFile(0, "received_report.txt") - -# Stop server -serverStop() -logout() diff --git a/exmpl/collections.plic b/exmpl/collections.plic deleted file mode 100644 index 1861eb3..0000000 --- a/exmpl/collections.plic +++ /dev/null @@ -1,68 +0,0 @@ -# Lists -let nums = [1, 2, 3, 4, 5] -show $ length nums # 5 -show $ null [] # true -show $ nums[0] # 1 - -# List operations -show $ map(lambda x -> x * 2, nums) # [2, 4, 6, 8, 10] -show $ filter(lambda x -> x > 2, nums) # [3, 4, 5] -show $ foldl(lambda acc x -> acc + x, nums, 0) # 15 -show $ take 3 nums # [1, 2, 3] -show $ drop 2 nums # [3, 4, 5] -show $ reverse nums # [5, 4, 3, 2, 1] -show $ concat [[1,2], [3,4]] # [1, 2, 3, 4] -show $ zip [1,2] [a,b] # [(1, 'a'), (2, 'b')] - -# Strings -let hello = "Hello" -let world = "World" -show $ hello ++ " " ++ world # "Hello World" -show $ length hello # 5 -show $ hello[1] # 'e' -show $ split " " "Hello World" # ["Hello", "World"] -show $ join ", " ["one", "two"] # "one, two" -show $ startsWith "He" hello # true -show $ endsWith "lo" hello # true -show $ trim " abc " # "abc" -show $ replace "l" "L" hello # "HeLLo" -show $ substring 1 3 hello # "ell" - -# ByteStrings -let bytes = #B"48656C6C6F" # "Hello" -show $ bytes # #B"48656C6C6F" -show $ length bytes # 5 -show $ bytes[0] # 72 (ASCII 'H') -let packed = packBytes [72, 101, 108, 108, 111] -show $ packed # #B"48656C6C6F" -show $ unpackBytes packed # [72, 101, 108, 108, 111] - -# Tuples -let tup = (1, "two", 3.0) -show $ tup[1] # "two" -show $ length tup # 3 - -# Records (structs) -struct Person = (name = String, age = Num) -let alice = Person(name = "Alice", age = 30) -show $ alice.name # "Alice" -show $ alice.age # 30 - -# Maps -let m = %(1 => "one", 2 => "two") -show $ mapGet m 1 # "one" -let m2 = mapSet m 3 "three" -show $ m2 # %{1: one, 2: two, 3: three} -show $ mapKeys m2 # [1, 2, 3] -show $ mapValues m2 # ["one", "two", "three"] -show $ mapContains m2 2 # true -show $ mapSize m2 # 3 - -# Sets -let set = %[1, 2, 3] -show $ setAdd set 4 # %[1,2,3,4] -show $ setRemove set 2 # %[1,3] -show $ setContains set 2 # true -show $ setUnion set %[3,4,5] # %[1,2,3,4,5] -show $ setSize set # 3 -show $ listToSet [1,2,2,3] # %[1,2,3] diff --git a/exmpl/concurrency.plic b/exmpl/concurrency.plic deleted file mode 100644 index 16e9b22..0000000 --- a/exmpl/concurrency.plic +++ /dev/null @@ -1,51 +0,0 @@ -# Spawn a process that waits for a message and prints it -let p = spawn(lambda () -> { - let msg = procRecv() - show $ "Received: " ++ show $ msg - procExit(msg) -}) - -# Send a message to the process -procSend(p, "Hello from main!") - -# Wait for the process to finish and get its exit value -let result = procWait(p) -show $ "Process exited with: " ++ show $ result - -# Self PID -let self = procSelf() -show $ self # prints - -# Sleep -sleep(2s) -show $ "Slept for 2 seconds" - -# After – schedule a function to run after a delay -after(1s, lambda () -> show $ "Delayed message") - -# More complex process example with multiple messages -let p2 = spawn(lambda () -> { - loop { - let msg = procRecv() - if msg == "stop" then - break "stopped" - else - show $ "Process 2 received: " ++ show $ msg - } -}) -procSend(p2, "first") -procSend(p2, "second") -sleep(500ms) -procSend(p2, "stop") -let exit = procWait(p2) -show $ "p2 exit: " ++ show $ exit - -# Show that processes can run concurrently -let p3 = spawn(lambda () -> { - sleep(1s) - procSend(procSelf(), "done") - procRecv() # wait for ack -}) -procSend(p3, "ack") -let res = procWait(p3) -show $ "p3 result: " ++ show $ res diff --git a/exmpl/control.plic b/exmpl/control.plic deleted file mode 100644 index 944c801..0000000 --- a/exmpl/control.plic +++ /dev/null @@ -1,59 +0,0 @@ -# If‑then‑else -let age = 20 -if age >= 18 then - show $ "Adult" -else - show $ "Minor" - -# For loop over list -let sum = 0 -for x in [1, 2, 3, 4]: - sum = sum + x -show $ sum # 10 - -# For over set -let s = %[apple, banana, cherry] -for item in s: - show $ item # prints each string - -# For over map -let m = %(1 => "one", 2 => "two") -for pair in m: - show $ pair # prints (1, "one") etc. - -# While loop -let i = 0 -while i < 5: - i = i + 1 -show $ i # 5 - -# Infinite loop with break -let counter = 0 -loop { - counter = counter + 1 - if counter == 3 then - break "done" -} -show $ counter # 3 (last value before break) - -# Pattern matching (one‑line) -show $ case 2 of 1 -> "one"; 2 -> "two"; _ -> "other" # two - -# Pattern matching (multiline) -case 10 of - 0 -> "zero" - n -> "positive: " ++ show $ n -# returns "positive: 10" - -# Tuple pattern -let pair = (1, "apple") -case pair of - (1, fruit) -> "found " ++ fruit - _ -> "other" -# returns "found apple" - -# List pattern -case [1, 2, 3] of - [x, y, z] -> x + y + z - _ -> 0 -# returns 6 From cc5e155f8ffc09cdfcd8f93b67b549976f21c1c1 Mon Sep 17 00:00:00 2001 From: one-of-all Date: Sun, 19 Jul 2026 11:56:45 +0300 Subject: [PATCH 09/18] Add files via upload --- hl/vsc/plic.tmLanguage.json | 298 +++++++++++++----------------------- src/main.rs | 20 +-- src/parser.rs | 11 +- 3 files changed, 117 insertions(+), 212 deletions(-) diff --git a/hl/vsc/plic.tmLanguage.json b/hl/vsc/plic.tmLanguage.json index cc747d2..7d9c0c2 100644 --- a/hl/vsc/plic.tmLanguage.json +++ b/hl/vsc/plic.tmLanguage.json @@ -1,213 +1,121 @@ { - "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", - "name": "plic", - "scopeName": "source.plic", - "fileTypes": ["cl", "plic"], - "patterns": [ - { - "include": "#comments" - }, - { - "include": "#strings" - }, - { - "include": "#numbers" - }, - { - "include": "#uid" - }, - { - "include": "#keywords" - }, - { - "include": "#types" - }, - { - "include": "#operators" - }, - { - "include": "#identifiers" - } - ], - "repository": { - "comments": { - "patterns": [ - { - "name": "comment.line.number-sign.plic", - "match": "#[^B-].*$" - }, - { - "begin": "#-", - "end": "-#", - "name": "comment.block.plic", - "patterns": [ - { - "include": "#comments" - } - ] - } - ] - }, - "strings": { - "patterns": [ - { - "name": "string.quoted.double.plic", - "begin": "\"", - "end": "\"", - "patterns": [ - { - "name": "constant.character.escape.plic", - "match": "\\\\(n|t|r|\\\\|\"|')" - } - ] - }, - { - "name": "string.quoted.single.plic", - "begin": "'", - "end": "'", - "patterns": [ - { - "name": "constant.character.escape.plic", - "match": "\\\\(n|t|r|\\\\|'|\")" - } - ] - }, - { - "name": "string.quoted.double.fstring.plic", - "begin": "f\"", - "end": "\"", - "patterns": [ - { - "include": "#fstring_expression" - }, - { - "name": "constant.character.escape.plic", - "match": "\\\\(n|t|r|\\\\|\"|')" - } - ] - }, - { - "name": "string.quoted.double.bytestring.plic", - "begin": "#B\"", - "end": "\"", - "patterns": [ - { - "name": "constant.character.hex.plic", - "match": "[0-9A-Fa-f]{2}" - } - ] - } - ] - }, - "fstring_expression": { - "patterns": [ - { - "begin": "{", - "end": "}", - "name": "meta.embedded.expression.plic", - "patterns": [ - { - "include": "$self" - } - ] - } - ] - }, - "numbers": { - "patterns": [ - { - "name": "constant.numeric.integer.plic", - "match": "\\b\\d+\\b" - }, - { - "name": "constant.numeric.float.plic", - "match": "\\b\\d+\\.\\d+\\b" - }, - { - "name": "constant.numeric.duration.plic", - "match": "\\b\\d+(\\.\\d+)?[smMh]\\b" - } - ] - }, - "uid": { - "name": "constant.other.uid.plic", - "match": "@[A-Za-z_][A-Za-z0-9_]*" - }, - "keywords": { - "patterns": [ - { - "name": "keyword.control.plic", - "match": "\\b(if|then|else|case|of|try|catch|error|for|while|lambda|and|or|not|class|extends|new|let|in|data|struct|loop|break|load|del)\\b" - }, - { - "name": "keyword.other.plic", - "match": "\\b(addContact|removeContact|newChat|addMember|removeMember|deleteChat|sendChat|sendFileToChat|inbox|history|downloads|saveFile|serverStart|serverStop|connect|getPublicIP|setExternalIP|toFloat|toInt|typeof)\\b" - } - ] - }, - "types": { - "patterns": [ - { - "name": "storage.type.plic", - "match": "\\b(Int|Float|Char|String|Bool|Unit|Uid|ByteString|Duration|List|Tuple|Record|Pid|DateTime|Json|Maybe|Either|ChatMsg|FileInfo|FileTransfer|FetchOptions|FetchResult|Map|Set|ClassInstance)\\b" - } - ] - }, - "operators": { - "patterns": [ + "scopeName": "source.plic", + "patterns": [ { - "name": "keyword.operator.arithmetic.plic", - "match": "[-+*/%]" + "include": "#keywords" }, { - "name": "keyword.operator.comparison.plic", - "match": "==|!=|<=|>=|<|>" + "include": "#builtins" }, { - "name": "keyword.operator.logical.plic", - "match": "&&|\\|\\||!" + "include": "#types" }, { - "name": "keyword.operator.pipe.plic", - "match": "\\|>" + "include": "#strings" }, { - "name": "keyword.operator.dollar.plic", - "match": "\\$" + "include": "#fstrings" }, { - "name": "keyword.operator.cons.plic", - "match": ":" + "include": "#numbers" }, { - "name": "keyword.operator.concat.plic", - "match": "\\+\\+" - }, - { - "name": "keyword.operator.assignment.plic", - "match": "=" - }, - { - "name": "keyword.operator.member.plic", - "match": "\\." - }, - { - "name": "keyword.operator.other.plic", - "match": "->|=>|::|;|,|\\(|\\)|\\[|\\]|\\{|\\}" + "include": "#comments" } - ] - }, - "identifiers": { - "patterns": [ - { - "name": "entity.name.function.plic", - "match": "\\b[A-Za-z_][A-Za-z0-9_]*\\b(?=\\s*\\()" - }, - { - "name": "variable.other.plic", - "match": "\\b[A-Za-z_][A-Za-z0-9_]*\\b" + ], + "repository": { + "keywords": { + "patterns": [ + { + "match": "\\b(let|if|then|else|case|of|lambda|data|struct|try|catch|error|for|while|in|and|or|not|class|extends|new|loop|break|load|del)\\b", + "name": "keyword.control.plic" + } + ] + }, + "builtins": { + "patterns": [ + { + "match": "\\b(sqrt|sin|cos|tan|asin|acos|atan|toFloat|toInt|show|parseInt|parseFloat|chr|ord|null|length|map|filter|foldl|foldr|take|drop|reverse|all|any|find|sort|sortBy|sum|concat|flatten|zip|zipWith|unzip|indexOf|lastIndexOf|split|join|startsWith|endsWith|trim|replace|substring|parseJson|encodeJson|lookup|formatTime|parseTime|addDuration|diffDuration|packBytes|unpackBytes|putStrLn|getLine|getArgs|readFile|readBinaryFile|writeFile|appendFile|writeBinaryFile|fileExists|fileSize|fetch|fetchOpts|login|logout|deleteUser|newChat|addMember|removeMember|deleteChat|open|send|sendFile|sendChat|sendFileToChat|inbox|history|downloads|saveFile|serverStart|serverStop|connect|getPublicIP|setExternalIP|spawn|procSelf|procSend|procRecv|procWait|procExit|sleep|after|Nothing|Just|maybe|mapGet|mapSet|mapRemove|mapKeys|mapValues|mapEntries|mapContains|mapSize|mapFilter|mapMerge|setAdd|setRemove|setContains|setUnion|setIntersection|setDifference|setSize|setFilter|setMap|listToSet|mapToList|sha256|sha256String|kyberKeyPair|kyberEncapsulate|kyberDecapsulate|listDir|createDir|removeDir|fileMove|filePermissions|setFilePermissions|typeof|exit|p2pPort)\\b", + "name": "support.function.plic" + } + ] + }, + "types": { + "patterns": [ + { + "match": "\\b(Num|Char|String|Bool|Unit|Uid|ByteString|List|Tuple|Record|Pid|DateTime|Duration|Json|Maybe|Either|ChatMsg|FileInfo|FileTransfer|FetchOptions|FetchResult|Map|Set|ClassInstance)\\b", + "name": "storage.type.plic" + } + ] + }, + "strings": { + "patterns": [ + { + "begin": "\"", + "end": "\"", + "name": "string.quoted.double.plic", + "patterns": [ + { + "match": "\\\\(n|t|r|\\\\|\"|')", + "name": "constant.character.escape.plic" + } + ] + } + ] + }, + "fstrings": { + "patterns": [ + { + "begin": "f\"", + "end": "\"", + "name": "string.quoted.double.fstring.plic", + "patterns": [ + { + "begin": "{", + "end": "}", + "name": "meta.embedded.expression.plic", + "patterns": [ + { + "include": "#keywords" + }, + { + "include": "#builtins" + }, + { + "include": "#types" + }, + { + "include": "#strings" + }, + { + "include": "#numbers" + } + ] + } + ] + } + ] + }, + "numbers": { + "patterns": [ + { + "match": "\\b[0-9]+(\\.[0-9]+)?\\b", + "name": "constant.numeric.plic" + } + ] + }, + "comments": { + "patterns": [ + { + "begin": "#-", + "end": "-#", + "name": "comment.block.plic" + }, + { + "match": "#.*$", + "name": "comment.line.plic" + } + ] } - ] } - } } diff --git a/src/main.rs b/src/main.rs index 0120d9e..b71ada8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,14 +23,14 @@ use codespan_reporting::term as term_reporting; static INTERRUPTED: AtomicBool = AtomicBool::new(false); -struct PlicHelper { +struct ChatLangHelper { env: Arc>, keywords: Vec, builtins: Vec, types: Vec, } -impl Completer for PlicHelper { +impl Completer for ChatLangHelper { type Candidate = String; fn complete(&self, line: &str, pos: usize, _ctx: &rustyline::Context<'_>) -> rustyline::Result<(usize, Vec)> { @@ -59,10 +59,10 @@ impl Completer for PlicHelper { } } -impl Highlighter for PlicHelper { +impl Highlighter for ChatLangHelper { fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> { let mut result = String::new(); - let mut in_string = false; // больше не mutable + let in_string = false; let mut in_comment = false; let mut multiline_comment_depth = 0; let mut in_fstring = false; @@ -193,18 +193,18 @@ impl Highlighter for PlicHelper { } } -impl Hinter for PlicHelper { +impl Hinter for ChatLangHelper { type Hint = String; fn hint(&self, _line: &str, _pos: usize, _ctx: &rustyline::Context<'_>) -> Option { None } } -impl Validator for PlicHelper { +impl Validator for ChatLangHelper { fn validate(&self, _ctx: &mut ValidationContext<'_>) -> rustyline::Result { Ok(ValidationResult::Valid(None)) } } -impl Helper for PlicHelper {} +impl Helper for ChatLangHelper {} fn main() { // Global panic handler to avoid unwrap messages. @@ -271,7 +271,7 @@ fn main() { return; } - let helper = PlicHelper { + let helper = ChatLangHelper { env: Arc::clone(&env_arc), keywords: vec![ "let".into(), "if".into(), "then".into(), "else".into(), @@ -348,7 +348,7 @@ fn main() { let mut rl = Editor::new().unwrap(); rl.set_helper(Some(helper)); - let _ = rl.load_history(".plic_history"); + let _ = rl.load_history(".chatlang_history"); let mut buffer = String::new(); let mut in_multiline = false; @@ -468,5 +468,5 @@ fn main() { Err(_) => break, } } - let _ = rl.save_history(".plic_history"); + let _ = rl.save_history(".chatlang_history"); } diff --git a/src/parser.rs b/src/parser.rs index 18528cc..607ab7e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -431,7 +431,6 @@ fn parse_atom(tokens: &[Token], pos: &mut usize) -> ParseResult { type_ann = Some(type_str); } - // Read parameters as a sequence of identifiers WITHOUT commas. let mut params = Vec::new(); while *pos < tokens.len() && matches!(tokens[*pos].kind, TokenKind::Ident(_)) { if let TokenKind::Ident(p) = &tokens[*pos].kind { @@ -628,8 +627,8 @@ fn parse_atom(tokens: &[Token], pos: &mut usize) -> ParseResult { break; } } - let end = expect_kind(tokens, pos, TokenKind::RParen)?; - Ok(Expr::StructDef(name, fields, Span::new(start, end))) + let _end = expect_kind(tokens, pos, TokenKind::RParen)?; + Ok(Expr::StructDef(name, fields, Span::new(start, _end))) } TokenKind::Data => { *pos += 1; @@ -673,8 +672,8 @@ fn parse_atom(tokens: &[Token], pos: &mut usize) -> ParseResult { break; } } - let end = tokens[*pos - 1].end; - Ok(Expr::DataDef(name, params, constructors, Span::new(start, end))) + let _end = tokens[*pos - 1].end; + Ok(Expr::DataDef(name, params, constructors, Span::new(start, _end))) } TokenKind::For => { *pos += 1; @@ -703,7 +702,6 @@ fn parse_atom(tokens: &[Token], pos: &mut usize) -> ParseResult { TokenKind::Loop => { *pos += 1; expect_kind(tokens, pos, TokenKind::LBrace)?; - // Parse a block of expressions inside braces, separated by semicolons or newlines. let mut exprs = Vec::new(); while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RBrace { let e = parse_expr(tokens, pos)?; @@ -894,7 +892,6 @@ fn parse_postfix(mut expr: Expr, tokens: &[Token], pos: &mut usize) -> ParseResu let end = expect_kind(tokens, pos, TokenKind::RBracket)?; expr = Expr::Index(Box::new(expr), Box::new(index), Span::new(start, end)); } else if *pos < tokens.len() && tokens[*pos].kind == TokenKind::LBrace { - // Record update: expr { field = value, ... } let start = tokens[*pos].start; *pos += 1; let mut updates = Vec::new(); From c07d6c9b4691203b4018d5113d49f2a15ef43f64 Mon Sep 17 00:00:00 2001 From: one-of-all Date: Sun, 19 Jul 2026 14:15:24 +0300 Subject: [PATCH 10/18] Create install_debug.sh --- install_debug.sh | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 install_debug.sh diff --git a/install_debug.sh b/install_debug.sh new file mode 100644 index 0000000..fd3721f --- /dev/null +++ b/install_debug.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +set -e + +echo "Building PLIC in debug mode..." +cargo build + +BIN_SRC="target/debug/plic" +BIN_DEST="/bin/plic" + +if [ ! -f "$BIN_SRC" ]; then + echo "Error: build failed, binary not found at $BIN_SRC" + exit 1 +fi + +echo "Installing binary to $BIN_DEST (requires sudo)..." +sudo cp "$BIN_SRC" "$BIN_DEST" +sudo chmod 755 "$BIN_DEST" + +echo "Installation complete! You can now run 'plic'." From 1ca0250f60a1def8e7c7e032dc8fbed1350cd616 Mon Sep 17 00:00:00 2001 From: one-of-all Date: Sun, 19 Jul 2026 14:27:41 +0300 Subject: [PATCH 11/18] Update README.md --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index f0d159c..84c251c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # PLIC +## Примечание + +Язык создовался в сильной спешке, и неуоторые вещи не были отпалерованны. В случае если установка через `install.sh`, используйт скрипт `install_debug.sh` + +## Введение + **PLIC** — это динамический язык программирования, сочетающий функциональное, императивное и объектно‑ориентированное программирование. Встроенная поддержка P2P‑сообщений и файлового обмена позволяет использовать его как платформу для децентрализованных чат‑приложений. ## Особенности From a856b07f2d51615903914ef188dde143ec10fc59 Mon Sep 17 00:00:00 2001 From: one-of-all Date: Sun, 19 Jul 2026 14:29:59 +0300 Subject: [PATCH 12/18] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 84c251c..9f0093f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Примечание -Язык создовался в сильной спешке, и неуоторые вещи не были отпалерованны. В случае если установка через `install.sh`, используйт скрипт `install_debug.sh` +Язык создовался в сильной спешке, и некоторые вещи не были отпалерованны. В случае если установка через `install.sh` провалилась, то используйте скрипт `install_debug.sh` ## Введение From bf76bf1ffb29d52ed2c776cb59f15d1eb07ff579 Mon Sep 17 00:00:00 2001 From: one-of-all Date: Sun, 19 Jul 2026 19:58:59 +0300 Subject: [PATCH 13/18] Update doc.md --- doc.md | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/doc.md b/doc.md index 9354883..d21edae 100644 --- a/doc.md +++ b/doc.md @@ -505,33 +505,23 @@ Alice **Установка внешнего IP и запуск сервера с паролем:** ``` >>> setExternalIP "203.0.113.5" -() >>> serverStart "0.0.0.0:9000" "secret" -() ``` **Алиса:** ``` >>> login @alice -() >>> connect "127.0.0.1:9000" @alice "secret" -1 >>> newChat "general" [@bob, @alice] -general >>> open "general" -() >>> sendChat "general" "Hello everyone!" -true >>> send @bob "Hello Bob!" -true ``` **Боб (другой экземпляр):** ``` >>> login @bob -() >>> connect "127.0.0.1:9000" @bob "secret" -1 >>> show inbox [[Message from @alice in general: "Hello everyone!"], [Message from @alice: "Hello Bob!"]] >>> show $ history "general" @@ -541,22 +531,17 @@ true **Передача файла:** ``` >>> writeFile "report.txt" "Sales data" -() >>> sendFileToChat "general" "report.txt" -true >>> show downloads [[FileTransfer from @alice: report.txt]] >>> saveFile 0 "received_report.txt" -true ``` ### 10.14. Процессы ``` >>> let p = spawn lambda () -> (procRecv |> show) >>> procSend p "Hi" -() >>> sleep 1s -() ``` ### 10.15. Криптография @@ -574,7 +559,7 @@ true ## 11. Технические детали P2P - Протокол: JSON-строки поверх TLS. -- Сертификаты генерируются автоматически (`chatlang_cert.pem`, `chatlang_key.pem`). +- Сертификаты генерируются автоматически (`plic_cert.pem`, `plic_key.pem`). - P2P-порт: 19000 (можно изменить с помощью `--p2p-port`). - Порт сервера контактов: настраивается (например, 9000). - Внешний IP можно задать вручную или получить через `getPublicIP()`. From c8ea517662ec037d06745dd8931f48ca2855935f Mon Sep 17 00:00:00 2001 From: one-of-all Date: Sun, 19 Jul 2026 20:01:45 +0300 Subject: [PATCH 14/18] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9f0093f..576ab2c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ ## Примечание -Язык создовался в сильной спешке, и некоторые вещи не были отпалерованны. В случае если установка через `install.sh` провалилась, то используйте скрипт `install_debug.sh` +Язык создовался в сильной спешке, и некоторые вещи не были отпалерованны. В случае если установка через `install.sh` провалилась, используйте скрипт `install_debug.sh` + +Прошу прощение за то что документация в некоторых моментах может не соответствовать финальной версии языка, он прошел не мало изменений и у меня не всегда хватало времени потдерживать документацию в актуальном состоянии. ## Введение From 7a1fc79c8d0ff9256284b1e60b258f9e5a839175 Mon Sep 17 00:00:00 2001 From: one-of-all Date: Sun, 19 Jul 2026 20:13:27 +0300 Subject: [PATCH 15/18] Update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 576ab2c..342354d 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,7 @@ ## Примечание -Язык создовался в сильной спешке, и некоторые вещи не были отпалерованны. В случае если установка через `install.sh` провалилась, используйте скрипт `install_debug.sh` - -Прошу прощение за то что документация в некоторых моментах может не соответствовать финальной версии языка, он прошел не мало изменений и у меня не всегда хватало времени потдерживать документацию в актуальном состоянии. +Язык создовался в сильной спешке, и некоторые вещи не были отполированны. В случае если установка через `install.sh` провалилась, используйте скрипт `install_debug.sh` ## Введение From d5ff45b2501583e32a2fad94bc269a3a13c6ba38 Mon Sep 17 00:00:00 2001 From: one-of-all Date: Mon, 20 Jul 2026 09:35:06 +0300 Subject: [PATCH 16/18] Add files via upload --- doc.md | 23 +++- src/ast.rs | 11 +- src/builtins.rs | 185 +++++++++++------------------ src/eval.rs | 307 ++++++++++++++++++++++++++++++++++++++++-------- src/lexer.rs | 6 +- src/main.rs | 296 +++++++++++++++++++++++++++++----------------- src/parser.rs | 69 +++++++---- src/types.rs | 38 ++---- 8 files changed, 592 insertions(+), 343 deletions(-) diff --git a/doc.md b/doc.md index d21edae..6fdf96b 100644 --- a/doc.md +++ b/doc.md @@ -11,7 +11,6 @@ PLIC — это динамический язык программировани - **Упоминания** – `@uid` в сообщениях доставляет личную копию. - **Вывод ошибок** – файл, строка, столбец, фрагмент кода. - **F-строки** – `f"текст {выражение} текст"` с подсветкой в REPL. -- **`del`** – удаление переменной. - **`load`** – импорт файлов. - **Устойчивость к панике** – нет `unwrap`; ошибки обрабатываются. @@ -81,7 +80,7 @@ Hello, World! ### 4.1. Основы - Литералы, переменные. - Применение функции каррировано и левоассоциативно: `f a b c` = `(((f a) b) c)`. -- Лямбда: `lambda x y -> x + y` или `\ x -> x * 2`. +- Лямбда: `lambda x y -> x + y`. - Индексация: `expr[index]` работает со списками, строками, байтовыми строками, кортежами, map (возвращает значение или `Unit`), set (возвращает `Bool`). ### 4.2. Условный оператор @@ -217,7 +216,6 @@ data Option a = None | Some a ### 9.1. Математика - `sqrt`, `sin`, `cos`, `tan`, `asin`, `acos`, `atan :: Num -> Num` -- `toFloat :: Num -> Num`, `toInt :: Num -> Num` ### 9.2. Преобразования и интроспекция - `show :: a -> ()` – печатает значение и возвращает `()`. @@ -352,7 +350,6 @@ data Option a = None | Some a - `kyberDecapsulate :: SecretKey -> Ciphertext -> SharedSecret` ### 9.15. Управление переменными -- `del :: String -> ()` – удаляет переменную. - `load :: String -> ()` – загружает и выполняет файл. ### 9.16. P2P @@ -395,6 +392,7 @@ error: variable 'x' already defined a >>> show '\n' + >>> let msg = "Hello" >>> show msg Hello @@ -505,23 +503,33 @@ Alice **Установка внешнего IP и запуск сервера с паролем:** ``` >>> setExternalIP "203.0.113.5" +() >>> serverStart "0.0.0.0:9000" "secret" +() ``` **Алиса:** ``` >>> login @alice +() >>> connect "127.0.0.1:9000" @alice "secret" +1 >>> newChat "general" [@bob, @alice] +general >>> open "general" +() >>> sendChat "general" "Hello everyone!" +true >>> send @bob "Hello Bob!" +true ``` **Боб (другой экземпляр):** ``` >>> login @bob +() >>> connect "127.0.0.1:9000" @bob "secret" +1 >>> show inbox [[Message from @alice in general: "Hello everyone!"], [Message from @alice: "Hello Bob!"]] >>> show $ history "general" @@ -531,15 +539,18 @@ Alice **Передача файла:** ``` >>> writeFile "report.txt" "Sales data" +() >>> sendFileToChat "general" "report.txt" +true >>> show downloads [[FileTransfer from @alice: report.txt]] >>> saveFile 0 "received_report.txt" +true ``` ### 10.14. Процессы ``` ->>> let p = spawn lambda () -> (procRecv |> show) +>>> let p = spawn lambda -> (procRecv |> show) >>> procSend p "Hi" >>> sleep 1s ``` @@ -559,7 +570,7 @@ true ## 11. Технические детали P2P - Протокол: JSON-строки поверх TLS. -- Сертификаты генерируются автоматически (`plic_cert.pem`, `plic_key.pem`). +- Сертификаты генерируются автоматически (`chatlang_cert.pem`, `chatlang_key.pem`). - P2P-порт: 19000 (можно изменить с помощью `--p2p-port`). - Порт сервера контактов: настраивается (например, 9000). - Внешний IP можно задать вручную или получить через `getPublicIP()`. diff --git a/src/ast.rs b/src/ast.rs index a4ae743..ab0be2e 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -1,6 +1,3 @@ -//! Abstract Syntax Tree with source spans. - -/// Source location (byte offset from start of file). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Span { pub start: usize, @@ -18,8 +15,8 @@ impl Span { #[derive(Debug, Clone, PartialEq)] pub enum Literal { - Int(i64), - Float(f64), + Int(i64), // will become Num(f64) after parsing + Float(f64), // also Num Char(char), String(String), Bool(bool), @@ -114,6 +111,10 @@ pub enum Expr { filters: Vec>, span: Span, }, + // New: type cast expression + Cast(Box, String, Span), + // New: super method call + SuperMethod { method: String, args: Vec, span: Span }, } #[derive(Debug, Clone, PartialEq)] diff --git a/src/builtins.rs b/src/builtins.rs index 4d0952e..dcd9412 100644 --- a/src/builtins.rs +++ b/src/builtins.rs @@ -5,7 +5,7 @@ use crate::eval::{ }; use crate::p2p::{self, P2pMessage}; use crate::server; -use crate::types::{Environment, Value, JsonValue, FetchResult, Number}; +use crate::types::{Environment, Value, JsonValue, FetchResult}; use base64::engine::general_purpose::STANDARD; use base64::Engine; use chrono::{DateTime, Local}; @@ -30,9 +30,8 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: env.set( "sqrt".to_string(), builtin!("sqrt", 1, |args| { - if let [Value::Num(n)] = &args[..] { - let x = n.to_f64(); - Ok(Value::Num(Number::Float(x.sqrt()))) + if let [Value::Num(x)] = &args[..] { + Ok(Value::Num(x.sqrt())) } else { Err(ChatError::new("sqrt expects a Num argument", 1)) } @@ -42,9 +41,8 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: env.set( "sin".to_string(), builtin!("sin", 1, |args| { - if let [Value::Num(n)] = &args[..] { - let x = n.to_f64(); - Ok(Value::Num(Number::Float(x.sin()))) + if let [Value::Num(x)] = &args[..] { + Ok(Value::Num(x.sin())) } else { Err(ChatError::new("sin expects a Num argument", 1)) } @@ -54,9 +52,8 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: env.set( "cos".to_string(), builtin!("cos", 1, |args| { - if let [Value::Num(n)] = &args[..] { - let x = n.to_f64(); - Ok(Value::Num(Number::Float(x.cos()))) + if let [Value::Num(x)] = &args[..] { + Ok(Value::Num(x.cos())) } else { Err(ChatError::new("cos expects a Num argument", 1)) } @@ -66,9 +63,8 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: env.set( "tan".to_string(), builtin!("tan", 1, |args| { - if let [Value::Num(n)] = &args[..] { - let x = n.to_f64(); - Ok(Value::Num(Number::Float(x.tan()))) + if let [Value::Num(x)] = &args[..] { + Ok(Value::Num(x.tan())) } else { Err(ChatError::new("tan expects a Num argument", 1)) } @@ -78,9 +74,8 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: env.set( "asin".to_string(), builtin!("asin", 1, |args| { - if let [Value::Num(n)] = &args[..] { - let x = n.to_f64(); - Ok(Value::Num(Number::Float(x.asin()))) + if let [Value::Num(x)] = &args[..] { + Ok(Value::Num(x.asin())) } else { Err(ChatError::new("asin expects a Num argument", 1)) } @@ -90,9 +85,8 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: env.set( "acos".to_string(), builtin!("acos", 1, |args| { - if let [Value::Num(n)] = &args[..] { - let x = n.to_f64(); - Ok(Value::Num(Number::Float(x.acos()))) + if let [Value::Num(x)] = &args[..] { + Ok(Value::Num(x.acos())) } else { Err(ChatError::new("acos expects a Num argument", 1)) } @@ -102,36 +96,15 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: env.set( "atan".to_string(), builtin!("atan", 1, |args| { - if let [Value::Num(n)] = &args[..] { - let x = n.to_f64(); - Ok(Value::Num(Number::Float(x.atan()))) + if let [Value::Num(x)] = &args[..] { + Ok(Value::Num(x.atan())) } else { Err(ChatError::new("atan expects a Num argument", 1)) } }), ); - env.set( - "toFloat".to_string(), - builtin!("toFloat", 1, |args| { - if let [Value::Num(n)] = &args[..] { - Ok(Value::Num(Number::Float(n.to_f64()))) - } else { - Err(ChatError::new("toFloat expects a Num argument", 1)) - } - }), - ); - - env.set( - "toInt".to_string(), - builtin!("toInt", 1, |args| { - if let [Value::Num(n)] = &args[..] { - Ok(Value::Num(Number::Int(n.to_i64()))) - } else { - Err(ChatError::new("toInt expects a Num argument", 1)) - } - }), - ); + // Removed toFloat and toInt. env.set( "show".to_string(), @@ -150,7 +123,7 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: builtin!("parseInt", 1, |args| { if let [Value::String(s)] = &args[..] { s.parse::() - .map(|i| Value::Num(Number::Int(i))) + .map(|i| Value::Num(i as f64)) .map_err(|_| ChatError::new("parseInt: invalid integer string", 1)) } else { Err(ChatError::new("parseInt expects a String argument", 1)) @@ -163,7 +136,7 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: builtin!("parseFloat", 1, |args| { if let [Value::String(s)] = &args[..] { s.parse::() - .map(|f| Value::Num(Number::Float(f))) + .map(Value::Num) .map_err(|_| ChatError::new("parseFloat: invalid float string", 1)) } else { Err(ChatError::new("parseFloat expects a String argument", 1)) @@ -174,14 +147,15 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: env.set( "chr".to_string(), builtin!("chr", 1, |args| { - if let [Value::Num(Number::Int(i))] = &args[..] { - if let Some(c) = std::char::from_u32(*i as u32) { + if let [Value::Num(x)] = &args[..] { + let i = *x as u32; + if let Some(c) = std::char::from_u32(i) { Ok(Value::Char(c)) } else { Err(ChatError::new("chr: invalid Unicode code point", 1)) } } else { - Err(ChatError::new("chr expects an Int (Num) argument", 1)) + Err(ChatError::new("chr expects a Num argument", 1)) } }), ); @@ -190,7 +164,7 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: "ord".to_string(), builtin!("ord", 1, |args| { if let [Value::Char(c)] = &args[..] { - Ok(Value::Num(Number::Int(*c as i64))) + Ok(Value::Num(*c as u32 as f64)) } else { Err(ChatError::new("ord expects a Char argument", 1)) } @@ -224,12 +198,12 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: builtin!("length", 1, |args| { if let [v] = &args[..] { match v { - Value::List(l) => Ok(Value::Num(Number::Int(l.len() as i64))), - Value::String(s) => Ok(Value::Num(Number::Int(s.len() as i64))), - Value::ByteString(b) => Ok(Value::Num(Number::Int(b.len() as i64))), - Value::Map(m) => Ok(Value::Num(Number::Int(m.len() as i64))), - Value::Set(s) => Ok(Value::Num(Number::Int(s.len() as i64))), - Value::Tuple(t) => Ok(Value::Num(Number::Int(t.len() as i64))), + Value::List(l) => Ok(Value::Num(l.len() as f64)), + Value::String(s) => Ok(Value::Num(s.len() as f64)), + Value::ByteString(b) => Ok(Value::Num(b.len() as f64)), + Value::Map(m) => Ok(Value::Num(m.len() as f64)), + Value::Set(s) => Ok(Value::Num(s.len() as f64)), + Value::Tuple(t) => Ok(Value::Num(t.len() as f64)), _ => Err(ChatError::new( "length expects a List, String, ByteString, Map, Set, or Tuple", 1, @@ -335,7 +309,7 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: env.set( "take".to_string(), builtin!("take", 2, |args| { - if let [Value::Num(Number::Int(n)), v] = &args[..] { + if let [Value::Num(n), v] = &args[..] { let n = *n as usize; match v { Value::List(l) => { @@ -357,7 +331,7 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: } } else { Err(ChatError::new( - "take expects an Int (Num) and a collection", + "take expects a Num (integer) and a collection", 1, )) } @@ -367,7 +341,7 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: env.set( "drop".to_string(), builtin!("drop", 2, |args| { - if let [Value::Num(Number::Int(n)), v] = &args[..] { + if let [Value::Num(n), v] = &args[..] { let n = *n as usize; match v { Value::List(l) => { @@ -395,7 +369,7 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: } } else { Err(ChatError::new( - "drop expects an Int (Num) and a collection", + "drop expects a Num (integer) and a collection", 1, )) } @@ -520,18 +494,9 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: env.set(params[0].clone(), a.clone()); env.set(params[1].clone(), b.clone()); let result = - eval::eval_expr(&body, &mut env, Arc::clone(&state_sortby)).unwrap_or(Value::Num(Number::Int(0))); + eval::eval_expr(&body, &mut env, Arc::clone(&state_sortby)).unwrap_or(Value::Num(0.0)); match result { - Value::Num(Number::Int(i)) => i.cmp(&0), - Value::Num(Number::Float(f)) => { - if f < 0.0 { - std::cmp::Ordering::Less - } else if f > 0.0 { - std::cmp::Ordering::Greater - } else { - std::cmp::Ordering::Equal - } - } + Value::Num(x) => x.partial_cmp(&0.0).unwrap_or(std::cmp::Ordering::Equal), _ => std::cmp::Ordering::Equal, } }); @@ -552,12 +517,11 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: let mut total = 0.0; for item in list { match item { - Value::Num(Number::Int(i)) => total += *i as f64, - Value::Num(Number::Float(f)) => total += f, + Value::Num(x) => total += x, _ => return Err(ChatError::new("sum expects a list of numbers", 1)), } } - Ok(Value::Num(Number::Float(total))) + Ok(Value::Num(total)) } else { Err(ChatError::new("sum expects a List argument", 1)) } @@ -683,7 +647,7 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: if let [Value::List(list), elem] = &args[..] { for (i, item) in list.iter().enumerate() { if item.display() == elem.display() { - return Ok(Value::Maybe(Some(Box::new(Value::Num(Number::Int(i as i64)))))); + return Ok(Value::Maybe(Some(Box::new(Value::Num(i as f64))))); } } Ok(Value::Maybe(None)) @@ -699,7 +663,7 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: if let [Value::List(list), elem] = &args[..] { for i in (0..list.len()).rev() { if list[i].display() == elem.display() { - return Ok(Value::Maybe(Some(Box::new(Value::Num(Number::Int(i as i64)))))); + return Ok(Value::Maybe(Some(Box::new(Value::Num(i as f64))))); } } Ok(Value::Maybe(None)) @@ -797,7 +761,7 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: env.set( "substring".to_string(), builtin!("substring", 3, |args| { - if let [Value::Num(Number::Int(start)), Value::Num(Number::Int(len)), Value::String(s)] = &args[..] { + if let [Value::Num(start), Value::Num(len), Value::String(s)] = &args[..] { let start = *start as usize; let len = *len as usize; if start + len > s.len() { @@ -807,7 +771,7 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: } } else { Err(ChatError::new( - "substring expects two Int (Num) arguments and a String", + "substring expects two Num arguments and a String", 1, )) } @@ -820,7 +784,7 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: if let [Value::String(s)] = &args[..] { let v: SerdeValue = serde_json::from_str(s).map_err(|e| ChatError::new(&format!("parseJson: {}", e), 1))?; - Ok(Value::Json(serde_json_to_chatlang(v))) + Ok(Value::Json(serde_json_to_plic(v))) } else { Err(ChatError::new("parseJson expects a String argument", 1)) } @@ -831,7 +795,7 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: "encodeJson".to_string(), builtin!("encodeJson", 1, |args| { if let [Value::Json(j)] = &args[..] { - let serde_val = chatlang_json_to_serde(j); + let serde_val = plic_json_to_serde(j); let json_str = serde_json::to_string(&serde_val) .map_err(|e| ChatError::new(&format!("encodeJson: {}", e), 1))?; Ok(Value::String(json_str)) @@ -939,14 +903,15 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: if let [Value::List(list)] = &args[..] { let mut bytes = Vec::new(); for item in list { - if let Value::Num(Number::Int(i)) = item { - if *i < 0 || *i > 255 { + if let Value::Num(x) = item { + let i = *x as i64; + if i < 0 || i > 255 { return Err(ChatError::new("packBytes: byte value out of range (0‑255)", 1)); } - bytes.push(*i as u8); + bytes.push(i as u8); } else { return Err(ChatError::new( - "packBytes expects a list of Int (Num) values", + "packBytes expects a list of Num values", 1, )); } @@ -962,7 +927,7 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: "unpackBytes".to_string(), builtin!("unpackBytes", 1, |args| { if let [Value::ByteString(b)] = &args[..] { - let list = b.iter().map(|&x| Value::Num(Number::Int(x as i64))).collect(); + let list = b.iter().map(|&x| Value::Num(x as f64)).collect(); Ok(Value::List(list)) } else { Err(ChatError::new("unpackBytes expects a ByteString argument", 1)) @@ -1092,7 +1057,7 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: if let [Value::String(path)] = &args[..] { let meta = std::fs::metadata(path).map_err(|e| ChatError::new(&format!("fileSize: {}", e), 1))?; - Ok(Value::Num(Number::Int(meta.len() as i64))) + Ok(Value::Num(meta.len() as f64)) } else { Err(ChatError::new("fileSize expects a String path", 1)) } @@ -1159,7 +1124,7 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: use std::os::unix::fs::PermissionsExt; let meta = fs::metadata(path).map_err(|e| ChatError::new(&format!("filePermissions: {}", e), 1))?; - Ok(Value::Num(Number::Int(meta.permissions().mode() as i64))) + Ok(Value::Num(meta.permissions().mode() as f64)) } else { Err(ChatError::new("filePermissions expects a String path", 1)) } @@ -1168,14 +1133,14 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: #[cfg(not(unix))] env.set( "filePermissions".to_string(), - builtin!("filePermissions", 1, |_args| Ok(Value::Num(Number::Int(0)))), + builtin!("filePermissions", 1, |_args| Ok(Value::Num(0.0))), ); #[cfg(unix)] env.set( "setFilePermissions".to_string(), builtin!("setFilePermissions", 2, |args| { - if let [Value::String(path), Value::Num(Number::Int(mode))] = &args[..] { + if let [Value::String(path), Value::Num(mode)] = &args[..] { use std::os::unix::fs::PermissionsExt; let mut perms = fs::metadata(path) .map_err(|e| ChatError::new(&format!("setFilePermissions: {}", e), 1))? @@ -1186,7 +1151,7 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: Ok(Value::Unit) } else { Err(ChatError::new( - "setFilePermissions expects a String path and an Int (Num) mode", + "setFilePermissions expects a String path and a Num mode", 1, )) } @@ -1682,13 +1647,13 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: env.set( "saveFile".to_string(), builtin!("saveFile", 2, move |args| { - if let [Value::Num(Number::Int(index)), Value::String(path)] = &args[..] { + if let [Value::Num(index), Value::String(path)] = &args[..] { let mut state = state_clone.lock().unwrap(); state.save_file(*index as usize, path)?; Ok(Value::Bool(true)) } else { Err(ChatError::new( - "saveFile expects an Int (Num) index and a String path", + "saveFile expects a Num index and a String path", 1, )) } @@ -1805,7 +1770,7 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: } } let count = state.contacts.len() as i64; - Ok(Value::Num(Number::Int(count))) + Ok(Value::Num(count as f64)) }), ); @@ -2074,7 +2039,7 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: "mapSize".to_string(), builtin!("mapSize", 1, |args| { if let [Value::Map(map)] = &args[..] { - Ok(Value::Num(Number::Int(map.len() as i64))) + Ok(Value::Num(map.len() as f64)) } else { Err(ChatError::new("mapSize expects a Map argument", 1)) } @@ -2210,7 +2175,7 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: "setSize".to_string(), builtin!("setSize", 1, |args| { if let [Value::Set(set)] = &args[..] { - Ok(Value::Num(Number::Int(set.len() as i64))) + Ok(Value::Num(set.len() as f64)) } else { Err(ChatError::new("setSize expects a Set argument", 1)) } @@ -2413,23 +2378,7 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: }), ); - let global_env_del = global_env.clone(); - env.set( - "del".to_string(), - builtin!("del", 1, move |args| { - if let [Value::String(name)] = &args[..] { - let mut env_guard = global_env_del.lock().unwrap(); - if env_guard.vars.remove(name).is_some() { - env_guard.type_map.remove(name); - Ok(Value::Unit) - } else { - Err(ChatError::new(&format!("Variable '{}' not found", name), 1)) - } - } else { - Err(ChatError::new("del expects a String (variable name)", 1)) - } - }), - ); + // Removed 'del' builtin. // p2pPort builtin let state_for_p2p_port = state.clone(); @@ -2437,7 +2386,7 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: "p2pPort".to_string(), builtin!("p2pPort", 0, move |_args| { let port = state_for_p2p_port.lock().unwrap().p2p_port; - Ok(Value::Num(Number::Int(port as i64))) + Ok(Value::Num(port as f64)) }), ); @@ -2452,27 +2401,27 @@ pub fn populate(env: &mut Environment, state: Arc>, global_env: } } -fn serde_json_to_chatlang(v: SerdeValue) -> JsonValue { +fn serde_json_to_plic(v: SerdeValue) -> JsonValue { match v { SerdeValue::Null => JsonValue::Null, SerdeValue::Bool(b) => JsonValue::Bool(b), SerdeValue::Number(n) => JsonValue::Number(n.as_f64().unwrap_or(0.0)), SerdeValue::String(s) => JsonValue::String(s), SerdeValue::Array(arr) => { - let items: Vec = arr.into_iter().map(serde_json_to_chatlang).collect(); + let items: Vec = arr.into_iter().map(serde_json_to_plic).collect(); JsonValue::Array(items) } SerdeValue::Object(map) => { let mut obj = BTreeMap::new(); for (k, v) in map { - obj.insert(k, serde_json_to_chatlang(v)); + obj.insert(k, serde_json_to_plic(v)); } JsonValue::Object(obj) } } } -fn chatlang_json_to_serde(j: &JsonValue) -> SerdeValue { +fn plic_json_to_serde(j: &JsonValue) -> SerdeValue { match j { JsonValue::Null => SerdeValue::Null, JsonValue::Bool(b) => SerdeValue::Bool(*b), @@ -2481,13 +2430,13 @@ fn chatlang_json_to_serde(j: &JsonValue) -> SerdeValue { } JsonValue::String(s) => SerdeValue::String(s.clone()), JsonValue::Array(arr) => { - let items: Vec = arr.iter().map(chatlang_json_to_serde).collect(); + let items: Vec = arr.iter().map(plic_json_to_serde).collect(); SerdeValue::Array(items) } JsonValue::Object(map) => { let mut obj = serde_json::Map::new(); for (k, v) in map { - obj.insert(k.clone(), chatlang_json_to_serde(v)); + obj.insert(k.clone(), plic_json_to_serde(v)); } SerdeValue::Object(obj) } diff --git a/src/eval.rs b/src/eval.rs index 0056840..655706b 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -1,9 +1,7 @@ -//! Evaluator with currying, Num unification, loops, break, and list comprehensions. - use crate::ast::*; use crate::chat::ChatState; use crate::error::ChatError; -use crate::types::{Environment, Value, Number}; +use crate::types::{Environment, Value}; use std::collections::BTreeMap; use std::collections::BTreeSet; use std::sync::{Arc, Mutex, atomic::Ordering}; @@ -225,14 +223,16 @@ pub fn eval_expr( let s = eval_expr(start, env, Arc::clone(&state))?; let e = eval_expr(end, env, Arc::clone(&state))?; match (s, e) { - (Value::Num(Number::Int(a)), Value::Num(Number::Int(b))) => { + (Value::Num(a), Value::Num(b)) => { + let a = a as i64; + let b = b as i64; let mut list = Vec::new(); for i in a..b { - list.push(Value::Num(Number::Int(i))); + list.push(Value::Num(i as f64)); } Ok(Value::List(list)) } - _ => Err(ChatError::with_span("Range requires Ints", 1, *span)), + _ => Err(ChatError::with_span("Range requires Num (integer values)", 1, *span)), } } Expr::Pipe(left, right, span) => { @@ -272,9 +272,8 @@ pub fn eval_expr( let coll = eval_expr(list, env, Arc::clone(&state))?; let index = eval_expr(idx, env, Arc::clone(&state))?; let index_int = match index { - Value::Num(Number::Int(i)) => i, - Value::Num(Number::Float(f)) => f as i64, - _ => return Err(ChatError::with_span("Index must be numeric (Int or Float)", 1, *span)), + Value::Num(x) => x as i64, + _ => return Err(ChatError::with_span("Index must be Num (integer)", 1, *span)), }; match coll { Value::Map(map) => { @@ -309,7 +308,7 @@ pub fn eval_expr( if i < 0 || i as usize >= b.len() { Err(ChatError::with_span("Index out of bounds", 1, *span)) } else { - Ok(Value::Num(Number::Int(b[i as usize] as i64))) + Ok(Value::Num(b[i as usize] as f64)) } } _ => Err(ChatError::with_span("Indexing requires list, string, byte string, map, or set", 1, *span)), @@ -430,9 +429,10 @@ pub fn eval_expr( Ok(Value::Unit) } Expr::New(class_name, args, span) => { + // Create instance with fields from args (by position) let class_def = env.get(class_name) .ok_or_else(|| ChatError::with_span(&format!("Class '{}' not defined", class_name), 1, *span))?; - if let Value::Record(info) = class_def { + if let Value::Record(ref info) = class_def { let fields = if let Some(Value::List(field_list)) = info.get("fields") { field_list.iter().filter_map(|v| { if let Value::String(s) = v { Some(s.clone()) } else { None } @@ -449,10 +449,42 @@ pub fn eval_expr( field_values.insert(field.clone(), Value::Unit); } } - let instance = Value::ClassInstance { + let mut instance = Value::ClassInstance { class: class_name.clone(), fields: field_values, }; + // If there is an init method, call it with the arguments + if let Some(init_method) = find_method(&class_def, "init", env).ok() { + // Call init on the instance with the same args + let mut call_args = vec![instance.clone()]; + for arg in args { + call_args.push(eval_expr(arg, env, Arc::clone(&state))?); + } + // Evaluate init method (it may modify fields via self) + match init_method { + Value::Closure(params, body, closure_env) => { + let mut new_env = closure_env.clone(); + if params.len() != call_args.len() { + return Err(ChatError::with_span("Wrong number of arguments to init", 1, *span)); + } + for (p, a) in params.iter().zip(call_args) { + new_env.set(p.clone(), a); + } + let _result = eval_expr(&body, &mut new_env, Arc::clone(&state))?; + // After init, the instance might be updated; we need to reflect changes. + // Since we passed instance as a value, it was cloned; but we need to get updated fields. + // We can retrieve self from the environment (if named "self") and use it. + if let Some(Value::ClassInstance { fields: updated_fields, .. }) = new_env.get("self") { + instance = Value::ClassInstance { + class: class_name.clone(), + fields: updated_fields.clone(), + }; + } + // Alternatively, we could require init to return the instance, but we'll rely on self mutation. + } + _ => return Err(ChatError::with_span("init is not a function", 1, *span)), + } + } Ok(instance) } else { Err(ChatError::with_span("Invalid class definition", 1, *span)) @@ -558,6 +590,55 @@ pub fn eval_expr( eval_comp(expr, generators, filters, env, Arc::clone(&state), &mut result)?; Ok(Value::List(result)) } + Expr::Cast(expr, target_type, span) => { + let val = eval_expr(expr, env, Arc::clone(&state))?; + cast_value(val, target_type, *span) + } + Expr::SuperMethod { method, args, span } => { + let self_val = env.get("self") + .ok_or_else(|| ChatError::with_span("super used outside of method (no self)", 1, *span))?; + match self_val { + Value::ClassInstance { ref class, fields: _ } => { + // Find parent class + let class_def = env.get(&class) + .ok_or_else(|| ChatError::with_span(&format!("Class '{}' not found", class), 1, *span))?; + let parent_name = if let Value::Record(info) = class_def { + if let Some(Value::String(parent)) = info.get("extends") { + if parent.is_empty() { + return Err(ChatError::with_span("No superclass", 1, *span)); + } + parent.clone() + } else { + return Err(ChatError::with_span("No superclass", 1, *span)); + } + } else { + return Err(ChatError::with_span("Invalid class definition", 1, *span)); + }; + // Find method in parent hierarchy (starting from parent) + let parent_def = env.get(&parent_name) + .ok_or_else(|| ChatError::with_span(&format!("Parent class '{}' not found", parent_name), 1, *span))?; + let method_val = find_method(&parent_def, method, env)?; + let mut all_args = vec![self_val.clone()]; + for a in args { + all_args.push(eval_expr(a, env, Arc::clone(&state))?); + } + match method_val { + Value::Closure(params, body, closure_env) => { + let mut new_env = closure_env.clone(); + if params.len() != all_args.len() { + return Err(ChatError::with_span("Wrong number of arguments to super method", 1, *span)); + } + for (p, a) in params.iter().zip(all_args) { + new_env.set(p.clone(), a); + } + eval_expr(&body, &mut new_env, state) + } + _ => Err(ChatError::with_span("Super method is not a function", 1, *span)), + } + } + _ => Err(ChatError::with_span("super called on non-class instance", 1, *span)), + } + } } } @@ -605,13 +686,15 @@ fn e_span(e: &Expr) -> Span { Expr::SetLiteral(_, s) => *s, Expr::FString(_, s) => *s, Expr::ListComp { span, .. } => *span, + Expr::Cast(_, _, s) => *s, + Expr::SuperMethod { span, .. } => *span, } } fn eval_literal(lit: &Literal) -> Result { Ok(match lit { - Literal::Int(i) => Value::Num(Number::Int(*i)), - Literal::Float(f) => Value::Num(Number::Float(*f)), + Literal::Int(i) => Value::Num(*i as f64), + Literal::Float(f) => Value::Num(*f), Literal::Char(c) => Value::Char(*c), Literal::String(s) => Value::String(s.clone()), Literal::Bool(b) => Value::Bool(*b), @@ -684,36 +767,28 @@ fn eval_binop(op: &BinOp, left: Value, right: Value, span: Span) -> Result num_op(left, right, |a,b| a-b, |a,b| a-b, span), - BinOp::Mul => num_op(left, right, |a,b| a*b, |a,b| a*b, span), - BinOp::Div => match (left, right) { - (Value::Num(Number::Int(a)), Value::Num(Number::Int(b))) => { - if b == 0 { Err(ChatError::with_span("Division by zero", 1, span)) } - else { Ok(Value::Num(Number::Int(a / b))) } - } - (Value::Num(Number::Float(a)), Value::Num(Number::Float(b))) => { - if b == 0.0 { Err(ChatError::with_span("Division by zero", 1, span)) } - else { Ok(Value::Num(Number::Float(a / b))) } - } - (Value::Num(Number::Int(a)), Value::Num(Number::Float(b))) => { - if b == 0.0 { Err(ChatError::with_span("Division by zero", 1, span)) } - else { Ok(Value::Num(Number::Float(a as f64 / b))) } - } - (Value::Num(Number::Float(a)), Value::Num(Number::Int(b))) => { - if b == 0 { Err(ChatError::with_span("Division by zero", 1, span)) } - else { Ok(Value::Num(Number::Float(a / b as f64))) } + num_op(left, right, |a,b| a+b, span) + } + BinOp::Sub => num_op(left, right, |a,b| a-b, span), + BinOp::Mul => num_op(left, right, |a,b| a*b, span), + BinOp::Div => { + match (left, right) { + (Value::Num(a), Value::Num(b)) => { + if b == 0.0 { Err(ChatError::with_span("Division by zero", 1, span)) } + else { Ok(Value::Num(a / b)) } + } + _ => Err(ChatError::with_span("Division requires Num", 1, span)), } - _ => Err(ChatError::with_span("Division requires Num", 1, span)), - }, - BinOp::Mod => match (left, right) { - (Value::Num(Number::Int(a)), Value::Num(Number::Int(b))) => { - if b == 0 { Err(ChatError::with_span("Modulo by zero", 1, span)) } - else { Ok(Value::Num(Number::Int(a % b))) } + } + BinOp::Mod => { + match (left, right) { + (Value::Num(a), Value::Num(b)) => { + if b == 0.0 { Err(ChatError::with_span("Modulo by zero", 1, span)) } + else { Ok(Value::Num(a % b)) } + } + _ => Err(ChatError::with_span("Modulo requires Num (integers)", 1, span)), } - _ => Err(ChatError::with_span("Modulo requires Int", 1, span)), - }, + } BinOp::Eq => Ok(Value::Bool(left.display() == right.display())), BinOp::Neq => Ok(Value::Bool(left.display() != right.display())), BinOp::Lt => cmp_op(left, right, |a,b| a < b, span), @@ -737,10 +812,10 @@ fn eval_binop(op: &BinOp, left: Value, right: Value, span: Span) -> Result { - if let Value::Num(Number::Int(i)) = left { + if let Value::Num(i) = left { Ok(Value::Bool(b.contains(&(i as u8)))) } else { - Err(ChatError::with_span("in for ByteString requires Int", 1, span)) + Err(ChatError::with_span("in for ByteString requires Num (integer)", 1, span)) } } Value::Set(set) => { @@ -762,12 +837,9 @@ fn eval_binop(op: &BinOp, left: Value, right: Value, span: Span) -> Result i64, ffn: fn(f64, f64) -> f64, span: Span) -> Result { +fn num_op(l: Value, r: Value, op: fn(f64, f64) -> f64, span: Span) -> Result { match (l, r) { - (Value::Num(Number::Int(a)), Value::Num(Number::Int(b))) => Ok(Value::Num(Number::Int(ifn(a, b)))), - (Value::Num(Number::Float(a)), Value::Num(Number::Float(b))) => Ok(Value::Num(Number::Float(ffn(a, b)))), - (Value::Num(Number::Int(a)), Value::Num(Number::Float(b))) => Ok(Value::Num(Number::Float(ffn(a as f64, b)))), - (Value::Num(Number::Float(a)), Value::Num(Number::Int(b))) => Ok(Value::Num(Number::Float(ffn(a, b as f64)))), + (Value::Num(a), Value::Num(b)) => Ok(Value::Num(op(a, b))), _ => Err(ChatError::with_span("Arithmetic requires Num", 1, span)), } } @@ -780,8 +852,7 @@ fn cmp_op(l: Value, r: Value, f: fn(f64, f64) -> bool, _span: Span) -> Result Result { match v { - Value::Num(Number::Int(i)) => Ok(*i as f64), - Value::Num(Number::Float(x)) => Ok(*x), + Value::Num(x) => Ok(*x), _ => Err(ChatError::new("Comparison requires numeric value", 1)), } } @@ -1051,3 +1122,137 @@ fn find_method(class_def: &Value, method: &str, env: &Environment) -> Result Result { + match target_type { + "Num" => { + match val { + Value::Num(_) => Ok(val), + Value::String(s) => { + s.parse::() + .map(Value::Num) + .map_err(|_| ChatError::with_span("Cannot cast string to Num", 1, span)) + } + Value::Char(c) => Ok(Value::Num(c as u32 as f64)), + Value::Bool(b) => Ok(Value::Num(if b { 1.0 } else { 0.0 })), + _ => Err(ChatError::with_span(&format!("Cannot cast {} to Num", type_name(&val)), 1, span)), + } + } + "String" => { + match val { + Value::String(_) => Ok(val), + Value::Num(x) => Ok(Value::String(x.to_string())), + Value::Char(c) => Ok(Value::String(c.to_string())), + Value::Bool(b) => Ok(Value::String(b.to_string())), + Value::Unit => Ok(Value::String("()".to_string())), + Value::Uid(u) => Ok(Value::String(u)), + _ => Err(ChatError::with_span(&format!("Cannot cast {} to String", type_name(&val)), 1, span)), + } + } + "Char" => { + match val { + Value::Char(_) => Ok(val), + Value::Num(x) => { + let c = x as u32; + if let Some(ch) = std::char::from_u32(c) { + Ok(Value::Char(ch)) + } else { + Err(ChatError::with_span("Invalid Unicode code point", 1, span)) + } + } + Value::String(s) => { + if s.len() == 1 { + Ok(Value::Char(s.chars().next().unwrap())) + } else { + Err(ChatError::with_span("String must be exactly one character", 1, span)) + } + } + _ => Err(ChatError::with_span(&format!("Cannot cast {} to Char", type_name(&val)), 1, span)), + } + } + "Bool" => { + match val { + Value::Bool(_) => Ok(val), + Value::Num(x) => Ok(Value::Bool(x != 0.0)), + Value::String(s) => Ok(Value::Bool(!s.is_empty())), + _ => Err(ChatError::with_span(&format!("Cannot cast {} to Bool", type_name(&val)), 1, span)), + } + } + "ByteString" => { + match val { + Value::ByteString(_) => Ok(val), + Value::String(s) => Ok(Value::ByteString(s.into_bytes())), + Value::List(list) => { + let mut bytes = Vec::new(); + for v in list { + if let Value::Num(x) = v { + let b = x as u8; + if x < 0.0 || x > 255.0 { + return Err(ChatError::with_span("ByteString element out of range (0-255)", 1, span)); + } + bytes.push(b); + } else { + return Err(ChatError::with_span("List must contain numbers", 1, span)); + } + } + Ok(Value::ByteString(bytes)) + } + _ => Err(ChatError::with_span(&format!("Cannot cast {} to ByteString", type_name(&val)), 1, span)), + } + } + "List" => { + match val { + Value::List(_) => Ok(val), + Value::String(s) => Ok(Value::List(s.chars().map(Value::Char).collect())), + Value::ByteString(b) => Ok(Value::List(b.iter().map(|&x| Value::Num(x as f64)).collect())), + Value::Map(map) => { + let list: Vec = map.into_iter().map(|(k, v)| Value::Tuple(vec![Value::String(k), v])).collect(); + Ok(Value::List(list)) + } + Value::Set(set) => { + let list: Vec = set.into_iter().map(Value::String).collect(); + Ok(Value::List(list)) + } + _ => Err(ChatError::with_span(&format!("Cannot cast {} to List", type_name(&val)), 1, span)), + } + } + "Map" => { + match val { + Value::Map(_) => Ok(val), + Value::List(list) => { + let mut map = BTreeMap::new(); + for item in list { + match item { + Value::Tuple(v) if v.len() == 2 => { + map.insert(v[0].display(), v[1].clone()); + } + _ => return Err(ChatError::with_span("List elements must be 2-element tuples", 1, span)), + } + } + Ok(Value::Map(map)) + } + _ => Err(ChatError::with_span(&format!("Cannot cast {} to Map", type_name(&val)), 1, span)), + } + } + "Set" => { + match val { + Value::Set(_) => Ok(val), + Value::List(list) => { + let set: BTreeSet<_> = list.into_iter().map(|v| v.display()).collect(); + Ok(Value::Set(set)) + } + _ => Err(ChatError::with_span(&format!("Cannot cast {} to Set", type_name(&val)), 1, span)), + } + } + _ => { + // For custom types, we could allow if the value is already that type, or maybe not. + // For simplicity, we only allow if the value is already of that type. + if type_name(&val) == target_type { + Ok(val) + } else { + Err(ChatError::with_span(&format!("Cannot cast {} to {}", type_name(&val), target_type), 1, span)) + } + } + } +} diff --git a/src/lexer.rs b/src/lexer.rs index ac4a08b..ca6bd1d 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -1,5 +1,3 @@ -//! Lexer with position tracking. - use crate::ast::Literal; use std::time::Duration; @@ -43,7 +41,6 @@ pub enum TokenKind { RBracket, Comma, Semicolon, - Backslash, Pipe, Arrow, FatArrow, @@ -89,6 +86,7 @@ pub enum TokenKind { New, Loop, Break, + Super, // new token for `super` Ident(String), Literal(Literal), FString(String), @@ -217,7 +215,6 @@ impl Lexer { ']' => { self.advance(); Ok(Some(Token { kind: TokenKind::RBracket, start, end })) } ',' => { self.advance(); Ok(Some(Token { kind: TokenKind::Comma, start, end })) } ';' => { self.advance(); Ok(Some(Token { kind: TokenKind::Semicolon, start, end })) } - '\\' => { self.advance(); Ok(Some(Token { kind: TokenKind::Backslash, start, end })) } '|' => { self.advance(); if self.current() == Some('>') { @@ -363,6 +360,7 @@ impl Lexer { "new" => Ok(Some(Token { kind: TokenKind::New, start, end })), "loop" => Ok(Some(Token { kind: TokenKind::Loop, start, end })), "break" => Ok(Some(Token { kind: TokenKind::Break, start, end })), + "super" => Ok(Some(Token { kind: TokenKind::Super, start, end })), _ => Ok(Some(Token { kind: TokenKind::Ident(ident), start, end })), } } diff --git a/src/main.rs b/src/main.rs index b71ada8..1debcd6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,7 @@ -//! REPL with syntax highlighting and error reporting. - use plic::chat::ChatState; use plic::eval::eval_expr; use plic::types::Environment; -use plic::parser::{parse_expression, parse_script, ParseError, strip_comments}; +use plic::parser::{parse_expression, parse_script}; use rustyline::Editor; use rustyline::error::ReadlineError; use rustyline::completion::Completer; @@ -23,14 +21,14 @@ use codespan_reporting::term as term_reporting; static INTERRUPTED: AtomicBool = AtomicBool::new(false); -struct ChatLangHelper { +struct PlicHelper { env: Arc>, keywords: Vec, builtins: Vec, types: Vec, } -impl Completer for ChatLangHelper { +impl Completer for PlicHelper { type Candidate = String; fn complete(&self, line: &str, pos: usize, _ctx: &rustyline::Context<'_>) -> rustyline::Result<(usize, Vec)> { @@ -59,19 +57,22 @@ impl Completer for ChatLangHelper { } } -impl Highlighter for ChatLangHelper { +impl Highlighter for PlicHelper { fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> { let mut result = String::new(); - let in_string = false; - let mut in_comment = false; - let mut multiline_comment_depth = 0; + let mut chars = line.chars().peekable(); + let mut in_string = false; let mut in_fstring = false; - let mut brace_depth = 0; + let mut fstring_brace_depth = 0; + let mut escape = false; + let mut multiline_comment_depth = 0; + let mut in_comment = false; + let keywords = [ - "let", "if", "then", "else", "case", "of", "lambda", "\\", + "let", "if", "then", "else", "case", "of", "lambda", "data", "struct", "try", "catch", "error", "for", "while", "true", "false", "in", "and", "or", "not", "class", "extends", "new", - "loop", "break", "load", "del" + "loop", "break", "load", "super" ]; let types = [ "Num", "Char", "String", "Bool", "Unit", "Uid", @@ -80,7 +81,7 @@ impl Highlighter for ChatLangHelper { "FileTransfer", "FetchOptions", "FetchResult", "Map", "Set", "ClassInstance" ]; - let mut chars = line.chars().peekable(); + while let Some(ch) = chars.next() { if ch == '#' && chars.peek() == Some(&'-') { chars.next(); @@ -128,27 +129,94 @@ impl Highlighter for ChatLangHelper { result.push(ch); continue; } - if in_fstring && ch == '"' { - result.push_str("\"\x1b[0m"); - in_fstring = false; + + if ch == '"' && !in_fstring { + if !in_string { + in_string = true; + result.push_str("\x1b[32m"); + result.push(ch); + } else { + if escape { + result.push(ch); + } else { + result.push_str("\"\x1b[0m"); + in_string = false; + } + escape = false; + } continue; } - if in_fstring && ch == '{' { - brace_depth += 1; - result.push_str("\x1b[0m"); - result.push(ch); + + if in_string { + if ch == '\\' && !escape { + escape = true; + result.push(ch); + } else if ch == '"' && escape { + escape = false; + result.push(ch); + } else { + if escape { escape = false; } + result.push(ch); + } continue; } - if in_fstring && ch == '}' { - brace_depth -= 1; + + if in_fstring { + if ch == '"' && fstring_brace_depth == 0 { + result.push_str("\"\x1b[0m"); + in_fstring = false; + continue; + } + if ch == '{' { + if let Some(&next) = chars.peek() { + if next == '{' { + chars.next(); + result.push_str("{{"); + continue; + } + } + fstring_brace_depth += 1; + result.push_str("\x1b[0m"); + result.push(ch); + let mut expr_chars = String::new(); + let mut depth = 1; + while let Some(c) = chars.next() { + if c == '{' { + depth += 1; + expr_chars.push(c); + } else if c == '}' { + depth -= 1; + if depth == 0 { + break; + } else { + expr_chars.push(c); + } + } else { + expr_chars.push(c); + } + } + result.push_str(&expr_chars); + result.push_str("\x1b[32m"); + result.push('}'); + continue; + } + if ch == '}' { + if let Some(&next) = chars.peek() { + if next == '}' { + chars.next(); + result.push_str("}}"); + continue; + } + } + fstring_brace_depth -= 1; + result.push_str("\x1b[32m"); + result.push(ch); + continue; + } result.push_str("\x1b[32m"); result.push(ch); continue; } - if in_fstring && brace_depth > 0 { - result.push(ch); - continue; - } if ch.is_alphabetic() || ch == '_' { let mut word = String::new(); @@ -193,21 +261,20 @@ impl Highlighter for ChatLangHelper { } } -impl Hinter for ChatLangHelper { +impl Hinter for PlicHelper { type Hint = String; fn hint(&self, _line: &str, _pos: usize, _ctx: &rustyline::Context<'_>) -> Option { None } } -impl Validator for ChatLangHelper { +impl Validator for PlicHelper { fn validate(&self, _ctx: &mut ValidationContext<'_>) -> rustyline::Result { Ok(ValidationResult::Valid(None)) } } -impl Helper for ChatLangHelper {} +impl Helper for PlicHelper {} fn main() { - // Global panic handler to avoid unwrap messages. std::panic::set_hook(Box::new(|panic_info| { eprintln!("\x1b[31mFatal error\x1b[0m: {}", panic_info); std::process::exit(1); @@ -271,21 +338,20 @@ fn main() { return; } - let helper = ChatLangHelper { + let helper = PlicHelper { env: Arc::clone(&env_arc), keywords: vec![ "let".into(), "if".into(), "then".into(), "else".into(), - "case".into(), "of".into(), "lambda".into(), "\\".into(), + "case".into(), "of".into(), "lambda".into(), "data".into(), "struct".into(), "try".into(), "catch".into(), "error".into(), "for".into(), "while".into(), "in".into(), "and".into(), "or".into(), "not".into(), "class".into(), "extends".into(), "new".into(), - "loop".into(), "break".into(), "load".into(), "del".into(), + "loop".into(), "break".into(), "load".into(), "super".into(), ], builtins: vec![ "sqrt".into(), "sin".into(), "cos".into(), "tan".into(), "asin".into(), "acos".into(), "atan".into(), - "toFloat".into(), "toInt".into(), "show".into(), "parseInt".into(), "parseFloat".into(), "chr".into(), "ord".into(), "null".into(), "length".into(), "map".into(), "filter".into(), @@ -328,7 +394,7 @@ fn main() { "listDir".into(), "createDir".into(), "removeDir".into(), "fileMove".into(), "filePermissions".into(), "setFilePermissions".into(), "typeof".into(), "getPublicIP".into(), "setExternalIP".into(), - "exit".into(), "load".into(), "del".into(), + "exit".into(), "load".into(), "logout".into(), "deleteUser".into(), "deleteChat".into(), "listChats".into(), "members".into(), "addContact".into(), "removeContact".into(), @@ -348,10 +414,11 @@ fn main() { let mut rl = Editor::new().unwrap(); rl.set_helper(Some(helper)); - let _ = rl.load_history(".chatlang_history"); + let _ = rl.load_history(".plic_history"); let mut buffer = String::new(); let mut in_multiline = false; + let mut first_indent = 0; loop { let prompt = if buffer.is_empty() { @@ -364,96 +431,114 @@ fn main() { Ok(line) => { rl.add_history_entry(line.as_str()); let trimmed = line.trim(); + let indent = line.chars().take_while(|c| *c == ' ').count(); + + if !in_multiline { + if trimmed.is_empty() { + continue; + } - if in_multiline && (trimmed == "end" || (line.chars().take_while(|c| *c == ' ').count() == 0 && trimmed.is_empty())) { - let full_block = buffer.clone(); - match parse_script(&full_block) { + if trimmed == "{" || trimmed == "}" { + eprintln!("\x1b[31merror\x1b[0m: Unexpected token '{}'", trimmed); + continue; + } + + // If line ends with a single colon (not ::), enter multiline mode with colon removed + if trimmed.ends_with(':') && !line.contains("::") { + let mut modified = line.trim_end().to_string(); + while modified.ends_with(':') || modified.ends_with(' ') { + modified.pop(); + } + buffer = modified; + in_multiline = true; + first_indent = indent; + continue; + } + + match parse_expression(&line) { Ok(expr) => { let mut env_guard = env_arc.lock().unwrap(); match eval_expr(&expr, &mut env_guard, Arc::clone(&state)) { Ok(_) => {}, Err(err) => eprintln!("\x1b[31merror\x1b[0m: {}", err), } + continue; + } + Err(_) => { + buffer = line; + in_multiline = true; + first_indent = indent; + continue; } - Err(e) => eprintln!("\x1b[31merror\x1b[0m: Parse error: {}", e), } - buffer.clear(); - in_multiline = false; - continue; - } - - let stripped = strip_comments(&line); - if stripped.trim().is_empty() && !in_multiline { - continue; - } - - if !in_multiline && trimmed.ends_with(':') { - buffer.push_str(&line); - in_multiline = true; - continue; - } + } else { + if trimmed.is_empty() { + if !buffer.is_empty() { + let full_block = buffer.clone(); + match parse_script(&full_block) { + Ok(expr) => { + let mut env_guard = env_arc.lock().unwrap(); + match eval_expr(&expr, &mut env_guard, Arc::clone(&state)) { + Ok(_) => {}, + Err(err) => eprintln!("\x1b[31merror\x1b[0m: {}", err), + } + } + Err(e) => eprintln!("\x1b[31merror\x1b[0m: Parse error: {}", e), + } + buffer.clear(); + in_multiline = false; + first_indent = 0; + } + continue; + } - if in_multiline { - buffer.push('\n'); - buffer.push_str(&line); - let current_indent = line.chars().take_while(|c| *c == ' ').count(); - if current_indent == 0 && !trimmed.is_empty() && trimmed != "end" { + if indent > first_indent { + buffer.push('\n'); + buffer.push_str(&line); + continue; + } else { let full_block = buffer.clone(); - match parse_script(&full_block) { + let block_ok = match parse_script(&full_block) { Ok(expr) => { let mut env_guard = env_arc.lock().unwrap(); match eval_expr(&expr, &mut env_guard, Arc::clone(&state)) { - Ok(_) => {}, - Err(err) => eprintln!("\x1b[31merror\x1b[0m: {}", err), + Ok(_) => true, + Err(err) => { + eprintln!("\x1b[31merror\x1b[0m: {}", err); + false + } } } - Err(e) => eprintln!("\x1b[31merror\x1b[0m: Parse error: {}", e), - } + Err(e) => { + eprintln!("\x1b[31merror\x1b[0m: Parse error: {}", e); + false + } + }; buffer.clear(); in_multiline = false; - continue; - } - continue; - } + first_indent = 0; - match parse_expression(&stripped) { - Ok(expr) => { - let mut env_guard = env_arc.lock().unwrap(); - match eval_expr(&expr, &mut env_guard, Arc::clone(&state)) { - Ok(_) => {}, - Err(err) => { - if err.span.is_some() { - let mut files = Files::new(); - let file_id = files.add("", line.clone()); - let diagnostic = Diagnostic::error() - .with_message(err.message) - .with_labels(vec![ - Label::primary(file_id, err.span.unwrap().start..err.span.unwrap().end) - ]); - let writer = StandardStream::stderr(ColorChoice::Always); - let config = term_reporting::Config::default(); - let _ = term_reporting::emit(&mut writer.lock(), &config, &files, &diagnostic); - } else { - eprintln!("\x1b[31merror\x1b[0m: {}", err); + if !block_ok { + continue; + } + + if !trimmed.is_empty() { + match parse_expression(&line) { + Ok(expr) => { + let mut env_guard = env_arc.lock().unwrap(); + match eval_expr(&expr, &mut env_guard, Arc::clone(&state)) { + Ok(_) => {}, + Err(err) => eprintln!("\x1b[31merror\x1b[0m: {}", err), + } + } + Err(_) => { + buffer = line; + in_multiline = true; + first_indent = indent; } } } - } - Err(ParseError { message, span }) => { - if span != plic::ast::Span::dummy() { - let mut files = Files::new(); - let file_id = files.add("", line.clone()); - let diagnostic = Diagnostic::error() - .with_message(message) - .with_labels(vec![ - Label::primary(file_id, span.start..span.end) - ]); - let writer = StandardStream::stderr(ColorChoice::Always); - let config = term_reporting::Config::default(); - let _ = term_reporting::emit(&mut writer.lock(), &config, &files, &diagnostic); - } else { - eprintln!("\x1b[31merror\x1b[0m: Parse error: {}", message); - } + continue; } } } @@ -462,11 +547,12 @@ fn main() { println!("Aborted multi-line block."); buffer.clear(); in_multiline = false; + first_indent = 0; } continue; } Err(_) => break, } } - let _ = rl.save_history(".chatlang_history"); + let _ = rl.save_history(".plic_history"); } diff --git a/src/parser.rs b/src/parser.rs index 607ab7e..ecfc38b 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,5 +1,3 @@ -//! Parser with indentation‑sensitive blocks, list comprehensions, and spans. - use crate::ast::*; use crate::lexer::{LexError, Lexer, Token, TokenKind}; @@ -25,7 +23,6 @@ impl From for ParseError { pub type ParseResult = Result; -/// Remove comments from input, handling nested `#- ... -#` blocks. pub fn strip_comments(input: &str) -> String { let mut output = String::new(); let mut chars = input.chars().peekable(); @@ -54,7 +51,6 @@ pub fn strip_comments(input: &str) -> String { } continue; } else { - // Line comment while let Some(c) = chars.next() { if c == '\n' { output.push(c); @@ -154,6 +150,8 @@ fn e_span(e: &Expr) -> Span { Expr::SetLiteral(_, s) => *s, Expr::FString(_, s) => *s, Expr::ListComp { span, .. } => *span, + Expr::Cast(_, _, s) => *s, + Expr::SuperMethod { span, .. } => *span, } } @@ -403,6 +401,31 @@ fn parse_atom(tokens: &[Token], pos: &mut usize) -> ParseResult { } parse_postfix(expr, tokens, pos) } + TokenKind::Super => { + *pos += 1; + let _end = tokens[*pos - 1].end; + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Dot { + *pos += 1; + if let TokenKind::Ident(method) = &tokens[*pos].kind { + let method = method.clone(); + *pos += 1; + expect_kind(tokens, pos, TokenKind::LParen)?; + let mut args = Vec::new(); + while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RParen { + args.push(parse_expr(tokens, pos)?); + if *pos < tokens.len() && tokens[*pos].kind == TokenKind::Comma { + *pos += 1; + } + } + let end2 = expect_kind(tokens, pos, TokenKind::RParen)?; + Ok(Expr::SuperMethod { method, args, span: Span::new(start, end2) }) + } else { + Err(ParseError { message: "expected method name after super.".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }) + } + } else { + Err(ParseError { message: "expected .method after super".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }) + } + } TokenKind::If => { *pos += 1; let cond = parse_expr(tokens, pos)?; @@ -582,7 +605,7 @@ fn parse_atom(tokens: &[Token], pos: &mut usize) -> ParseResult { Ok(expr) } } - TokenKind::Backslash | TokenKind::Lambda => { + TokenKind::Lambda => { *pos += 1; let mut params = Vec::new(); while *pos < tokens.len() && matches!(tokens[*pos].kind, TokenKind::Ident(_)) { @@ -701,21 +724,9 @@ fn parse_atom(tokens: &[Token], pos: &mut usize) -> ParseResult { } TokenKind::Loop => { *pos += 1; - expect_kind(tokens, pos, TokenKind::LBrace)?; - let mut exprs = Vec::new(); - while *pos < tokens.len() && tokens[*pos].kind != TokenKind::RBrace { - let e = parse_expr(tokens, pos)?; - exprs.push(e); - if *pos < tokens.len() && (tokens[*pos].kind == TokenKind::Semicolon || tokens[*pos].kind == TokenKind::NEWLINE) { - *pos += 1; - } - } - let end = expect_kind(tokens, pos, TokenKind::RBrace)?; - let body = if exprs.len() == 1 { - exprs.remove(0) - } else { - Expr::Block(exprs, Span::new(start, end)) - }; + expect_kind(tokens, pos, TokenKind::Colon)?; + let body = parse_expr_or_block(tokens, pos)?; + let end = e_span(&body).end; Ok(Expr::Loop(Box::new(body), Span::new(start, end))) } TokenKind::Break => { @@ -891,6 +902,17 @@ fn parse_postfix(mut expr: Expr, tokens: &[Token], pos: &mut usize) -> ParseResu let index = parse_expr(tokens, pos)?; let end = expect_kind(tokens, pos, TokenKind::RBracket)?; expr = Expr::Index(Box::new(expr), Box::new(index), Span::new(start, end)); + } else if *pos < tokens.len() && tokens[*pos].kind == TokenKind::DoubleColon { + let start = tokens[*pos].start; + *pos += 1; + let type_name = match &tokens[*pos].kind { + TokenKind::Ident(t) => t.clone(), + _ => return Err(ParseError { message: "expected type name after ::".to_string(), span: Span::new(tokens[*pos].start, tokens[*pos].end) }), + }; + *pos += 1; + let end = tokens[*pos - 1].end; + expr = Expr::Cast(Box::new(expr), type_name, Span::new(start, end)); + continue; } else if *pos < tokens.len() && tokens[*pos].kind == TokenKind::LBrace { let start = tokens[*pos].start; *pos += 1; @@ -957,8 +979,9 @@ fn parse_application(mut func: Expr, tokens: &[Token], pos: &mut usize) -> Parse fn is_argument_start(kind: &TokenKind) -> bool { matches!(kind, - TokenKind::Literal(_) | TokenKind::Ident(_) | TokenKind::LParen | TokenKind::LBracket | - TokenKind::Backslash | TokenKind::Lambda | TokenKind::If | TokenKind::Let | TokenKind::Case | + TokenKind::Literal(_) | TokenKind::Ident(_) | TokenKind::Super | + TokenKind::LParen | TokenKind::LBracket | + TokenKind::Lambda | TokenKind::If | TokenKind::Let | TokenKind::Case | TokenKind::Try | TokenKind::Error | TokenKind::For | TokenKind::While | TokenKind::Not | TokenKind::Minus | TokenKind::Percent | TokenKind::Class | TokenKind::New | TokenKind::Struct | TokenKind::Data | TokenKind::FString(_) | TokenKind::Loop | TokenKind::Break @@ -1063,7 +1086,7 @@ fn parse_pattern(tokens: &[Token], pos: &mut usize) -> Result, } -#[derive(Debug, Clone, PartialEq)] -pub enum Number { - Int(i64), - Float(f64), -} - -impl Number { - pub fn to_f64(&self) -> f64 { - match self { - Number::Int(i) => *i as f64, - Number::Float(f) => *f, - } - } - pub fn to_i64(&self) -> i64 { - match self { - Number::Int(i) => *i, - Number::Float(f) => *f as i64, - } - } -} - #[derive(Clone)] pub enum Value { - Num(Number), + Num(f64), // unified number type Char(char), String(String), Bool(bool), @@ -121,15 +100,12 @@ impl fmt::Debug for Value { impl Value { pub fn display(&self) -> String { match self { - Value::Num(n) => match n { - Number::Int(i) => format!("{}", i), - Number::Float(x) => { - let s = format!("{:.10}", x); - if s == "-0.0000000000" { "0.0".to_string() } - else { - let trimmed = s.trim_end_matches('0').trim_end_matches('.'); - if trimmed.is_empty() { "0".to_string() } else { trimmed.to_string() } - } + Value::Num(x) => { + let s = format!("{:.10}", x); + if s == "-0.0000000000" { "0.0".to_string() } + else { + let trimmed = s.trim_end_matches('0').trim_end_matches('.'); + if trimmed.is_empty() { "0".to_string() } else { trimmed.to_string() } } } Value::Char(c) => c.to_string(), From b9444735482cbcb526012e2d913c8dfc0f168c62 Mon Sep 17 00:00:00 2001 From: one-of-all Date: Mon, 20 Jul 2026 09:35:20 +0300 Subject: [PATCH 17/18] Delete install_debug.sh --- install_debug.sh | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 install_debug.sh diff --git a/install_debug.sh b/install_debug.sh deleted file mode 100644 index fd3721f..0000000 --- a/install_debug.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -set -e - -echo "Building PLIC in debug mode..." -cargo build - -BIN_SRC="target/debug/plic" -BIN_DEST="/bin/plic" - -if [ ! -f "$BIN_SRC" ]; then - echo "Error: build failed, binary not found at $BIN_SRC" - exit 1 -fi - -echo "Installing binary to $BIN_DEST (requires sudo)..." -sudo cp "$BIN_SRC" "$BIN_DEST" -sudo chmod 755 "$BIN_DEST" - -echo "Installation complete! You can now run 'plic'." From 5ae9a5f79842aa45367322267347f9c43615a6b6 Mon Sep 17 00:00:00 2001 From: one-of-all Date: Mon, 20 Jul 2026 09:35:33 +0300 Subject: [PATCH 18/18] Update README.md --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index 342354d..f0d159c 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,5 @@ # PLIC -## Примечание - -Язык создовался в сильной спешке, и некоторые вещи не были отполированны. В случае если установка через `install.sh` провалилась, используйте скрипт `install_debug.sh` - -## Введение - **PLIC** — это динамический язык программирования, сочетающий функциональное, императивное и объектно‑ориентированное программирование. Встроенная поддержка P2P‑сообщений и файлового обмена позволяет использовать его как платформу для децентрализованных чат‑приложений. ## Особенности