1 Commits

Author SHA1 Message Date
4f989271c5 Clarifications and waiver 2025-11-21 18:56:30 +01:00
12 changed files with 7 additions and 627 deletions

View File

@@ -1,3 +1,5 @@
THIS SOFTWARE IS PROVIDED WITHOUT WARRANTY
The code in this repository is free for noncommercial use, including derivative works and inclusion in other software if those are also free for noncommercial use. Commercial use, or inclusion in any derivative works licensed for commercial use is forbidden under this general licence.
Identifying marks stored in the repository are restricted for use with an unmodified copy of this software. If you distribute modified versions of this software, you must either replace these identifying marks or modify them in a way that clearly indicates that what you are distributing is a derivative work and not this official vversion. You must also replace any contact information in such a way that your derivative work does not suggest that we may be contacted about issues. Your derivative work may use the original identifying marks and contact information to identify this project as its basis, while emphasizing that the authors of the original project are neither in control of, nor liable for the derivative work.

View File

@@ -1,283 +0,0 @@
# Networking in Orchid
This document explains how to use TCP networking in the Orchid programming language.
## Prerequisites
Build the project with the nightly Rust toolchain:
```sh
rustup run nightly cargo build
```
## Socket Module Overview
The socket module is available at `std::socket::tcp` and provides functions for TCP client/server communication.
### Importing
```orchid
import std::socket::tcp::(bind, accept, connect, read, write_all, close)
```
Or import all functions:
```orchid
import std::socket::tcp
```
## API Reference
### Server Functions
| Function | Signature | Description |
|----------|-----------|-------------|
| `bind` | `String -> TcpListener` | Create a TCP listener bound to an address |
| `accept` | `TcpListener -> TcpStream` | Accept an incoming connection |
| `listener_addr` | `TcpListener -> String` | Get the address the listener is bound to |
### Client Functions
| Function | Signature | Description |
|----------|-----------|-------------|
| `connect` | `String -> TcpStream` | Connect to a TCP server |
### Stream Functions
| Function | Signature | Description |
|----------|-----------|-------------|
| `read` | `TcpStream -> Int -> String` | Read up to N bytes as UTF-8 string |
| `read_bytes` | `TcpStream -> Int -> String` | Read up to N bytes as raw byte string |
| `read_exact` | `TcpStream -> Int -> String` | Read exactly N bytes |
| `write` | `TcpStream -> String -> Int` | Write data, returns bytes written |
| `write_all` | `TcpStream -> String -> Int` | Write all data |
| `flush` | `TcpStream -> Int` | Flush the stream buffer |
| `close` | `TcpStream -> Int` | Close the connection |
| `peer_addr` | `TcpStream -> String` | Get the remote peer's address |
| `local_addr` | `TcpStream -> String` | Get the local address |
## Quick Start Examples
### Testing Socket Bind
```sh
rustup run nightly cargo orcx -- exec "std::socket::tcp::bind \"127.0.0.1:8080\""
```
Output:
```
<TcpListener 127.0.0.1:8080>
```
### Testing Connection (expect error if no server running)
```sh
rustup run nightly cargo orcx -- exec "std::socket::tcp::connect \"127.0.0.1:8080\""
```
Output (if no server):
```
error: IO error: Connection refused (os error 111): @
```
## Example Scripts
### TCP Echo Server
Location: `examples/tcp-echo-server/src/main.orc`
```orchid
import std::socket::tcp::(bind, accept, read, write_all, close)
const handle_client := \client. do cps {
cps data = read client 1024;
cps _ = write_all client data;
cps _ = close client;
cps pass 0;
}
const accept_loop := \server. do cps {
cps client = accept server;
cps _ = handle_client client;
cps pass $ accept_loop server;
}
const main := do cps {
cps server = bind "127.0.0.1:8080";
cps pass $ accept_loop server;
}
```
Run the echo server:
```sh
rustup run nightly cargo orcx -- exec --proj ./examples/tcp-echo-server "src::main::main"
```
### TCP Client
Location: `examples/tcp-client/src/main.orc`
```orchid
import std::socket::tcp::(connect, read, write_all, close, peer_addr)
import system::io::println
const main := do cps {
cps conn = connect "127.0.0.1:8080";
cps addr = peer_addr conn;
cps println $ "Connected to ${addr}";
cps _ = write_all conn "Hello from Orchid!\n";
cps response = read conn 1024;
cps println $ "Server response: ${response}";
cps _ = close conn;
cps pass 0;
}
```
Run the client (requires a server running on port 8080):
```sh
rustup run nightly cargo orcx -- exec --proj ./examples/tcp-client "src::main::main"
```
### HTTP Server
Location: `examples/http-server/src/main.orc`
```orchid
import std::socket::tcp::(bind, accept, read, write_all, close, peer_addr, listener_addr)
import system::io::println
const http_response := "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 44\r\nConnection: close\r\n\r\n<html><body><h1>Hello Orchid!</h1></body></html>"
const handle_client := \client. do cps {
cps addr = peer_addr client;
cps println $ "Client connected: ${addr}";
cps request = read client 4096;
cps println $ "Received request from ${addr}";
cps _ = write_all client http_response;
cps _ = close client;
cps println $ "Connection closed: ${addr}";
cps pass 0;
}
const accept_loop := \server. do cps {
cps client = accept server;
cps _ = handle_client client;
cps pass $ accept_loop server;
}
const main := do cps {
cps server = bind "127.0.0.1:8080";
cps addr = listener_addr server;
cps println $ "HTTP Server listening on ${addr}";
cps println "Press Ctrl+C to stop";
cps pass $ accept_loop server;
}
```
Run the HTTP server:
```sh
rustup run nightly cargo orcx -- exec --proj ./examples/http-server "src::main::main"
```
Then open http://127.0.0.1:8080 in your browser or test with curl:
```sh
curl http://127.0.0.1:8080
```
## Understanding the CPS Syntax
Orchid uses Continuation-Passing Style (CPS) for side effects. The `do cps { ... }` block is used for sequential operations:
- `cps variable = expression;` - Bind the result of an expression to a variable
- `cps expression;` - Execute an expression for its side effects
- `cps pass value;` - Return a value from the block
Example breakdown:
```orchid
const main := do cps {
cps server = bind "127.0.0.1:8080"; -- Bind socket, store in 'server'
cps client = accept server; -- Accept connection, store in 'client'
cps data = read client 1024; -- Read data from client
cps _ = write_all client data; -- Write data back (ignore result)
cps _ = close client; -- Close connection
cps pass 0; -- Return 0
}
```
## Error Handling
Socket operations return errors when they fail. Errors are displayed with the IO error message:
```
error: IO error: Connection refused (os error 111): @
```
Common errors:
- `Connection refused` - No server listening on the target address
- `Address already in use` - Port is already bound by another process
- `Connection reset by peer` - Remote end closed the connection unexpectedly
## Building a Simple Protocol
Here's an example of a simple request/response protocol:
```orchid
import std::socket::tcp::(bind, accept, read, write_all, close)
const handle_request := \request.
if request == "PING\n" then "PONG\n"
else if request == "TIME\n" then "2024-01-01 00:00:00\n"
else "UNKNOWN\n"
const handle_client := \client. do cps {
cps request = read client 1024;
cps response = pass $ handle_request request;
cps _ = write_all client response;
cps _ = close client;
cps pass 0;
}
const server_loop := \server. do cps {
cps client = accept server;
cps _ = handle_client client;
cps pass $ server_loop server;
}
const main := do cps {
cps server = bind "127.0.0.1:9000";
cps pass $ server_loop server;
}
```
## Limitations
- The current implementation handles one client at a time (sequential accept loop)
- Sockets are non-serializable (cannot be persisted or transferred across boundaries)
- No UDP support (TCP only)
- No TLS/SSL support
## Troubleshooting
### Port Already in Use
If you get "Address already in use", either:
1. Wait for the previous process to release the port
2. Use a different port
3. Kill the process using the port: `lsof -i :8080` then `kill <pid>`
### Connection Refused
Ensure the server is running before starting the client.
### Build Errors
Make sure to use the nightly toolchain:
```sh
rustup run nightly cargo build
```

