From 63fba47fa707ebba64f9c4544609cbe5e51b7212 Mon Sep 17 00:00:00 2001 From: Arnab Ghosh Date: Tue, 4 Aug 2026 15:18:33 -0700 Subject: [PATCH] enh: document Quon comments and diagnose C-style comment syntax (#372) --- frontend/src/diagnostics.rs | 6 +- frontend/src/lexer.rs | 65 +++++++++++++++++++ frontend/tests/lexer.rs | 44 +++++++++++++ frontend/tests/lsp_diagnostics.rs | 13 ++++ frontend/tests/parser.rs | 26 ++++++++ .../src/content/docs/language/introduction.md | 51 +++++++++++++++ 6 files changed, 203 insertions(+), 2 deletions(-) diff --git a/frontend/src/diagnostics.rs b/frontend/src/diagnostics.rs index 539fe851..d8bd0cc9 100644 --- a/frontend/src/diagnostics.rs +++ b/frontend/src/diagnostics.rs @@ -17,6 +17,7 @@ impl DiagnosticCode { // Lexer pub const LEX_INVALID_CHAR: Self = Self("quon.lex.invalid-char"); pub const LEX_UNTERMINATED_COMMENT: Self = Self("quon.lex.unterminated-comment"); + pub const LEX_UNSUPPORTED_COMMENT: Self = Self("quon.lex.unsupported-comment"); // Parser (v1: default bucket only) pub const PARSE_UNEXPECTED_TOKEN: Self = Self("quon.parse.unexpected-token"); @@ -217,9 +218,10 @@ pub(crate) fn from_stage(errors: Vec>) -> Vec { errors.into_iter().map(Diagnostic::from).collect() } -/// Classify a lexer error message into a stable code. pub(crate) fn classify_lex_error(src: &str, message: &str, span: SimpleSpan) -> RichDiagnostic { - let code = if (message.contains("comment") && message.contains("unclosed")) + let code = if message.contains("C-style line comments") { + DiagnosticCode::LEX_UNSUPPORTED_COMMENT + } else if (message.contains("comment") && message.contains("unclosed")) || unterminated_block_comment_at(src, span) { DiagnosticCode::LEX_UNTERMINATED_COMMENT diff --git a/frontend/src/lexer.rs b/frontend/src/lexer.rs index cdd743b6..da8e62f3 100644 --- a/frontend/src/lexer.rs +++ b/frontend/src/lexer.rs @@ -140,6 +140,9 @@ impl std::fmt::Display for Token { /// On success returns the tokens (no `Eof` is appended; the parser uses `end()`). /// On failure returns one `(message, span)` per lexical error — never panics. pub fn lex(src: &str) -> Result>, Vec>> { + if let Some(start) = c_style_comment_outside_comments(src) { + return Err(vec![(C_STYLE_COMMENT_MSG.to_owned(), (start..start + 2).into())]); + } lexer().parse(src).into_result().map_err(|errs| { errs.into_iter() .map(|e| (e.to_string(), *e.span())) @@ -149,6 +152,13 @@ pub fn lex(src: &str) -> Result>, Vec>> { /// Tokenize `src`, returning structured diagnostics on failure. pub fn lex_rich(src: &str) -> Result>, Vec> { + if let Some(start) = c_style_comment_outside_comments(src) { + return Err(vec![crate::diagnostics::classify_lex_error( + src, + C_STYLE_COMMENT_MSG, + (start..start + 2).into(), + )]); + } if let Some(start) = unterminated_block_comment_outside_line_comments(src) { return Err(vec![crate::diagnostics::RichDiagnostic::new( crate::diagnostics::DiagnosticCode::LEX_UNTERMINATED_COMMENT, @@ -207,6 +217,61 @@ fn unterminated_block_comment_outside_line_comments(src: &str) -> Option None } +/// Diagnostic text for the `//` (C-style line comment) mistake. +/// +/// `//` is not a Quon operator and the lexer has no C-style comment syntax, so a +/// first-time author reaching for `//` only gets a generic parse error. This +/// message names the unsupported spelling and recommends the supported `--` +/// (line) and `{- ... -}` (block) forms. +const C_STYLE_COMMENT_MSG: &str = "C-style line comments (`//`) are not supported; use `--` for line comments or `{- ... -}` for block comments"; + +/// Detect a `//` sequence in real source — outside `--` line comments and +/// `{- -}` block comments — and return the byte offset of its first slash. +/// +/// The chumsky lexer has no `//` token, so without this guard `//` lexes as two +/// `Slash` tokens and the user sees an opaque "unexpected token" parser error. +/// Surfacing the mistake at the lex boundary lets us emit a diagnostic that +/// recommends the supported comment spellings. +fn c_style_comment_outside_comments(src: &str) -> Option { + let bytes = src.as_bytes(); + let mut i = 0usize; + while i < bytes.len() { + // `--` line comment: skip to end of line (any `//` inside is comment text). + if i + 1 < bytes.len() && bytes[i] == b'-' && bytes[i + 1] == b'-' { + i += 2; + while i < bytes.len() && bytes[i] != b'\n' { + i += 1; + } + continue; + } + // Nested `{- -}` block comment: skip the whole block (any `//` inside is + // comment text). An unterminated block is reported separately by + // [`unterminated_block_comment_outside_line_comments`]. + if i + 1 < bytes.len() && bytes[i] == b'{' && bytes[i + 1] == b'-' { + let mut depth = 1u32; + i += 2; + while i + 1 < bytes.len() && depth > 0 { + if bytes[i] == b'{' && bytes[i + 1] == b'-' { + depth += 1; + i += 2; + } else if bytes[i] == b'-' && bytes[i + 1] == b'}' { + depth -= 1; + i += 2; + } else { + i += 1; + } + } + continue; + } + // `//` in real source: the C-style line-comment mistake. + if i + 1 < bytes.len() && bytes[i] == b'/' && bytes[i + 1] == b'/' { + return Some(i); + } + i += 1; + } + None +} + type LexErr<'src> = extra::Err>; fn lexer<'src>() -> impl Parser<'src, &'src str, Vec>, LexErr<'src>> { diff --git a/frontend/tests/lexer.rs b/frontend/tests/lexer.rs index 8c579c54..e0be6e43 100644 --- a/frontend/tests/lexer.rs +++ b/frontend/tests/lexer.rs @@ -190,3 +190,47 @@ fn unknown_char_is_span_accurate_error_not_panic() { // The offending `#` is at byte offset 2. assert_eq!(span.start, 2); } + + +#[test] +fn c_style_comment_is_lex_error_recommending_dash() { + // `//` is not a Quon operator; the common C-style comment mistake must + // surface as a lexer error that names the spelling and recommends `--`. + let err = lex("a // comment").expect_err("expected `//` to be a lex error"); + assert!(!err.is_empty(), "no errors emitted for `//`"); + let (msg, span) = &err[0]; + assert!( + msg.contains("//"), + "message should name the unsupported spelling: {msg:?}" + ); + assert!( + msg.contains("--"), + "message should recommend `--`: {msg:?}" + ); + // The span covers both slashes, starting at the first one (byte offset 2). + assert_eq!(span.start, 2); + assert_eq!(span.end - span.start, 2); +} + +#[test] +fn single_slash_still_lexes() { + // `/` is the division operator and must still tokenize after the `//` guard. + use Token::*; + assert_eq!(toks("a / b"), vec![Ident("a".into()), Slash, Ident("b".into())]); +} + +#[test] +fn c_style_comment_not_flagged_inside_real_comments() { + // `//` appearing inside a `--` line comment or `{- -}` block comment is + // part of the comment text, not source — it must not be flagged. + use Token::*; + assert_eq!(toks("a -- https://example.com\nb"), vec![ + Ident("a".into()), + Newline, + Ident("b".into()), + ]); + assert_eq!(toks("a {- // ignored -} b"), vec![ + Ident("a".into()), + Ident("b".into()), + ]); +} diff --git a/frontend/tests/lsp_diagnostics.rs b/frontend/tests/lsp_diagnostics.rs index 04c79647..0017677a 100644 --- a/frontend/tests/lsp_diagnostics.rs +++ b/frontend/tests/lsp_diagnostics.rs @@ -69,6 +69,19 @@ fn unterminated_block_comment_has_code() { assert_code(src, "quon.lex.unterminated-comment"); } + +#[test] +fn c_style_comment_has_code() { + // `//` is the common C-style comment mistake; it must surface as a stable + // lexer diagnostic that recommends the supported `--` spelling. + let src = "fn f(): Int = 1 // oops"; + assert_code(src, "quon.lex.unsupported-comment"); + let d = first_with_code(src, "quon.lex.unsupported-comment"); + let slashes = src.find("//").unwrap(); + assert_eq!(d.span.start, slashes); + assert!(d.message.contains("--"), "message should recommend `--`: {}", d.message); +} + #[test] fn desugar_run_trailing_bind_has_code() { let src = "fn f(): Q = run { x <- measure(qubit()) }\n"; diff --git a/frontend/tests/parser.rs b/frontend/tests/parser.rs index 8a245cb7..e6dff7e3 100644 --- a/frontend/tests/parser.rs +++ b/frontend/tests/parser.rs @@ -327,3 +327,29 @@ fn nat_only_alias_params_still_parse() { other => panic!("expected type alias, got {other:?}"), } } + + +#[test] +fn valid_comments_parse() { + // Line and block comments are part of the lex grammar; a program using both + // must parse exactly like the stripped form. + let with_comments = "\ +-- a line comment before the declaration +fn f(): Int = {- block comment -} 1 + 2 -- trailing line comment"; + let stripped = "fn f(): Int = 1 + 2"; + assert_eq!( + parse_stripped(with_comments), + parse_stripped(stripped), + "comments must not alter the parsed AST" + ); +} + +#[test] +fn c_style_comment_fails_at_lex_with_recommendation() { + // `//` is rejected by the lexer, so the program never reaches the parser; + // the lex error must name the spelling and recommend `--`. + let src = "fn f(): Int = 1 // oops"; + let err = lex(src).expect_err("expected `//` to be a lex error"); + let (msg, _) = &err[0]; + assert!(msg.contains("//") && msg.contains("--"), "lex message: {msg:?}"); +} diff --git a/website/src/content/docs/language/introduction.md b/website/src/content/docs/language/introduction.md index b02ae962..e33da1df 100644 --- a/website/src/content/docs/language/introduction.md +++ b/website/src/content/docs/language/introduction.md @@ -268,6 +268,57 @@ the optimizer in an ambiguous state. The full pipeline is documented in the explored stage-by-stage in the [compiler internals](/architecture/compiler-internals/) page `(rationale — Architecture)`. +## Comments + +Quon source uses two comment spellings, both drawn from the language's +own punctuation rather than from C: + +- **Line comments** start with `--` and run to the end of the line. Use them for + per-gate notes, citations, or short rationale. + + ```kotlin + fn bell_state(): Circuit<2, 2, 2, Clifford> = circuit { + H @0 |> CNOT @(0, 1) -- prepare a Bell pair + } + ``` + +- **Block comments** use `{- ... -}` and may span multiple lines or nest. Use + them for longer explanations, including over a circuit body. + + ```kotlin + {- This circuit prepares the maximally entangled Bell state |Φ+⟩. + See Nielsen & Chuang, §1.3. -} + fn bell_state(): Circuit<2, 2, 2, Clifford> = circuit { + H @0 |> CNOT @(0, 1) + } + ``` + +Block comments nest, so a `{- -}` region can safely enclose another `{- -}` +region when commenting out a block of code that itself contains comments. + +### C-style `//` is not a comment + +A common first instinct is to reach for `//`, the line-comment spelling used by +C, Java, and Rust. Quon does **not** support `//` — `--` is the line-comment +spelling. Writing `//` is a lex error, not a silent no-op, and the diagnostic +names the unsupported spelling and recommends the supported one: + +```kotlin +fn f(): Int = 1 // oops +``` + +``` +error: C-style line comments (`//`) are not supported; use `--` for line comments or `{- ... -}` for block comments + --> source.qn:1:16 + | +1 | fn f(): Int = 1 // oops + | ^^ +``` + +`/`, on its own, remains the division operator on classical integers and floats; +only the two-slash sequence is rejected. + + ## How to read this guide This guide introduces Quon one concept at a time, in an order chosen so that