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

@@ -15,6 +15,7 @@ mod package;
mod parser;
mod registry;
mod schema;
mod symbol_table;
mod typechecker;
mod types;
@@ -47,8 +48,10 @@ Commands:
:env Show user-defined bindings
:clear Clear the environment
:load <file> Load and execute a file
:reload, :r Reload the last loaded file
:trace on/off Enable/disable effect tracing
:traces Show recorded effect traces
:ast <expr> Show the AST of an expression (for debugging)
Keyboard:
Tab Autocomplete
@@ -57,6 +60,10 @@ Keyboard:
Up/Down Browse history
Ctrl-R Search history
Effects:
All code in the REPL runs with Console, File, and other standard effects.
Use :trace on to see effect invocations during execution.
Examples:
> let x = 42
> x + 1
@@ -65,6 +72,9 @@ Examples:
> fn double(n: Int): Int = n * 2
> :type double
double : fn(Int) -> Int
> :load myfile.lux
> :reload
> double(21)
42
@@ -175,6 +185,10 @@ fn main() {
compile_to_c(&args[2], output_path, run_after, emit_c);
}
}
"doc" => {
// Generate API documentation
generate_docs(&args[2..]);
}
path => {
// Run a file
run_file(path);
@@ -207,6 +221,8 @@ fn print_help() {
println!(" lux registry Start package registry server");
println!(" -s, --storage <dir> Storage directory (default: ./lux-registry)");
println!(" -b, --bind <addr> Bind address (default: 127.0.0.1:8080)");
println!(" lux doc [file] [-o dir] Generate API documentation (HTML)");
println!(" --json Output as JSON");
println!(" lux --lsp Start LSP server (for IDE integration)");
println!(" lux --help Show this help");
println!(" lux --version Show version");
@@ -1428,6 +1444,681 @@ let output = run main() with {}
println!(" lux src/main.lux");
}
/// Generate API documentation for Lux source files
fn generate_docs(args: &[String]) {
use std::path::Path;
use std::collections::HashMap;
let output_json = args.iter().any(|a| a == "--json");
let output_dir = args.iter()
.position(|a| a == "-o")
.and_then(|i| args.get(i + 1))
.map(|s| s.as_str())
.unwrap_or("docs");
let input_file = args.iter().find(|a| !a.starts_with('-') && *a != output_dir);
// Collect files to document
let mut files_to_doc = Vec::new();
if let Some(path) = input_file {
if Path::new(path).is_file() {
files_to_doc.push(path.to_string());
} else {
eprintln!("File not found: {}", path);
std::process::exit(1);
}
} else {
// Auto-discover files
if Path::new("src").is_dir() {
collect_lux_files_for_docs("src", &mut files_to_doc);
}
if Path::new("stdlib").is_dir() {
collect_lux_files_for_docs("stdlib", &mut files_to_doc);
}
}
if files_to_doc.is_empty() {
eprintln!("No .lux files found to document");
std::process::exit(1);
}
// Create output directory
if !output_json {
if let Err(e) = std::fs::create_dir_all(output_dir) {
eprintln!("Failed to create output directory: {}", e);
std::process::exit(1);
}
}
let mut all_docs: HashMap<String, ModuleDoc> = HashMap::new();
let mut error_count = 0;
for file_path in &files_to_doc {
let source = match std::fs::read_to_string(file_path) {
Ok(s) => s,
Err(e) => {
eprintln!("{}: ERROR - {}", file_path, e);
error_count += 1;
continue;
}
};
match extract_module_doc(&source, file_path) {
Ok(doc) => {
let module_name = Path::new(file_path)
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "unknown".to_string());
all_docs.insert(module_name, doc);
}
Err(e) => {
eprintln!("{}: PARSE ERROR - {}", file_path, e);
error_count += 1;
}
}
}
if output_json {
// Output as JSON
println!("{}", docs_to_json(&all_docs));
} else {
// Generate HTML files
let index_html = generate_index_html(&all_docs);
let index_path = format!("{}/index.html", output_dir);
if let Err(e) = std::fs::write(&index_path, &index_html) {
eprintln!("Failed to write index.html: {}", e);
error_count += 1;
}
for (module_name, doc) in &all_docs {
let html = generate_module_html(module_name, doc);
let path = format!("{}/{}.html", output_dir, module_name);
if let Err(e) = std::fs::write(&path, &html) {
eprintln!("Failed to write {}: {}", path, e);
error_count += 1;
}
}
// Generate CSS
let css_path = format!("{}/style.css", output_dir);
if let Err(e) = std::fs::write(&css_path, DOC_CSS) {
eprintln!("Failed to write style.css: {}", e);
error_count += 1;
}
println!("Generated documentation in {}/", output_dir);
println!(" {} modules documented", all_docs.len());
if error_count > 0 {
println!(" {} errors", error_count);
}
}
}
fn collect_lux_files_for_docs(dir: &str, files: &mut Vec<String>) {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() && path.extension().map(|e| e == "lux").unwrap_or(false) {
files.push(path.to_string_lossy().to_string());
} else if path.is_dir() {
collect_lux_files_for_docs(&path.to_string_lossy(), files);
}
}
}
}
#[derive(Debug, Clone)]
struct ModuleDoc {
description: Option<String>,
functions: Vec<FunctionDoc>,
types: Vec<TypeDoc>,
effects: Vec<EffectDoc>,
}
#[derive(Debug, Clone)]
struct FunctionDoc {
name: String,
signature: String,
description: Option<String>,
is_public: bool,
properties: Vec<String>,
}
#[derive(Debug, Clone)]
struct TypeDoc {
name: String,
definition: String,
description: Option<String>,
is_public: bool,
}
#[derive(Debug, Clone)]
struct EffectDoc {
name: String,
operations: Vec<String>,
description: Option<String>,
}
fn extract_module_doc(source: &str, path: &str) -> Result<ModuleDoc, String> {
use modules::ModuleLoader;
use std::path::Path;
let mut loader = ModuleLoader::new();
let file_path = Path::new(path);
if let Some(parent) = file_path.parent() {
loader.add_search_path(parent.to_path_buf());
}
let program = loader.load_source(source, Some(file_path))
.map_err(|e| format!("{}", e))?;
let mut module_desc: Option<String> = None;
let mut functions = Vec::new();
let mut types = Vec::new();
let mut effects = Vec::new();
let mut pending_doc: Option<String> = None;
// Extract module-level comment (first comment before any declarations)
let lines: Vec<&str> = source.lines().collect();
let mut module_comment = Vec::new();
for line in &lines {
let trimmed = line.trim();
if trimmed.starts_with("//") {
module_comment.push(trimmed.trim_start_matches('/').trim());
} else if !trimmed.is_empty() {
break;
}
}
if !module_comment.is_empty() {
module_desc = Some(module_comment.join("\n"));
}
for decl in &program.declarations {
match decl {
ast::Declaration::Function(f) => {
// Build signature
let params: Vec<String> = f.params.iter()
.map(|p| format!("{}: {}", p.name.name, format_type(&p.typ)))
.collect();
let effects_str = if f.effects.is_empty() {
String::new()
} else {
format!(" with {{{}}}", f.effects.iter().map(|e| e.name.clone()).collect::<Vec<_>>().join(", "))
};
let props: Vec<String> = f.properties.iter()
.map(|p| format!("{:?}", p).to_lowercase())
.collect();
let props_str = if props.is_empty() {
String::new()
} else {
format!(" is {}", props.join(", "))
};
let signature = format!(
"fn {}({}): {}{}{}",
f.name.name,
params.join(", "),
format_type(&f.return_type),
props_str,
effects_str
);
// Extract doc comment
let doc = extract_doc_comment(source, f.span.start);
functions.push(FunctionDoc {
name: f.name.name.clone(),
signature,
description: doc,
is_public: matches!(f.visibility, ast::Visibility::Public),
properties: props,
});
}
ast::Declaration::Type(t) => {
let doc = extract_doc_comment(source, t.span.start);
types.push(TypeDoc {
name: t.name.name.clone(),
definition: format_type_def(t),
description: doc,
is_public: matches!(t.visibility, ast::Visibility::Public),
});
}
ast::Declaration::Effect(e) => {
let doc = extract_doc_comment(source, e.span.start);
let ops: Vec<String> = e.operations.iter()
.map(|op| {
let params: Vec<String> = op.params.iter()
.map(|p| format!("{}: {}", p.name.name, format_type(&p.typ)))
.collect();
format!("{}({}): {}", op.name.name, params.join(", "), format_type(&op.return_type))
})
.collect();
effects.push(EffectDoc {
name: e.name.name.clone(),
operations: ops,
description: doc,
});
}
_ => {}
}
}
Ok(ModuleDoc {
description: module_desc,
functions,
types,
effects,
})
}
fn extract_doc_comment(source: &str, pos: usize) -> Option<String> {
// Look backwards from the declaration for doc comments
let prefix = &source[..pos];
let lines: Vec<&str> = prefix.lines().collect();
let mut doc_lines = Vec::new();
for line in lines.iter().rev() {
let trimmed = line.trim();
if trimmed.starts_with("///") {
doc_lines.push(trimmed.trim_start_matches('/').trim());
} else if trimmed.starts_with("//") {
// Regular comment, skip
continue;
} else if trimmed.is_empty() {
if !doc_lines.is_empty() {
break;
}
} else {
break;
}
}
if doc_lines.is_empty() {
None
} else {
doc_lines.reverse();
Some(doc_lines.join("\n"))
}
}
fn format_type(t: &ast::TypeExpr) -> String {
match t {
ast::TypeExpr::Named(ident) => ident.name.clone(),
ast::TypeExpr::App(base, args) => {
let args_str: Vec<String> = args.iter().map(format_type).collect();
format!("{}<{}>", format_type(base), args_str.join(", "))
}
ast::TypeExpr::Function { params, return_type, .. } => {
let params_str: Vec<String> = params.iter().map(format_type).collect();
format!("fn({}): {}", params_str.join(", "), format_type(return_type))
}
ast::TypeExpr::Tuple(types) => {
let types_str: Vec<String> = types.iter().map(format_type).collect();
format!("({})", types_str.join(", "))
}
ast::TypeExpr::Record(fields) => {
let fields_str: Vec<String> = fields.iter()
.map(|f| format!("{}: {}", f.name.name, format_type(&f.typ)))
.collect();
format!("{{ {} }}", fields_str.join(", "))
}
ast::TypeExpr::Unit => "Unit".to_string(),
ast::TypeExpr::Versioned { base, .. } => format_type(base),
}
}
fn format_type_def(t: &ast::TypeDecl) -> String {
match &t.definition {
ast::TypeDef::Alias(typ) => format!("type {} = {}", t.name.name, format_type(typ)),
ast::TypeDef::Enum(variants) => {
let variants_str: Vec<String> = variants.iter()
.map(|v| {
match &v.fields {
ast::VariantFields::Unit => v.name.name.clone(),
ast::VariantFields::Tuple(types) => {
let types_str: Vec<String> = types.iter().map(format_type).collect();
format!("{}({})", v.name.name, types_str.join(", "))
}
ast::VariantFields::Record(fields) => {
let fields_str: Vec<String> = fields.iter()
.map(|f| format!("{}: {}", f.name.name, format_type(&f.typ)))
.collect();
format!("{}{{ {} }}", v.name.name, fields_str.join(", "))
}
}
})
.collect();
format!("type {} = {}", t.name.name, variants_str.join(" | "))
}
ast::TypeDef::Record(fields) => {
let fields_str: Vec<String> = fields.iter()
.map(|f| format!("{}: {}", f.name.name, format_type(&f.typ)))
.collect();
format!("type {} = {{ {} }}", t.name.name, fields_str.join(", "))
}
}
}
fn docs_to_json(docs: &std::collections::HashMap<String, ModuleDoc>) -> String {
let mut json = String::from("{\n");
let mut first_module = true;
for (name, doc) in docs {
if !first_module {
json.push_str(",\n");
}
first_module = false;
json.push_str(&format!(" \"{}\": {{\n", escape_json(name)));
if let Some(desc) = &doc.description {
json.push_str(&format!(" \"description\": \"{}\",\n", escape_json(desc)));
}
// Functions
json.push_str(" \"functions\": [\n");
for (i, f) in doc.functions.iter().enumerate() {
json.push_str(&format!(
" {{\"name\": \"{}\", \"signature\": \"{}\", \"public\": {}, \"description\": {}}}",
escape_json(&f.name),
escape_json(&f.signature),
f.is_public,
f.description.as_ref().map(|d| format!("\"{}\"", escape_json(d))).unwrap_or("null".to_string())
));
if i < doc.functions.len() - 1 {
json.push(',');
}
json.push('\n');
}
json.push_str(" ],\n");
// Types
json.push_str(" \"types\": [\n");
for (i, t) in doc.types.iter().enumerate() {
json.push_str(&format!(
" {{\"name\": \"{}\", \"definition\": \"{}\", \"public\": {}, \"description\": {}}}",
escape_json(&t.name),
escape_json(&t.definition),
t.is_public,
t.description.as_ref().map(|d| format!("\"{}\"", escape_json(d))).unwrap_or("null".to_string())
));
if i < doc.types.len() - 1 {
json.push(',');
}
json.push('\n');
}
json.push_str(" ],\n");
// Effects
json.push_str(" \"effects\": [\n");
for (i, e) in doc.effects.iter().enumerate() {
let ops_json: Vec<String> = e.operations.iter()
.map(|o| format!("\"{}\"", escape_json(o)))
.collect();
json.push_str(&format!(
" {{\"name\": \"{}\", \"operations\": [{}], \"description\": {}}}",
escape_json(&e.name),
ops_json.join(", "),
e.description.as_ref().map(|d| format!("\"{}\"", escape_json(d))).unwrap_or("null".to_string())
));
if i < doc.effects.len() - 1 {
json.push(',');
}
json.push('\n');
}
json.push_str(" ]\n");
json.push_str(" }");
}
json.push_str("\n}");
json
}
fn escape_json(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
fn generate_index_html(docs: &std::collections::HashMap<String, ModuleDoc>) -> String {
let mut html = String::from(r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lux API Documentation</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>Lux API Documentation</h1>
</header>
<main>
<h2>Modules</h2>
<ul class="module-list">
"#);
let mut modules: Vec<_> = docs.keys().collect();
modules.sort();
for name in modules {
let doc = &docs[name];
let desc = doc.description.as_ref()
.map(|d| d.lines().next().unwrap_or(""))
.unwrap_or("");
html.push_str(&format!(
" <li><a href=\"{}.html\">{}</a> - {}</li>\n",
name, name, html_escape(desc)
));
}
html.push_str(r#" </ul>
</main>
</body>
</html>"#);
html
}
fn generate_module_html(name: &str, doc: &ModuleDoc) -> String {
let mut html = format!(r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{} - Lux API</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<a href="index.html">Back to Index</a>
<h1>{}</h1>
</header>
<main>
"#, name, name);
if let Some(desc) = &doc.description {
html.push_str(&format!(" <div class=\"module-description\">{}</div>\n", html_escape(desc)));
}
// Types
if !doc.types.is_empty() {
html.push_str(" <section>\n <h2>Types</h2>\n");
for t in &doc.types {
let visibility = if t.is_public { "pub " } else { "" };
html.push_str(&format!(
" <div class=\"item\">\n <code class=\"signature\">{}{}</code>\n",
visibility, html_escape(&t.definition)
));
if let Some(desc) = &t.description {
html.push_str(&format!(" <p class=\"description\">{}</p>\n", html_escape(desc)));
}
html.push_str(" </div>\n");
}
html.push_str(" </section>\n");
}
// Effects
if !doc.effects.is_empty() {
html.push_str(" <section>\n <h2>Effects</h2>\n");
for e in &doc.effects {
html.push_str(&format!(
" <div class=\"item\">\n <h3>effect {}</h3>\n",
html_escape(&e.name)
));
if let Some(desc) = &e.description {
html.push_str(&format!(" <p class=\"description\">{}</p>\n", html_escape(desc)));
}
html.push_str(" <ul class=\"operations\">\n");
for op in &e.operations {
html.push_str(&format!(" <li><code>{}</code></li>\n", html_escape(op)));
}
html.push_str(" </ul>\n </div>\n");
}
html.push_str(" </section>\n");
}
// Functions
if !doc.functions.is_empty() {
html.push_str(" <section>\n <h2>Functions</h2>\n");
for f in &doc.functions {
let visibility = if f.is_public { "pub " } else { "" };
html.push_str(&format!(
" <div class=\"item\" id=\"{}\">\n <code class=\"signature\">{}{}</code>\n",
f.name, visibility, html_escape(&f.signature)
));
if let Some(desc) = &f.description {
html.push_str(&format!(" <p class=\"description\">{}</p>\n", html_escape(desc)));
}
html.push_str(" </div>\n");
}
html.push_str(" </section>\n");
}
html.push_str(r#" </main>
</body>
</html>"#);
html
}
fn html_escape(s: &str) -> String {
s.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
}
const DOC_CSS: &str = r#"
:root {
--bg-color: #1a1a2e;
--text-color: #e0e0e0;
--link-color: #64b5f6;
--code-bg: #16213e;
--header-bg: #0f3460;
--accent: #e94560;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
margin: 0;
padding: 0;
line-height: 1.6;
}
header {
background-color: var(--header-bg);
padding: 1rem 2rem;
border-bottom: 2px solid var(--accent);
}
header h1 {
margin: 0;
color: white;
}
header a {
color: var(--link-color);
text-decoration: none;
font-size: 0.9rem;
}
main {
max-width: 900px;
margin: 0 auto;
padding: 2rem;
}
h2 {
color: var(--accent);
border-bottom: 1px solid var(--accent);
padding-bottom: 0.5rem;
}
h3 {
color: var(--link-color);
}
.module-list {
list-style: none;
padding: 0;
}
.module-list li {
padding: 0.5rem 0;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.module-list a {
color: var(--link-color);
text-decoration: none;
font-weight: bold;
}
.item {
background-color: var(--code-bg);
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
}
.signature {
display: block;
background-color: rgba(0,0,0,0.3);
padding: 0.5rem 1rem;
border-radius: 4px;
font-family: 'Fira Code', 'Monaco', monospace;
overflow-x: auto;
}
.description {
margin-top: 0.5rem;
color: #aaa;
}
.operations {
list-style: none;
padding-left: 1rem;
}
.operations li {
padding: 0.25rem 0;
}
.module-description {
background-color: var(--code-bg);
padding: 1rem;
border-radius: 8px;
margin-bottom: 2rem;
border-left: 3px solid var(--accent);
}
"#;
fn run_file(path: &str) {
use modules::ModuleLoader;
use std::path::Path;
@@ -1485,6 +2176,7 @@ struct LuxHelper {
keywords: HashSet<String>,
commands: Vec<String>,
user_defined: HashSet<String>,
last_loaded_file: Option<String>,
}
impl LuxHelper {
@@ -1502,7 +2194,8 @@ impl LuxHelper {
let commands = vec![
":help", ":h", ":quit", ":q", ":type", ":t", ":clear", ":load", ":l",
":trace", ":traces", ":info", ":i", ":env", ":doc", ":d", ":browse", ":b",
":reload", ":r", ":trace", ":traces", ":info", ":i", ":env", ":doc", ":d",
":browse", ":b", ":ast",
]
.into_iter()
.map(String::from)
@@ -1512,6 +2205,7 @@ impl LuxHelper {
keywords,
commands,
user_defined: HashSet::new(),
last_loaded_file: None,
}
}
@@ -1860,11 +2554,31 @@ fn handle_command(
}
":load" | ":l" => {
if let Some(path) = arg {
helper.last_loaded_file = Some(path.to_string());
load_file(path, interp, checker, helper);
} else {
println!("Usage: :load <filename>");
}
}
":reload" | ":r" => {
if let Some(ref path) = helper.last_loaded_file.clone() {
println!("Reloading {}...", path);
// Clear environment first
*interp = Interpreter::new();
*checker = TypeChecker::new();
helper.user_defined.clear();
load_file(path, interp, checker, helper);
} else {
println!("No file to reload. Use :load <file> first.");
}
}
":ast" => {
if let Some(expr_str) = arg {
show_ast(expr_str);
} else {
println!("Usage: :ast <expression>");
}
}
":trace" => match arg {
Some("on") => {
interp.enable_tracing();
@@ -2163,6 +2877,23 @@ fn show_type(expr_str: &str, checker: &mut TypeChecker) {
}
}
fn show_ast(expr_str: &str) {
// Wrap expression in a let to parse it
let wrapped = format!("let _expr_ = {}", expr_str);
match Parser::parse_source(&wrapped) {
Ok(program) => {
// Pretty print the AST
for decl in &program.declarations {
println!("{:#?}", decl);
}
}
Err(e) => {
println!("Parse error: {}", e);
}
}
}
fn load_file(path: &str, interp: &mut Interpreter, checker: &mut TypeChecker, helper: &mut LuxHelper) {
let source = match std::fs::read_to_string(path) {
Ok(s) => s,