View File

@@ -35,14 +35,16 @@ Orchids and mangrove trees form complex ecosystems; The flowers persuade the tre
All contributions are welcome. For the time being, use the issue tracker to discuss ideas.
Unless we agree on different terms, by contributing to this software you declare that you have created or otherwise have the right to license your contribution, agree to license it publicly under the general noncommercial licence included in this repository, and grant me (the owner of the project) a permanent, unrestricted license to use, modify, distribute and relicense your contribution under any terms I see fit.
Unless we agree on different terms, by contributing to this software you declare that you have created or otherwise have the right to license your contribution, agree to license it publicly under the general noncommercial licence included in this repository, and grant me (the owner of the project) a permanent, unrestricted license to use, modify, distribute and relicense your contribution.
You retain ownership of your intellectual property to ensure that the copyleft protections cementing the noncommercial availability of the code are preserved.
## About the license
This software is free for noncommercial use. If you would like to use it for commercial purposes, or distribute your derivative work under a license that permits commercial use, contact me for a separate license. These licences are provided on a case-by-case basis with any limitations and compensation we agree on.
I generally appreciate the ethos of open-source software, and particularly the patterns used in copyleft to cement the longevity of the guarantees of the licence. However, I don't think commercial entities fit that ethos, and I think they should be addressed separately rather than attempting to ignore the inherent unfairness towards contributors.
I generally appreciate the ethos of free software, and particularly the patterns used in copyleft to cement the guarantees of the licence. However, I don't think commercial entities fit that ethos, and I think they should be addressed separately rather than attempting to ignore the inherent unfairness towards contributors.
My intent with the custom license included in this project is to enable the strong guarantees of open-source towards open-source, while leaving commercial users to engage with this project and its possible future ecosystem in a commercial way; if you intend to profit off my work, the barest cash flow should justify shooting me an email and agreeing on a simple temporary profit sharing deal until you figure out your business model, and the cash flow of a full scale business should more than justify dedicated attention to the software you rely on.
My intent with the custom license included in this project is to enable the strong guarantees of copyleft towards noncommercial users, while leaving commercial users to engage with this project and its possible future ecosystem in a commercial way; if you intend to profit off my work, the barest cash flow should justify shooting me an email and agreeing on a simple temporary profit sharing deal until you figure out your business model, and the cash flow of a full scale business should more than justify dedicated attention to the software you rely on.
The clause about identifying marks is intended to prevent another pitfall of open-source, wherein Linux distros borrow entire codebases, break them, and then distribute the result under the original author's name. If would like to package Orchid, I'd be delighted if you would talk to me about making it official, but if you would rather operate independently, you should present your project as the rogue derivative work that it is rather than borrowing the original project's identity for something its owner has no control over.

