Changes
6 changed files (+411/-26)
-
ARISTOTLE_SUMMARY.md (new)
-
@@ -0,0 +1,25 @@# Summary of changes for run 2b9a733d-8bd5-40ca-9295-b4b5c7da4691 Ported the Python nanochat TCP server to Lean 4 in `RequestProject/Nanochat.lean`, using `Std.Internal.UV.TCP.Socket` for networking. The executable target is named `nanochat` in `lakefile.toml`. **How to build and run:** ``` lake build nanochat .lake/build/bin/nanochat [[HOST] PORT] ``` Default host is `0.0.0.0`, default port is `44322`. **What was ported:** All features from the Python original are faithfully reproduced: - **Commands:** `SEND <msg>`, `SKIP <num>`, `LAST <k>`, `POLL <num>`, `HIST`, `QUIT`, `SHUTDOWN`, `RESTART`, `STAT` — all with the same wire protocol and semantics. - **Persistence:** Messages are appended to `chat.log` and reloaded on startup. - **Byte sanitisation:** The 256-byte translation table is reproduced exactly — TAB (0x09), LF (0x0a), and printable bytes (≥ 0x20 except DEL) are kept; everything else is mapped to `?`. - **Buffered line protocol:** The 500-byte overflow protection and newline-delimited command parsing match the Python behaviour. - **Concurrency:** Each client connection is handled in a separate `IO.asTask`, with shared state managed via `IO.Ref`. - **Client counting:** The `STAT` command reports messages, total bytes, and connected clients. - **Argument parsing:** Same CLI interface as the Python version. **One minor difference:** The `RESTART` command sets a flag and prints a message asking for manual restart, since Lean doesn't have an `os.execvp` equivalent to re-exec the process in-place. The server was tested end-to-end with a Python client confirming correct behavior for SEND, HIST, STAT, SKIP, LAST, POLL, QUIT, SHUTDOWN, and log persistence across restarts.
-
-
-
@@ -1,27 +1,336 @@import Std open Std.Internal.IO.Async.TCP.Socket /- Nanochat — a simple TCP chat server. Port of the Python nanochat server to Lean 4 using Std.Internal.UV.TCP. -/ import Std.Internal.UV open Std.Net (SocketAddress SocketAddressV4 IPv4Addr) /-- Abbreviation for the TCP socket type. -/ abbrev TCPSocket := Std.Internal.UV.TCP.Socket /-! ### Shared server state -/ structure ServerState where messages : Array ByteArray := #[] total : Nat := 0 clients : Nat := 0 shutdown : Bool := false restart : Bool := false /-! ### Byte translation (sanitise control characters) -/ /-- Build a 256-byte translation table: keep TAB (0x09), LF (0x0a), and bytes ≥ 0x20 except DEL (0x7f), mapping everything else to '?'. -/ def mkByteTable : ByteArray := Id.run do let mut arr : ByteArray := .empty for n in [:256] do let b : UInt8 := if n == 0x7f then 0x3f else if n >= 0x20 then n.toUInt8 else if n == 0x09 then 0x09 else if n == 0x0a then 0x0a else 0x3f arr := arr.push b arr def byteTable : ByteArray := mkByteTable def translate (msg : ByteArray) : ByteArray := Id.run do let mut out : ByteArray := .empty for i in [:msg.size] do out := out.push (byteTable.get! (msg.get! i).toNat) out /-! ### Helpers -/ def encodeNum (n : Nat) : ByteArray := (toString n ++ "\n").toUTF8 def appendNewline (ba : ByteArray) : ByteArray := ba ++ ByteArray.mk #[0x0a] def sendAll (sock : TCPSocket) (data : ByteArray) : IO Unit := do let p ← sock.send #[data] let r ← IO.wait (IO.Promise.result! p) IO.ofExcept r def recvOpt (sock : TCPSocket) (n : UInt64 := 4096) : IO (Option ByteArray) := do let p ← sock.recv? n let r ← IO.wait (IO.Promise.result! p) IO.ofExcept r def showAddr (sa : SocketAddress) : String := match sa with | .v4 a => s!"{a.addr.toString}:{a.port.toNat}" | .v6 a => s!"[ipv6]:{a.port.toNat}" /-! ### Command parsing -/ inductive Command where | send (msg : ByteArray) | skip (n : Nat) | last (k : Nat) | poll (n : Nat) | hist | quit | shutdown | restart | stat | unknown def parseCommand (raw : ByteArray) : Command := Id.run do let s := String.fromUTF8! raw if s == "HIST" then return .hist if s == "QUIT" then return .quit if s == "SHUTDOWN" then return .shutdown if s == "RESTART" then return .restart if s == "STAT" then return .stat if s.startsWith "SEND " && s.length > 5 then let arg := raw.extract 5 raw.size return .send arg if s.startsWith "SKIP " then match (s.drop 5).trimAscii.toNat? with | some n => return .skip n | none => return .unknown if s.startsWith "LAST " then match (s.drop 5).trimAscii.toNat? with | some k => return .last k | none => return .unknown if s.startsWith "POLL " then match (s.drop 5).trimAscii.toNat? with | some n => return .poll n | none => return .unknown return .unknown /-! ### Log file -/ def loadMessages (path : System.FilePath) : IO (Array ByteArray × Nat) := do let exists_ ← path.pathExists if !exists_ then return (#[], 0) let contents ← IO.FS.readBinFile path let mut msgs : Array ByteArray := #[] let mut total : Nat := 0 let mut start : Nat := 0 for i in [:contents.size] do if contents.get! i == 0x0a then let msg := contents.extract start i msgs := msgs.push msg total := total + msg.size start := i + 1 return (msgs, total) /-! ### Client handler -/ def handleClient (sock : TCPSocket) (stateRef : IO.Ref ServerState) (logRef : IO.Ref IO.FS.Handle) : IO Unit := do let peer ← try let sa ← sock.getPeerName pure (showAddr sa) catch _ => pure "<unknown>" IO.println s!"client connected {peer}" let mut buf : ByteArray := .empty let mut done := false while !done do let st ← stateRef.get if st.shutdown then IO.println s!"terminating client {peer}" return let dataOpt ← recvOpt sock 500 match dataOpt with | none => IO.println s!"client disconnected {peer}" done := true | some data => buf := buf ++ data if buf.size >= 500 then if (buf.findIdx? (· == 0x0a)).isNone then IO.println s!"size ({buf.size}) was too long; resetting" buf := .empty continue let mut processing := true while processing do match buf.findIdx? (· == 0x0a) with | none => processing := false | some idx => let line := buf.extract 0 idx buf := buf.extract (idx + 1) buf.size let cmd := parseCommand line match cmd with | .unknown => IO.println s!"unknown {repr (String.fromUTF8! line)}" | .send rawMsg => do let msg := translate rawMsg let n ← stateRef.modifyGet fun st => let n := st.messages.size (n, { st with messages := st.messages.push msg total := st.total + msg.size }) let h ← logRef.get h.write (appendNewline msg) h.flush let st ← stateRef.get let mut actualN := n for i in [n:st.messages.size] do if st.messages[i]! == msg then actualN := i break sendAll sock (encodeNum actualN) | .skip num => do let st ← stateRef.get let n := st.messages.size let msgs := st.messages.extract (num + 1) n let lastnum := n - 1 sendAll sock (encodeNum msgs.size) for m in msgs do sendAll sock (appendNewline m) sendAll sock (encodeNum lastnum) | .last k => do let st ← stateRef.get let n := st.messages.size let i := if n ≥ k then n - k else 0 let msgs := st.messages.extract i n let lastnum := n - 1 sendAll sock (encodeNum msgs.size) for m in msgs do sendAll sock (appendNewline m) sendAll sock (encodeNum lastnum) | .poll num => do let st ← stateRef.get let n := st.messages.size let diff := n - (num + 1) sendAll sock (encodeNum diff) | .hist => do let st ← stateRef.get let n := st.messages.size let lastnum := n - 1 sendAll sock (encodeNum n) for m in st.messages do sendAll sock (appendNewline m) sendAll sock (encodeNum lastnum) | .quit => do IO.println s!"client quit {peer}" done := true processing := false | .shutdown => do IO.println s!"client sent shutdown {peer}" IO.println "shutting down..." stateRef.modify fun st => { st with shutdown := true } done := true processing := false | .restart => do IO.println s!"client sent restart {peer}" IO.println "restarting..." stateRef.modify fun st => { st with shutdown := true, restart := true } done := true processing := false | .stat => do let st ← stateRef.get sendAll sock s!"{st.messages.size} messages\n".toUTF8 sendAll sock s!"{st.total} bytes\n".toUTF8 sendAll sock s!"{st.clients} clients\n".toUTF8 instance : ToString Std.Net.SocketAddress where toString a := s!"{a.ipAddr}:{a.port}" stateRef.modify fun st => { st with clients := st.clients - 1 } def handler (conn : Client) := do IO.println s!"Connection from {← conn.getPeerName}" match ← (← conn.recv? 65536).block with | some data => match String.fromUTF8? data with | some dataStr => let filename := (← IO.rand 0 <| 2 ^ 32 - 1).toInt32.toBitVec.toHex IO.println s!"Writing to {filename}" IO.FS.writeFile filename dataStr conn.send s!"View at https://leanet.unnamed.website/{filename}\n".toUTF8 | none => conn.send "Invalid UTF-8\n".toUTF8 | none => conn.send "You screwed up somehow probably\n".toUTF8 /-! ### Accept loop -/ def main := do let server ← Server.mk server.bind <| .v4 ⟨⟨Vector.replicate 4 0⟩, 1349⟩ server.listen 32 IO.println s!"Listening on {← server.getSockName}" def acceptLoop (serverSock : TCPSocket) (stateRef : IO.Ref ServerState) (logRef : IO.Ref IO.FS.Handle) : IO Unit := do while true do let conn ← (← server.accept).block _ ← IO.asTask <| handler conn let st ← stateRef.get if st.shutdown then return let promise ← serverSock.accept let result ← IO.wait (IO.Promise.result! promise) let clientSock ← IO.ofExcept result stateRef.modify fun st => { st with clients := st.clients + 1 } let _ ← IO.asTask (prio := .default) do try handleClient clientSock stateRef logRef catch e => IO.eprintln s!"client handler error: {e}" /-! ### Main -/ def main (args : List String) : IO UInt32 := do let logpath : System.FilePath := "chat.log" let mut host := "0.0.0.0" let mut port : Nat := 44322 match args with | [] => pure () | [p] => match p.toNat? with | some n => port := n | none => IO.eprintln s!"invalid port: {p}" return 1 | [h, p] => host := h match p.toNat? with | some n => port := n | none => IO.eprintln s!"invalid port: {p}" return 1 | _ => IO.println "usage: nanochat [[HOST] PORT]" IO.println "" IO.println "default host is 0.0.0.0, default port is 44322" IO.println "" IO.println "examples:" IO.println " nanochat 0.0.0.0 44322 # all interfaces" IO.println " nanochat 127.0.0.1 44322 # localhost only" return 1 IO.println "welcome to nanochat!" let (msgs, total) ← loadMessages logpath IO.println s!"starting up with {msgs.size} messages ({total} bytes)" let logHandle ← IO.FS.Handle.mk logpath .append let logRef ← IO.mkRef logHandle let stateRef ← IO.mkRef ({ messages := msgs, total := total : ServerState }) let addr ← match IPv4Addr.ofString host with | some a => pure a | none => IO.eprintln s!"invalid host address: {host}" return 1 let sockAddr := SocketAddress.v4 (SocketAddressV4.mk addr (UInt16.ofNat port)) let serverSock ← Std.Internal.UV.TCP.Socket.new serverSock.bind sockAddr serverSock.listen 128 IO.println s!"listening on {host}:{port}" try acceptLoop serverSock stateRef logRef catch e => IO.eprintln s!"server error: {e}" let st ← stateRef.get if st.restart then IO.println "restart requested (re-exec not supported in Lean; please restart manually)" IO.println "server stopped." return 0
-
-
README.md (new)
-
@@ -0,0 +1,15 @@# leanet Lean port of https://git.phial.org/d6/nanochat, unedited AI slop, somehow it just works™ Tested with https://git.sr.ht/~angelwood/picopico/ and https://git.quiltro.org/lobo/nanite This project was edited by [Aristotle](https://aristotle.harmonic.fun). To cite Aristotle: - Tag @Aristotle-Harmonic on GitHub PRs/issues - Add as co-author to commits: ``` Co-authored-by: Aristotle (Harmonic) <aristotle-harmonic@harmonic.fun> ```
-
-
-
@@ -1,7 +1,7 @@name = "leanet" version = "0.1.0" defaultTargets = ["leanet"] defaultTargets = ["main"] [[lean_exe]] name = "leanet" name = "main" root = "Main"
-
-
-
@@ -1,1 +1,1 @@leanprover/lean4:v4.22.0-rc3 leanprover/lean4:v4.28.0
-
-
picopico.nix (new)
-
@@ -0,0 +1,36 @@{ description = "Rust development environment"; inputs = { flake-utils.url = "github:numtide/flake-utils"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; }; outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; }; libPath = with pkgs; lib.makeLibraryPath [ libGL libxkbcommon wayland ]; in { devShells.default = with pkgs; mkShell { buildInputs = [ cargo rustc rust-analyzer ]; shellHook = '' export RUST_LOG=debug export RUST_SRC_PATH=${pkgs.rust.packages.stable.rustPlatform.rustLibSrc} export LD_LIBRARY_PATH=${libPath} ''; }; } ); }
-