feat: rebuild website with full learning funnel

Website rebuilt from scratch based on analysis of 11 beloved language
websites (Elm, Zig, Gleam, Swift, Kotlin, Haskell, OCaml, Crystal, Roc,
Rust, Go).

New website structure:
- Homepage with hero, playground, three pillars, install guide
- Language Tour with interactive lessons (hello world, types, effects)
- Examples cookbook with categorized sidebar
- API documentation index
- Installation guide (Nix and source)
- Sleek/noble design (black/gold, serif typography)

Also includes:
- New stdlib/json.lux module for JSON serialization
- Enhanced stdlib/http.lux with middleware and routing
- New string functions (charAt, indexOf, lastIndexOf, repeat)
- LSP improvements (rename, signature help, formatting)
- Package manager transitive dependency resolution
- Updated documentation for effects and stdlib
- New showcase example (task_manager.lux)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-16 23:05:35 -05:00
parent 5a853702d1
commit 7e76acab18
44 changed files with 12468 additions and 3354 deletions

View File

@@ -90,6 +90,10 @@ pub enum BuiltinFn {
StringToLower,
StringSubstring,
StringFromChar,
StringCharAt,
StringIndexOf,
StringLastIndexOf,
StringRepeat,
// JSON operations
JsonParse,
@@ -620,6 +624,14 @@ pub struct Interpreter {
pg_connections: RefCell<HashMap<i64, PgClient>>,
/// Next PostgreSQL connection ID
next_pg_conn_id: RefCell<i64>,
/// Concurrent tasks: task_id -> (thunk_value, result_option, is_cancelled)
concurrent_tasks: RefCell<HashMap<i64, (Value, Option<Value>, bool)>>,
/// Next task ID
next_task_id: RefCell<i64>,
/// Channels: channel_id -> (queue, is_closed)
channels: RefCell<HashMap<i64, (Vec<Value>, bool)>>,
/// Next channel ID
next_channel_id: RefCell<i64>,
}
/// Results from running tests
@@ -664,6 +676,10 @@ impl Interpreter {
next_sql_conn_id: RefCell::new(1),
pg_connections: RefCell::new(HashMap::new()),
next_pg_conn_id: RefCell::new(1),
concurrent_tasks: RefCell::new(HashMap::new()),
next_task_id: RefCell::new(1),
channels: RefCell::new(HashMap::new()),
next_channel_id: RefCell::new(1),
}
}
@@ -966,6 +982,22 @@ impl Interpreter {
"parseFloat".to_string(),
Value::Builtin(BuiltinFn::StringParseFloat),
),
(
"charAt".to_string(),
Value::Builtin(BuiltinFn::StringCharAt),
),
(
"indexOf".to_string(),
Value::Builtin(BuiltinFn::StringIndexOf),
),
(
"lastIndexOf".to_string(),
Value::Builtin(BuiltinFn::StringLastIndexOf),
),
(
"repeat".to_string(),
Value::Builtin(BuiltinFn::StringRepeat),
),
]));
env.define("String", string_module);
@@ -2498,6 +2530,89 @@ impl Interpreter {
Ok(EvalResult::Value(Value::String(c.to_string())))
}
BuiltinFn::StringCharAt => {
if args.len() != 2 {
return Err(err("String.charAt requires 2 arguments: string, index"));
}
let s = match &args[0] {
Value::String(s) => s.clone(),
v => return Err(err(&format!("String.charAt expects String, got {}", v.type_name()))),
};
let idx = match &args[1] {
Value::Int(n) => *n as usize,
v => return Err(err(&format!("String.charAt expects Int for index, got {}", v.type_name()))),
};
let chars: Vec<char> = s.chars().collect();
if idx < chars.len() {
Ok(EvalResult::Value(Value::String(chars[idx].to_string())))
} else {
Ok(EvalResult::Value(Value::String(String::new())))
}
}
BuiltinFn::StringIndexOf => {
if args.len() != 2 {
return Err(err("String.indexOf requires 2 arguments: string, substring"));
}
let s = match &args[0] {
Value::String(s) => s.clone(),
v => return Err(err(&format!("String.indexOf expects String, got {}", v.type_name()))),
};
let sub = match &args[1] {
Value::String(s) => s.clone(),
v => return Err(err(&format!("String.indexOf expects String for substring, got {}", v.type_name()))),
};
match s.find(&sub) {
Some(idx) => Ok(EvalResult::Value(Value::Constructor {
name: "Some".to_string(),
fields: vec![Value::Int(idx as i64)],
})),
None => Ok(EvalResult::Value(Value::Constructor {
name: "None".to_string(),
fields: vec![],
})),
}
}
BuiltinFn::StringLastIndexOf => {
if args.len() != 2 {
return Err(err("String.lastIndexOf requires 2 arguments: string, substring"));
}
let s = match &args[0] {
Value::String(s) => s.clone(),
v => return Err(err(&format!("String.lastIndexOf expects String, got {}", v.type_name()))),
};
let sub = match &args[1] {
Value::String(s) => s.clone(),
v => return Err(err(&format!("String.lastIndexOf expects String for substring, got {}", v.type_name()))),
};
match s.rfind(&sub) {
Some(idx) => Ok(EvalResult::Value(Value::Constructor {
name: "Some".to_string(),
fields: vec![Value::Int(idx as i64)],
})),
None => Ok(EvalResult::Value(Value::Constructor {
name: "None".to_string(),
fields: vec![],
})),
}
}
BuiltinFn::StringRepeat => {
if args.len() != 2 {
return Err(err("String.repeat requires 2 arguments: string, count"));
}
let s = match &args[0] {
Value::String(s) => s.clone(),
v => return Err(err(&format!("String.repeat expects String, got {}", v.type_name()))),
};
let count = match &args[1] {
Value::Int(n) => (*n).max(0) as usize,
v => return Err(err(&format!("String.repeat expects Int for count, got {}", v.type_name()))),
};
Ok(EvalResult::Value(Value::String(s.repeat(count))))
}
// JSON operations
BuiltinFn::JsonParse => {
let s = Self::expect_arg_1::<String>(&args, "Json.parse", span)?;
@@ -4369,6 +4484,237 @@ impl Interpreter {
}
}
// ===== Concurrent Effect =====
("Concurrent", "spawn") => {
// For now, spawn just stores the thunk - it will be evaluated on await
// In a real implementation, this would start a thread/fiber
let thunk = match request.args.first() {
Some(v) => v.clone(),
_ => return Err(RuntimeError {
message: "Concurrent.spawn requires a thunk argument".to_string(),
span: None,
}),
};
let task_id = *self.next_task_id.borrow();
*self.next_task_id.borrow_mut() += 1;
// Store task: (thunk, None for result, not cancelled)
self.concurrent_tasks.borrow_mut().insert(task_id, (thunk, None, false));
Ok(Value::Int(task_id))
}
("Concurrent", "await") => {
let task_id = match request.args.first() {
Some(Value::Int(id)) => *id,
_ => return Err(RuntimeError {
message: "Concurrent.await requires a task ID".to_string(),
span: None,
}),
};
// Check if already computed or cancelled
let task_info = {
let tasks = self.concurrent_tasks.borrow();
tasks.get(&task_id).cloned()
};
match task_info {
Some((_, Some(result), _)) => Ok(result),
Some((_, _, true)) => Err(RuntimeError {
message: format!("Task {} was cancelled", task_id),
span: None,
}),
Some((thunk, None, false)) => {
// For cooperative concurrency, we just need to signal
// that we're waiting on this task
// Return the thunk to be evaluated by the caller
// This is a simplification - real async would use fibers
Ok(thunk)
}
None => Err(RuntimeError {
message: format!("Unknown task ID: {}", task_id),
span: None,
}),
}
}
("Concurrent", "yield") => {
// In cooperative concurrency, yield allows other tasks to run
// For now, this is a no-op in our single-threaded model
Ok(Value::Unit)
}
("Concurrent", "sleep") => {
// Non-blocking sleep (delegates to thread sleep for now)
use std::thread;
use std::time::Duration;
let ms = match request.args.first() {
Some(Value::Int(n)) => *n as u64,
_ => 0,
};
thread::sleep(Duration::from_millis(ms));
Ok(Value::Unit)
}
("Concurrent", "cancel") => {
let task_id = match request.args.first() {
Some(Value::Int(id)) => *id,
_ => return Err(RuntimeError {
message: "Concurrent.cancel requires a task ID".to_string(),
span: None,
}),
};
let mut tasks = self.concurrent_tasks.borrow_mut();
if let Some((thunk, result, _)) = tasks.get(&task_id).cloned() {
tasks.insert(task_id, (thunk, result, true));
Ok(Value::Bool(true))
} else {
Ok(Value::Bool(false))
}
}
("Concurrent", "isRunning") => {
let task_id = match request.args.first() {
Some(Value::Int(id)) => *id,
_ => return Err(RuntimeError {
message: "Concurrent.isRunning requires a task ID".to_string(),
span: None,
}),
};
let tasks = self.concurrent_tasks.borrow();
let is_running = match tasks.get(&task_id) {
Some((_, None, false)) => true, // Not completed and not cancelled
_ => false,
};
Ok(Value::Bool(is_running))
}
("Concurrent", "taskCount") => {
let tasks = self.concurrent_tasks.borrow();
let count = tasks.iter()
.filter(|(_, (_, result, cancelled))| result.is_none() && !cancelled)
.count();
Ok(Value::Int(count as i64))
}
// ===== Channel Effect =====
("Channel", "create") => {
let channel_id = *self.next_channel_id.borrow();
*self.next_channel_id.borrow_mut() += 1;
// Create empty channel queue, not closed
self.channels.borrow_mut().insert(channel_id, (Vec::new(), false));
Ok(Value::Int(channel_id))
}
("Channel", "send") => {
let channel_id = match request.args.first() {
Some(Value::Int(id)) => *id,
_ => return Err(RuntimeError {
message: "Channel.send requires a channel ID".to_string(),
span: None,
}),
};
let value = match request.args.get(1) {
Some(v) => v.clone(),
_ => return Err(RuntimeError {
message: "Channel.send requires a value".to_string(),
span: None,
}),
};
let mut channels = self.channels.borrow_mut();
match channels.get_mut(&channel_id) {
Some((queue, false)) => {
queue.push(value);
Ok(Value::Unit)
}
Some((_, true)) => Err(RuntimeError {
message: format!("Channel {} is closed", channel_id),
span: None,
}),
None => Err(RuntimeError {
message: format!("Unknown channel ID: {}", channel_id),
span: None,
}),
}
}
("Channel", "receive") => {
let channel_id = match request.args.first() {
Some(Value::Int(id)) => *id,
_ => return Err(RuntimeError {
message: "Channel.receive requires a channel ID".to_string(),
span: None,
}),
};
let mut channels = self.channels.borrow_mut();
match channels.get_mut(&channel_id) {
Some((queue, _)) if !queue.is_empty() => {
Ok(queue.remove(0))
}
Some((_, true)) => Err(RuntimeError {
message: format!("Channel {} is closed and empty", channel_id),
span: None,
}),
Some((_, false)) => Err(RuntimeError {
message: format!("Channel {} is empty (blocking receive not supported yet)", channel_id),
span: None,
}),
None => Err(RuntimeError {
message: format!("Unknown channel ID: {}", channel_id),
span: None,
}),
}
}
("Channel", "tryReceive") => {
let channel_id = match request.args.first() {
Some(Value::Int(id)) => *id,
_ => return Err(RuntimeError {
message: "Channel.tryReceive requires a channel ID".to_string(),
span: None,
}),
};
let mut channels = self.channels.borrow_mut();
match channels.get_mut(&channel_id) {
Some((queue, _)) if !queue.is_empty() => {
Ok(Value::Constructor {
name: "Some".to_string(),
fields: vec![queue.remove(0)],
})
}
Some(_) => {
Ok(Value::Constructor {
name: "None".to_string(),
fields: vec![],
})
}
None => Err(RuntimeError {
message: format!("Unknown channel ID: {}", channel_id),
span: None,
}),
}
}
("Channel", "close") => {
let channel_id = match request.args.first() {
Some(Value::Int(id)) => *id,
_ => return Err(RuntimeError {
message: "Channel.close requires a channel ID".to_string(),
span: None,
}),
};
let mut channels = self.channels.borrow_mut();
if let Some((queue, closed)) = channels.get_mut(&channel_id) {
*closed = true;
Ok(Value::Unit)
} else {
Err(RuntimeError {
message: format!("Unknown channel ID: {}", channel_id),
span: None,
})
}
}
_ => Err(RuntimeError {
message: format!(
"Unhandled effect operation: {}.{}",