View File

@@ -1,29 +0,0 @@
import std::socket::tcp::(bind, accept, read, write_all, close, peer_addr, listener_addr)
import system::io::println
let http_response = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 44\r\nConnection: close\r\n\r\n<html><body><h1>Hello Orchid!</h1></body></html>"
let handle_client = \client. do cps {
cps addr = peer_addr client;
cps println $ "Client connected: ${addr}";
cps request = read client 4096;
cps println $ "Received request from ${addr}";
cps _ = write_all client http_response;
cps _ = close client;
cps println $ "Connection closed: ${addr}";
cps pass 0;
}
let accept_loop = \server. do cps {
cps client = accept server;
cps _ = handle_client client;
cps pass $ accept_loop server;
}
let main = do cps {
cps server = bind "127.0.0.1:8080";
cps addr = listener_addr server;
cps println $ "HTTP Server listening on ${addr}";
cps println "Press Ctrl+C to stop";
cps pass $ accept_loop server;
}

View File

@@ -1,31 +0,0 @@
import std::socket::tcp::(bind, accept, read, write_all, close, peer_addr, listener_addr)
import system::io::println
cps println $ "Hello Orchid!";
const http_response := "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 44\r\nConnection: close\r\n\r\n<html><body><h1>Hello Orchid!</h1></body></html>"
const handle_client := \client. do cps {
cps addr = peer_addr client;
cps println $ "Client connected: ${addr}";
cps request = read client 4096;
cps println $ "Received request from ${addr}";
cps _ = write_all client http_response;
cps _ = close client;
cps println $ "Connection closed: ${addr}";
cps pass 0;
}
const accept_loop := \server. do cps {
cps client = accept server;
cps _ = handle_client client;
cps pass $ accept_loop server;
}
const main := do cps {
cps server = bind "127.0.0.1:8080";
cps addr = listener_addr server;
cps println $ "HTTP Server listening on ${addr}";
cps println "Press Ctrl+C to stop";
cps pass $ accept_loop server;
}

