Skip to content

Commit 74a314d

Browse files
committed
Improve compiler diagnostics for playground
1 parent e37d47d commit 74a314d

17 files changed

Lines changed: 832 additions & 110 deletions

File tree

‎crates/cellscript-wasm/src/lib.rs‎

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,45 @@
1212
//! failure it is `{"error": "..."}` so the playground can parse it
1313
//! uniformly and render diagnostics.
1414
15+
use cellscript::error::{CompileError, Span};
16+
use serde::Serialize;
1517
use wasm_bindgen::prelude::*;
1618

19+
#[derive(Serialize)]
20+
struct CompileDiagnosticRange {
21+
start: CompileDiagnosticPosition,
22+
end: CompileDiagnosticPosition,
23+
}
24+
25+
#[derive(Serialize)]
26+
struct CompileDiagnosticPosition {
27+
line: usize,
28+
column: usize,
29+
offset: usize,
30+
}
31+
32+
#[derive(Serialize)]
33+
struct CompileDiagnostic {
34+
message: String,
35+
severity: &'static str,
36+
code: Option<String>,
37+
range: Option<CompileDiagnosticRange>,
38+
}
39+
40+
#[derive(Serialize)]
41+
struct CompileDiagnosticResult<T: Serialize> {
42+
metadata: Option<T>,
43+
diagnostics: Vec<CompileDiagnostic>,
44+
}
45+
46+
#[derive(Serialize)]
47+
struct LanguageServiceResult {
48+
completions: Vec<cellscript::lsp::CompletionItem>,
49+
hover: Option<cellscript::lsp::Hover>,
50+
definition: Option<cellscript::lsp::Location>,
51+
diagnostics: Vec<cellscript::lsp::Diagnostic>,
52+
}
53+
1754
/// Compile CellScript source to metadata JSON (path A, no ELF).
1855
///
1956
/// Returns a JSON string. On success this is the serialized
@@ -31,6 +68,51 @@ pub fn compile_metadata_json(source: &str, target: Option<String>) -> String {
3168
}
3269
}
3370

