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:
@@ -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: {}.{}",
|
||||
|
||||
Reference in New Issue
Block a user