feat: Elm-quality error messages with error codes

- Add ErrorCode enum with categorized codes (E01xx parse, E02xx type,
  E03xx name, E04xx effect, E05xx pattern, E06xx module, E07xx behavioral)
- Extend Diagnostic struct with error code, expected/actual types, and
  secondary spans
- Add format_type_diff() for visual type comparison in error messages
- Add help URLs linking to lux-lang.dev/errors/{code}
- Update typechecker, parser, and interpreter to use error codes
- Categorize errors with specific codes and helpful hints

Error messages now show:
- Error code in header: -- ERROR[E0301] ──
- Clear error category title
- Visual type diff for type mismatches
- Context-aware hints
- "Learn more" URL for documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-16 04:11:15 -05:00
parent bc1e5aa8a1
commit 3a46299404
5 changed files with 1200 additions and 45 deletions

View File

@@ -13,6 +13,7 @@ mod lsp;
mod modules;
mod package;
mod parser;
mod registry;
mod schema;
mod typechecker;
mod types;
@@ -126,6 +127,25 @@ fn main() {
// Package manager
handle_pkg_command(&args[2..]);
}
"registry" => {
// Run package registry server
let storage = args.iter()
.position(|a| a == "--storage" || a == "-s")
.and_then(|i| args.get(i + 1))
.map(|s| s.as_str())
.unwrap_or("./lux-registry");
let bind = args.iter()
.position(|a| a == "--bind" || a == "-b")
.and_then(|i| args.get(i + 1))
.map(|s| s.as_str())
.unwrap_or("127.0.0.1:8080");
if let Err(e) = registry::run_registry_server(storage, bind) {
eprintln!("Registry server error: {}", e);
std::process::exit(1);
}
}
"compile" => {
// Compile to native binary or JavaScript
if args.len() < 3 {
@@ -182,6 +202,9 @@ fn print_help() {
println!(" lux debug <file.lux> Start interactive debugger");
println!(" lux init [name] Initialize a new project");
println!(" lux pkg <command> Package manager (install, add, remove, list, update)");
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 --lsp Start LSP server (for IDE integration)");
println!(" lux --help Show this help");
println!(" lux --version Show version");
@@ -710,6 +733,9 @@ fn run_tests(args: &[String]) {
continue;
}
// Get auto-generated migrations from typechecker
let auto_migrations = checker.get_auto_migrations().clone();
// Find test functions (functions starting with test_)
let test_funcs: Vec<_> = program.declarations.iter().filter_map(|d| {
if let ast::Declaration::Function(f) = d {
@@ -723,6 +749,7 @@ fn run_tests(args: &[String]) {
if test_funcs.is_empty() {
// No test functions, run the whole file
let mut interp = Interpreter::new();
interp.register_auto_migrations(&auto_migrations);
interp.reset_test_results();
match interp.run(&program) {
@@ -755,6 +782,7 @@ fn run_tests(args: &[String]) {
for test_name in &test_funcs {
let mut interp = Interpreter::new();
interp.register_auto_migrations(&auto_migrations);
interp.reset_test_results();
// First run the file to define all functions
@@ -1046,6 +1074,16 @@ fn handle_pkg_command(args: &[String]) {
std::process::exit(1);
}
}
"search" => {
if args.len() < 2 {
eprintln!("Usage: lux pkg search <query>");
std::process::exit(1);
}
search_registry(&args[1]);
}
"publish" => {
publish_package(&project_root);
}
"help" | "--help" | "-h" => {
print_pkg_help();
}
@@ -1057,6 +1095,163 @@ fn handle_pkg_command(args: &[String]) {
}
}
fn search_registry(query: &str) {
let registry_url = std::env::var("LUX_REGISTRY_URL")
.unwrap_or_else(|_| "https://pkgs.lux-lang.org".to_string());
println!("Searching for '{}' in {}...", query, registry_url);
// Make HTTP request to registry
let url = format!("{}/api/v1/search?q={}", registry_url, query);
// Use a simple HTTP client (could use reqwest in production)
match simple_http_get(&url) {
Ok(response) => {
// Parse JSON response and display results
if response.contains("\"packages\":[]") {
println!("No packages found matching '{}'", query);
} else {
println!("\nFound packages:");
// Simple parsing of package names from JSON
for line in response.lines() {
if line.contains("\"name\":") {
if let Some(start) = line.find("\"name\":") {
let rest = &line[start + 8..];
if let Some(end) = rest.find('"') {
let name = &rest[..end];
println!(" {}", name);
}
}
}
}
}
}
Err(e) => {
eprintln!("Failed to connect to registry: {}", e);
eprintln!("Make sure the registry server is running or check LUX_REGISTRY_URL");
std::process::exit(1);
}
}
}
fn publish_package(project_root: &std::path::Path) {
use std::fs;
let registry_url = std::env::var("LUX_REGISTRY_URL")
.unwrap_or_else(|_| "https://pkgs.lux-lang.org".to_string());
// Load manifest
let manifest_path = project_root.join("lux.toml");
if !manifest_path.exists() {
eprintln!("No lux.toml found");
std::process::exit(1);
}
let manifest_content = fs::read_to_string(&manifest_path).unwrap();
// Extract project info
let mut name = String::new();
let mut version = String::new();
for line in manifest_content.lines() {
let line = line.trim();
if line.starts_with("name") {
if let Some(eq) = line.find('=') {
name = line[eq+1..].trim().trim_matches('"').to_string();
}
} else if line.starts_with("version") {
if let Some(eq) = line.find('=') {
version = line[eq+1..].trim().trim_matches('"').to_string();
}
}
}
if name.is_empty() || version.is_empty() {
eprintln!("Invalid lux.toml: missing name or version");
std::process::exit(1);
}
println!("Publishing {} v{} to {}...", name, version, registry_url);
// Create tarball of the package
let tarball_name = format!("{}-{}.tar.gz", name, version);
let tarball_path = project_root.join(&tarball_name);
// Use tar command to create tarball
let status = std::process::Command::new("tar")
.arg("-czf")
.arg(&tarball_path)
.arg("--exclude=.lux_packages")
.arg("--exclude=.git")
.arg("--exclude=target")
.arg("-C")
.arg(project_root)
.arg(".")
.status();
match status {
Ok(s) if s.success() => {
println!("Created package tarball: {}", tarball_name);
println!();
println!("To publish, upload the tarball to the registry:");
println!(" curl -X POST {}/api/v1/publish -F 'package=@{}'",
registry_url, tarball_name);
// Clean up tarball
// fs::remove_file(&tarball_path).ok();
}
Ok(_) => {
eprintln!("Failed to create package tarball");
std::process::exit(1);
}
Err(e) => {
eprintln!("Failed to run tar: {}", e);
std::process::exit(1);
}
}
}
fn simple_http_get(url: &str) -> Result<String, String> {
use std::io::{Read, Write};
use std::net::TcpStream;
// Parse URL
let url = url.strip_prefix("http://").or_else(|| url.strip_prefix("https://")).unwrap_or(url);
let (host_port, path) = if let Some(slash) = url.find('/') {
(&url[..slash], &url[slash..])
} else {
(url, "/")
};
let (host, port) = if let Some(colon) = host_port.find(':') {
(&host_port[..colon], host_port[colon+1..].parse::<u16>().unwrap_or(80))
} else {
(host_port, 80)
};
let mut stream = TcpStream::connect((host, port))
.map_err(|e| format!("Connection failed: {}", e))?;
let request = format!(
"GET {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n",
path, host
);
stream.write_all(request.as_bytes())
.map_err(|e| format!("Write failed: {}", e))?;
let mut response = String::new();
stream.read_to_string(&mut response)
.map_err(|e| format!("Read failed: {}", e))?;
// Extract body (after \r\n\r\n)
if let Some(body_start) = response.find("\r\n\r\n") {
Ok(response[body_start + 4..].to_string())
} else {
Ok(response)
}
}
fn print_pkg_help() {
println!("Lux Package Manager");
println!();
@@ -1075,6 +1270,11 @@ fn print_pkg_help() {
println!(" list, ls List dependencies and their status");
println!(" update Update all dependencies");
println!(" clean Remove installed packages");
println!(" search <query> Search for packages in the registry");
println!(" publish Publish package to the registry");
println!();
println!("Registry Configuration:");
println!(" Set LUX_REGISTRY_URL to use a custom registry (default: https://pkgs.lux-lang.org)");
println!();
println!("Examples:");
println!(" lux pkg init");
@@ -1084,6 +1284,8 @@ fn print_pkg_help() {
println!(" lux pkg add mylib --git https://github.com/user/mylib");
println!(" lux pkg add local-lib --path ../lib");
println!(" lux pkg remove http");
println!(" lux pkg search json");
println!(" lux pkg publish");
}
fn init_pkg_here(name: Option<&str>) {
@@ -3073,13 +3275,12 @@ c")"#;
#[test]
fn test_diagnostic_render_with_real_code() {
let source = "fn add(a: Int, b: Int): Int = a + b\nlet result = add(1, \"two\")";
let diag = Diagnostic {
severity: Severity::Error,
title: "Type Mismatch".to_string(),
message: "Expected Int but got String".to_string(),
span: Span { start: 56, end: 61 },
hints: vec!["The second argument should be an Int.".to_string()],
};
let diag = Diagnostic::error(
"Type Mismatch",
"Expected Int but got String",
Span { start: 56, end: 61 },
)
.with_hint("The second argument should be an Int.");
let output = render_diagnostic_plain(&diag, source, Some("example.lux"));
@@ -3098,8 +3299,10 @@ c")"#;
};
let diag = error.to_diagnostic();
assert_eq!(diag.title, "Unknown Name");
assert_eq!(diag.title, "Undefined Variable");
assert!(diag.hints.iter().any(|h| h.contains("spelling")));
// Check error code is set
assert!(diag.code.is_some());
}
#[test]