71+
/// Compile CellScript source and return a stable result envelope for tools.
72+
///
73+
/// On success the response is:
74+
/// `{ "metadata": <CompileMetadata>, "diagnostics": [] }`
75+
///
76+
/// On failure the response is:
77+
/// `{ "metadata": null, "diagnostics": [{ message, severity, code, range }, ...] }`
78+
///
79+
/// `range` is omitted when the compiler error is not tied to a source
80+
/// span. Offsets are UTF-8 byte offsets from the original source; line and
81+
/// column are 1-based.
82+
#[wasm_bindgen]
83+
pub fn compile_metadata_json_diagnostics(source: &str, target: Option<String>) -> String {
84+
let report = cellscript::compile_metadata_with_diagnostics(source, target);
85+
let result = CompileDiagnosticResult {
86+
metadata: report.metadata,
87+
diagnostics: report.diagnostics.iter().map(|error| diagnostic_from_error(error, source)).collect(),
88+
};
89+
serde_json::to_string(&result)
90+
.unwrap_or_else(|e| diagnostic_error_json(&format!("failed to serialize diagnostic report: {e}"), source))
91+
}
92+
93+
/// Query the in-process CellScript language service for browser tooling.
94+
///
95+
/// `line` and `character` are zero-based UTF-16 positions, matching LSP.
96+
/// The result contains completion, hover, definition and current document
97+
/// diagnostics in one JSON payload so the playground can avoid multiple
98+
/// WASM calls per cursor move.
99+
#[wasm_bindgen]
100+
pub fn language_service_json(source: &str, line: u32, character: u32) -> String {
101+
let uri = "file:///playground.cell";
102+
let position = cellscript::lsp::Position { line, character };
103+
let mut server = cellscript::lsp::LspServer::new();
104+
server.open_document(uri.to_string(), source.to_string());
105+
let result = LanguageServiceResult {
106+
completions: server.completion(uri, position),
107+
hover: server.hover(uri, position),
108+
definition: server.goto_definition(uri, position),
109+
diagnostics: server.get_diagnostics(uri),
110+
};
111+
serde_json::to_string(&result).unwrap_or_else(|error| {
112+
serde_json::json!({ "error": format!("failed to serialize language service result: {error}") }).to_string()
113+
})
114+
}
115+
34116
/// Return the compiler version string (e.g. "0.17.0").
35117
#[wasm_bindgen]
36118
pub fn version() -> String {
@@ -40,3 +122,54 @@ pub fn version() -> String {
40122
fn error_json(message: &str) -> String {
41123
serde_json::json!({ "error": message }).to_string()
42124
}
125+
126+
fn diagnostic_error_json(message: &str, source: &str) -> String {
127+
let result: CompileDiagnosticResult<serde_json::Value> = CompileDiagnosticResult {
128+
metadata: None,
129+
diagnostics: vec![diagnostic_from_error(&CompileError::without_span(message), source)],
130+
};
131+
serde_json::to_string(&result).unwrap_or_else(|_| {
132+
serde_json::json!({ "metadata": null, "diagnostics": [{ "message": message, "severity": "error" }] }).to_string()
133+
})
134+
}
135+
136+
fn diagnostic_from_error(error: &CompileError, source: &str) -> CompileDiagnostic {
137+
CompileDiagnostic {
138+
message: error.message.clone(),
139+
severity: error.severity.label(),
140+
code: error.code.clone(),
141+
range: span_range(error.span, source),
142+
}
143+
}
144+
145+
fn span_range(span: Span, source: &str) -> Option<CompileDiagnosticRange> {
146+
if span.line == 0 || span.column == 0 {
147+
return None;
148+
}
149+
let source_len = source.len();
150+
let start = span.start.min(source_len);
151+
let end = span.end.min(source_len).max(start);
152+
let (end_line, end_column) = line_column_at(source, end);
153+
Some(CompileDiagnosticRange {
154+
start: CompileDiagnosticPosition { line: span.line, column: span.column, offset: start },
155+
end: CompileDiagnosticPosition { line: end_line, column: end_column, offset: end },
156+
})
157+
}
158+
159+
fn line_column_at(source: &str, byte_offset: usize) -> (usize, usize) {
160+
let mut line = 1;
161+
let mut column = 1;
162+
let capped_offset = byte_offset.min(source.len());
163+
for (offset, ch) in source.char_indices() {
164+
if offset >= capped_offset {
165+
break;
166+
}
167+
if ch == '\n' {
168+
line += 1;
169+
column = 1;
170+
} else {
171+
column += 1;
172+
}
173+
}
174+
(line, column)
175+
}

‎src/ast/mod.rs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ pub enum Type {
318318
pub enum Stmt {
319319
Let(LetStmt),
320320
Expr(Expr),
321-
Return(Option<Expr>),
321+
Return(ReturnStmt),
322322
If(IfStmt),
323323
For(ForStmt),
324324
While(WhileStmt),
@@ -340,6 +340,12 @@ pub struct LetStmt {
340340
pub span: Span,
341341
}
342342

343+
#[derive(Debug, Clone)]
344+
pub struct ReturnStmt {
345+
pub value: Option<Expr>,
346+
pub span: Span,
347+
}
348+
343349
#[derive(Debug, Clone)]
344350
pub struct IfStmt {
345351
pub condition: Expr,

‎src/cli/novaseal_certification.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11803,7 +11803,7 @@ mod tests {
1180311803
fn run_fixture_git_with_identity(fiber_repo: &Path, args: &[&str]) {
1180411804
let output = std::process::Command::new("git")
1180511805
.current_dir(fiber_repo)
11806-
.args(["-c", "user.name=CellScript Test", "-c", "user.email=cellscript@example.invalid"])
11806+
.args(["-c", "user.name=CellScript Test", "-c", "user.email=cellscript@example.invalid", "-c", "commit.gpgsign=false"])
1180711807
.args(args)
1180811808
.output()
1180911809
.unwrap();

‎src/flow/mod.rs‎

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@ struct ActionStateContext {
2121
/// Validate declared flow transitions and statically check
2222
/// flow-aware creates that can be decided from source.
2323
pub fn check(module: &Module) -> Result<()> {
24+
let diagnostics = diagnostics(module);
25+
if let Some(error) = diagnostics.into_iter().next() {
26+
Err(error)
27+
} else {
28+
Ok(())
29+
}
30+
}
31+
32+
/// Return all flow diagnostics that can be collected without parser recovery.
33+
pub fn diagnostics(module: &Module) -> Vec<CompileError> {
34+
let mut diagnostics = Vec::new();
2435
let mut specs = HashMap::new();
2536

2637
for item in &module.items {
@@ -37,7 +48,8 @@ pub fn check(module: &Module) -> Result<()> {
3748
}
3849
}
3950
if states.len() < 2 {
40-
return Err(CompileError::new("flow must mention at least two states", machine.span));
51+
diagnostics.push(CompileError::new("flow must mention at least two states", machine.span));
52+
continue;
4153
}
4254
specs.insert(
4355
machine.target.base.clone(),
@@ -49,17 +61,25 @@ pub fn check(module: &Module) -> Result<()> {
4961
match item {
5062
Item::Action(action) => {
5163
let context = action_state_context(&specs, action);
52-
validate_stmt_list(&specs, &context, &action.body)?;
64+
if let Err(error) = validate_stmt_list(&specs, &context, &action.body) {
65+
diagnostics.push(error);
66+
}
5367
}
5468
Item::Function(function) => {
55-
validate_stmt_list(&specs, &ActionStateContext::default(), &function.body)?;
69+
if let Err(error) = validate_stmt_list(&specs, &ActionStateContext::default(), &function.body) {
70+
diagnostics.push(error);
71+
}
72+
}
73+
Item::Lock(lock) => {
74+
if let Err(error) = validate_stmt_list(&specs, &ActionStateContext::default(), &lock.body) {
75+
diagnostics.push(error);
76+
}
5677
}
57-
Item::Lock(lock) => validate_stmt_list(&specs, &ActionStateContext::default(), &lock.body)?,
5878
_ => {}
5979
}
6080
}
6181

62-
Ok(())
82+
diagnostics
6383
}
6484

6585
fn action_state_context(specs: &HashMap<String, FlowSpec>, action: &ActionDef) -> ActionStateContext {
@@ -95,8 +115,10 @@ fn collect_state_context_from_stmts(specs: &HashMap<String, FlowSpec>, context:
95115
}
96116
collect_state_context_from_expr(specs, context, &let_stmt.value);
97117
}
98-
Stmt::Expr(expr) | Stmt::Return(Some(expr)) => collect_state_context_from_expr(specs, context, expr),
99-
Stmt::Return(None) => {}
118+
Stmt::Expr(expr) | Stmt::Return(ReturnStmt { value: Some(expr), .. }) => {
119+
collect_state_context_from_expr(specs, context, expr)
120+
}
121+
Stmt::Return(ReturnStmt { value: None, .. }) => {}
100122
Stmt::If(if_stmt) => {
101123
collect_state_context_from_expr(specs, context, &if_stmt.condition);
102124
collect_state_context_from_stmts(specs, context, &if_stmt.then_branch);
@@ -244,8 +266,8 @@ fn validate_state_transition_stmt(specs: &HashMap<String, FlowSpec>, context: &A
244266
match stmt {
245267
Stmt::Let(let_stmt) => validate_state_transition_expr(specs, context, &let_stmt.value),
246268
Stmt::Expr(expr) => validate_state_transition_expr(specs, context, expr),
247-
Stmt::Return(Some(expr)) => validate_state_transition_expr(specs, context, expr),
248-
Stmt::Return(None) => Ok(()),
269+
Stmt::Return(ReturnStmt { value: Some(expr), .. }) => validate_state_transition_expr(specs, context, expr),
270+
Stmt::Return(ReturnStmt { value: None, .. }) => Ok(()),
249271
Stmt::If(if_stmt) => {
250272
validate_state_transition_expr(specs, context, &if_stmt.condition)?;
251273
validate_stmt_list(specs, context, &if_stmt.then_branch)?;

‎src/fmt/mod.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -387,8 +387,8 @@ impl Formatter {
387387
self.push_line(&line);
388388
}
389389
Stmt::Expr(expr) => self.push_line(&self.format_expr(expr)),
390-
Stmt::Return(None) => self.push_line("return"),
391-
Stmt::Return(Some(expr)) => self.push_line(&format!("return {}", self.format_expr(expr))),
390+
Stmt::Return(ReturnStmt { value: None, .. }) => self.push_line("return"),
391+
Stmt::Return(ReturnStmt { value: Some(expr), .. }) => self.push_line(&format!("return {}", self.format_expr(expr))),
392392
Stmt::If(if_stmt) => self.format_if_stmt(if_stmt),
393393
Stmt::For(for_stmt) => self.format_for_stmt(for_stmt),
394394
Stmt::While(while_stmt) => self.format_while_stmt(while_stmt),

‎src/ir/mod.rs‎

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,12 @@ impl IrGenerator {
477477
generator
478478
}
479479

480-
pub fn generate(mut self, ast: &Module) -> Result<IrModule> {
480+
pub fn generate(self, ast: &Module) -> Result<IrModule> {
481+
self.generate_diagnostics(ast)
482+
.map_err(|errors| errors.into_iter().next().unwrap_or_else(|| CompileError::without_span("IR lowering failed")))
483+
}
484+
485+
pub fn generate_diagnostics(mut self, ast: &Module) -> std::result::Result<IrModule, Vec<CompileError>> {
481486
for item in &ast.items {
482487
if let Item::Const(c) = item {
483488
self.constants.insert(c.name.clone(), (c.value.clone(), Self::convert_type(&c.ty)));
@@ -595,10 +600,10 @@ impl IrGenerator {
595600
Item::Use(_) => {}
596601
}
597602
}
598-
if let Some(error) = self.errors.into_iter().next() {
599-
Err(error)
600-
} else {
603+
if self.errors.is_empty() {
601604
Ok(self.module)
605+
} else {
606+
Err(self.errors)
602607
}
603608
}
604609

@@ -1050,7 +1055,7 @@ impl IrGenerator {
10501055
Stmt::Expr(expr) | Stmt::Let(LetStmt { value: expr, .. }) => {
10511056
self.check_expr_effects(expr, footprint);
10521057
}
1053-
Stmt::Return(Some(expr)) => {
1058+
Stmt::Return(ReturnStmt { value: Some(expr), .. }) => {
10541059
self.check_expr_effects(expr, footprint);
10551060
}
10561061
Stmt::If(if_stmt) => {
@@ -1742,11 +1747,11 @@ impl IrGenerator {
17421747
Some(active)
17431748
}
17441749
Stmt::Expr(expr) => self.lower_expr(expr, current, blocks, vars).current,
1745-
Stmt::Return(None) => {
1750+
Stmt::Return(ReturnStmt { value: None, .. }) => {
17461751
self.block_mut(blocks, current).terminator = IrTerminator::Return(None);
17471752
None
17481753
}
1749-
Stmt::Return(Some(expr)) => {
1754+
Stmt::Return(ReturnStmt { value: Some(expr), .. }) => {
17501755
let lowered = if let Some(return_type) = return_type {
17511756
self.lower_expr_with_expected_type(expr, return_type, current, blocks, vars)
17521757
} else {
@@ -5947,6 +5952,11 @@ pub fn generate(ast: &Module) -> Result<IrModule> {
59475952
generator.generate(ast)
59485953
}
59495954

5955+
pub fn generate_diagnostics(ast: &Module) -> std::result::Result<IrModule, Vec<CompileError>> {
5956+
let generator = IrGenerator::new(ast.name.clone());
5957+
generator.generate_diagnostics(ast)
5958+
}
5959+
59505960
pub fn generate_with_resolver(ast: &Module, resolver: &ModuleResolver, module_name: &str) -> Result<IrModule> {
59515961
generate_with_resolver_inner(ast, resolver, module_name, true)
59525962
}
@@ -6199,7 +6209,7 @@ fn collect_call_names_from_stmts(stmts: &[Stmt], names: &mut HashSet<String>) {
61996209

62006210
fn collect_call_names_from_stmt(stmt: &Stmt, names: &mut HashSet<String>) {
62016211
match stmt {
6202-
Stmt::Expr(expr) | Stmt::Let(LetStmt { value: expr, .. }) | Stmt::Return(Some(expr)) => {
6212+
Stmt::Expr(expr) | Stmt::Let(LetStmt { value: expr, .. }) | Stmt::Return(ReturnStmt { value: Some(expr), .. }) => {
62036213
collect_call_names_from_expr(expr, names);
62046214
}
62056215
Stmt::If(if_stmt) => {
@@ -6217,7 +6227,7 @@ fn collect_call_names_from_stmt(stmt: &Stmt, names: &mut HashSet<String>) {
62176227
collect_call_names_from_expr(&while_stmt.condition, names);
62186228
collect_call_names_from_stmts(&while_stmt.body, names);
62196229
}
6220-
Stmt::Return(None) => {}
6230+
Stmt::Return(ReturnStmt { value: None, .. }) => {}
62216231
}
62226232
}
62236233

@@ -6387,7 +6397,7 @@ fn infer_fn_effect_without_call_graph(function: &FnDef) -> EffectClass {
63876397

63886398
fn collect_ast_stmt_effects(stmt: &Stmt, footprint: &mut EffectFootprint) {
63896399
match stmt {
6390-
Stmt::Expr(expr) | Stmt::Let(LetStmt { value: expr, .. }) | Stmt::Return(Some(expr)) => {
6400+
Stmt::Expr(expr) | Stmt::Let(LetStmt { value: expr, .. }) | Stmt::Return(ReturnStmt { value: Some(expr), .. }) => {
63916401
collect_ast_expr_effects(expr, footprint);
63926402
}
63936403
Stmt::If(if_stmt) => {
@@ -6764,8 +6774,10 @@ fn collect_consumed_bindings_from_stmts(stmts: &[Stmt], bindings: &mut HashSet<S
67646774
for stmt in stmts {
67656775
match stmt {
67666776
Stmt::Let(let_stmt) => collect_consumed_bindings_from_expr(&let_stmt.value, bindings),
6767-
Stmt::Expr(expr) | Stmt::Return(Some(expr)) => collect_consumed_bindings_from_expr(expr, bindings),
6768-
Stmt::Return(None) => {}
6777+
Stmt::Expr(expr) | Stmt::Return(ReturnStmt { value: Some(expr), .. }) => {
6778+
collect_consumed_bindings_from_expr(expr, bindings)
6779+
}
6780+
Stmt::Return(ReturnStmt { value: None, .. }) => {}
67696781
Stmt::If(if_stmt) => {
67706782
collect_consumed_bindings_from_expr(&if_stmt.condition, bindings);
67716783
collect_consumed_bindings_from_stmts(&if_stmt.then_branch, bindings);

0 commit comments

Comments
 (0)