View File

@@ -1,13 +0,0 @@
import std::socket::tcp::(connect, read, write_all, close, peer_addr)
import system::io::println
const main := do cps {
cps conn = connect "127.0.0.1:8080";
cps addr = peer_addr conn;
cps println $ "Connected to ${addr}";
cps _ = write_all conn "Hello from Orchid!\n";
cps response = read conn 1024;
cps println $ "Server response: ${response}";
cps _ = close conn;
cps pass 0;
}

View File

@@ -1,19 +0,0 @@
import std::socket::tcp::(bind, accept, read, write_all, close)
const handle_client := \client. do cps {
cps data = read client 1024;
cps _ = write_all client data;
cps _ = close client;
cps pass 0;
}
const accept_loop := \server. do cps {
cps client = accept server;
cps _ = handle_client client;
cps pass $ accept_loop server;
}
const main := do cps {
cps server = bind "127.0.0.1:8080";
cps pass $ accept_loop server;
}

View File

@@ -3,7 +3,6 @@ pub mod option;
pub mod protocol;
pub mod record;
pub mod reflection;
pub mod socket;
pub mod std_system;
pub mod string;
pub mod tuple;

View File

@@ -1,2 +0,0 @@
pub mod socket_atom;
pub mod socket_lib;

View File

@@ -1,110 +0,0 @@
use std::borrow::Cow;
use std::rc::Rc;
use std::cell::RefCell;
use futures::lock::Mutex;
use never::Never;
use orchid_base::format::{FmtCtx, FmtUnit};
use orchid_extension::atom::{Atomic, MethodSetBuilder};
use orchid_extension::atom_owned::{OwnedAtom, OwnedVariant};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
pub struct TcpListenerAtom {
pub listener: Rc<Mutex<TcpListener>>,
pub addr: String,
}
impl Clone for TcpListenerAtom {
fn clone(&self) -> Self {
Self {
listener: self.listener.clone(),
addr: self.addr.clone(),
}
}
}
impl Atomic for TcpListenerAtom {
type Variant = OwnedVariant;
type Data = ();
fn reg_reqs() -> MethodSetBuilder<Self> { MethodSetBuilder::new() }
}
impl OwnedAtom for TcpListenerAtom {
type Refs = Never;
async fn val(&self) -> Cow<'_, Self::Data> { Cow::Owned(()) }
async fn print_atom<'a>(&'a self, _: &'a (impl FmtCtx + ?Sized + 'a)) -> FmtUnit {
format!("<TcpListener {}>", self.addr).into()
}
}
pub struct TcpStreamAtom {
pub stream: Rc<RefCell<Option<TcpStream>>>,
pub peer_addr: String,
pub local_addr: String,
}
impl Clone for TcpStreamAtom {
fn clone(&self) -> Self {
Self {
stream: self.stream.clone(),
peer_addr: self.peer_addr.clone(),
local_addr: self.local_addr.clone(),
}
}
}
impl Atomic for TcpStreamAtom {
type Variant = OwnedVariant;
type Data = ();
fn reg_reqs() -> MethodSetBuilder<Self> { MethodSetBuilder::new() }
}
impl OwnedAtom for TcpStreamAtom {
type Refs = Never;
async fn val(&self) -> Cow<'_, Self::Data> { Cow::Owned(()) }
async fn print_atom<'a>(&'a self, _: &'a (impl FmtCtx + ?Sized + 'a)) -> FmtUnit {
format!("<TcpStream {} -> {}>", self.local_addr, self.peer_addr).into()
}
}
impl TcpStreamAtom {
pub async fn read(&self, max_bytes: usize) -> std::io::Result<Vec<u8>> {
let mut stream_opt = self.stream.borrow_mut();
let stream = stream_opt.as_mut().ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::NotConnected, "Stream closed")
})?;
let mut buf = vec![0u8; max_bytes];
let n = stream.read(&mut buf).await?;
buf.truncate(n);
Ok(buf)
}
pub async fn write(&self, data: &[u8]) -> std::io::Result<usize> {
let mut stream_opt = self.stream.borrow_mut();
let stream = stream_opt.as_mut().ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::NotConnected, "Stream closed")
})?;
stream.write(data).await
}
pub async fn write_all(&self, data: &[u8]) -> std::io::Result<()> {
let mut stream_opt = self.stream.borrow_mut();
let stream = stream_opt.as_mut().ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::NotConnected, "Stream closed")
})?;
stream.write_all(data).await
}
pub async fn flush(&self) -> std::io::Result<()> {
let mut stream_opt = self.stream.borrow_mut();
let stream = stream_opt.as_mut().ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::NotConnected, "Stream closed")
})?;
stream.flush().await
}
pub fn close(&self) {
let _ = self.stream.borrow_mut().take();
}
}

