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:
219
src/main.rs
219
src/main.rs
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user