View File

@@ -1,131 +0,0 @@
use std::cell::RefCell;
use std::rc::Rc;
use futures::lock::Mutex;
use orchid_base::error::{OrcRes, OrcErrv, mk_errv_floating};
use orchid_extension::atom::TAtom;
use orchid_extension::atom_owned::own;
use orchid_extension::context::i;
use orchid_extension::conv::TryFromExpr;
use orchid_extension::expr::Expr;
use orchid_extension::tree::{GenMember, fun, prefix};
use tokio::net::{TcpListener, TcpStream};
use super::socket_atom::{TcpListenerAtom, TcpStreamAtom};
use crate::std::string::str_atom::StrAtom;
use crate::{Int, OrcString};
async fn io_err(e: std::io::Error) -> OrcErrv {
mk_errv_floating(i().i(&format!("IO error: {}", e)).await, "")
}
macro_rules! try_io {
($e:expr) => {
match $e {
Ok(v) => v,
Err(e) => return Err(io_err(e).await),
}
};
}
impl TryFromExpr for TcpListenerAtom {
async fn try_from_expr(expr: Expr) -> OrcRes<Self> {
let tatom = TAtom::<TcpListenerAtom>::try_from_expr(expr).await?;
Ok(own(&tatom).await)
}
}
impl TryFromExpr for TcpStreamAtom {
async fn try_from_expr(expr: Expr) -> OrcRes<Self> {
let tatom = TAtom::<TcpStreamAtom>::try_from_expr(expr).await?;
Ok(own(&tatom).await)
}
}
pub fn gen_socket_lib() -> Vec<GenMember> {
prefix("std::socket", [
prefix("tcp", [
fun(true, "bind", async |addr: OrcString| -> OrcRes<TcpListenerAtom> {
let addr_str = addr.get_string().await;
let listener = try_io!(TcpListener::bind(addr_str.as_str()).await);
let local_addr = listener.local_addr().map(|a| a.to_string()).unwrap_or_default();
Ok(TcpListenerAtom {
listener: Rc::new(Mutex::new(listener)),
addr: local_addr,
})
}),
fun(true, "accept", async |listener: TcpListenerAtom| -> OrcRes<TcpStreamAtom> {
let guard = listener.listener.lock().await;
let (stream, peer_addr) = try_io!(guard.accept().await);
let local_addr = stream.local_addr().map(|a| a.to_string()).unwrap_or_default();
Ok(TcpStreamAtom {
stream: Rc::new(RefCell::new(Some(stream))),
peer_addr: peer_addr.to_string(),
local_addr,
})
}),
fun(true, "connect", async |addr: OrcString| -> OrcRes<TcpStreamAtom> {
let addr_str = addr.get_string().await;
let stream = try_io!(TcpStream::connect(addr_str.as_str()).await);
let peer_addr = stream.peer_addr().map(|a| a.to_string()).unwrap_or_default();
let local_addr = stream.local_addr().map(|a| a.to_string()).unwrap_or_default();
Ok(TcpStreamAtom {
stream: Rc::new(RefCell::new(Some(stream))),
peer_addr,
local_addr,
})
}),
fun(true, "read", async |stream: TcpStreamAtom, max_bytes: Int| -> OrcRes<StrAtom> {
let data = try_io!(stream.read(max_bytes.0 as usize).await);
let s = String::from_utf8_lossy(&data).to_string();
Ok(StrAtom::new(Rc::new(s)))
}),
fun(true, "read_bytes", async |stream: TcpStreamAtom, max_bytes: Int| -> OrcRes<StrAtom> {
let data = try_io!(stream.read(max_bytes.0 as usize).await);
let s = data.iter().map(|b| *b as char).collect::<String>();
Ok(StrAtom::new(Rc::new(s)))
}),
fun(true, "read_exact", async |stream: TcpStreamAtom, num_bytes: Int| -> OrcRes<StrAtom> {
let mut result = Vec::new();
let target = num_bytes.0 as usize;
while result.len() < target {
let remaining = target - result.len();
let chunk = try_io!(stream.read(remaining).await);
if chunk.is_empty() {
return Err(mk_errv_floating(i().i("Connection closed before receiving all bytes").await, ""));
}
result.extend(chunk);
}
let s = String::from_utf8_lossy(&result).to_string();
Ok(StrAtom::new(Rc::new(s)))
}),
fun(true, "write", async |stream: TcpStreamAtom, data: OrcString| -> OrcRes<Int> {
let data_str = data.get_string().await;
let n = try_io!(stream.write(data_str.as_bytes()).await);
Ok(Int(n as i64))
}),
fun(true, "write_all", async |stream: TcpStreamAtom, data: OrcString| -> OrcRes<Int> {
let data_str = data.get_string().await;
try_io!(stream.write_all(data_str.as_bytes()).await);
Ok(Int(0))
}),
fun(true, "flush", async |stream: TcpStreamAtom| -> OrcRes<Int> {
try_io!(stream.flush().await);
Ok(Int(0))
}),
fun(true, "close", async |stream: TcpStreamAtom| -> Int {
stream.close();
Int(0)
}),
fun(true, "peer_addr", async |stream: TcpStreamAtom| {
StrAtom::new(Rc::new(stream.peer_addr.clone()))
}),
fun(true, "local_addr", async |stream: TcpStreamAtom| {
StrAtom::new(Rc::new(stream.local_addr.clone()))
}),
fun(true, "listener_addr", async |listener: TcpListenerAtom| {
StrAtom::new(Rc::new(listener.addr.clone()))
}),
]),
])
}

View File

@@ -27,8 +27,6 @@ use crate::std::protocol::types::{Tag, Tagged, gen_protocol_lib};
use crate::std::record::record_atom::Record;
use crate::std::record::record_lib::gen_record_lib;
use crate::std::reflection::sym_atom::{CreateSymAtom, SymAtom, gen_sym_lib};
use crate::std::socket::socket_atom::{TcpListenerAtom, TcpStreamAtom};
use crate::std::socket::socket_lib::gen_socket_lib;
use crate::std::string::str_lexer::StringLexer;
use crate::std::string::to_string::AsStrTag;
use crate::std::tuple::{CreateTuple, Tuple, TupleBuilder, gen_tuple_lib};
@@ -66,8 +64,6 @@ impl SystemCard for StdSystem {
Some(Tag::dynfo()),
Some(Tagged::dynfo()),
Some(AsStrTag::dynfo()),
Some(TcpListenerAtom::dynfo()),
Some(TcpStreamAtom::dynfo()),
]
}
}
@@ -96,7 +92,6 @@ impl System for StdSystem {
gen_tuple_lib(),
gen_protocol_lib(),
gen_sym_lib().await,
gen_socket_lib(),
])
}
async fn prelude() -> Vec<Sym> {