Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions frontend/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -217,9 +218,10 @@ pub(crate) fn from_stage(errors: Vec<Sp<String>>) -> Vec<Diagnostic> {
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
Expand Down
65 changes: 65 additions & 0 deletions frontend/src/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Sp<Token>>, Vec<Sp<String>>> {
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()))
Expand All @@ -149,6 +152,13 @@ pub fn lex(src: &str) -> Result<Vec<Sp<Token>>, Vec<Sp<String>>> {

/// Tokenize `src`, returning structured diagnostics on failure.
pub fn lex_rich(src: &str) -> Result<Vec<Sp<Token>>, Vec<crate::diagnostics::RichDiagnostic>> {
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,
Expand Down Expand Up @@ -207,6 +217,61 @@ fn unterminated_block_comment_outside_line_comments(src: &str) -> Option<usize>
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<usize> {
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<Rich<'src, char>>;

fn lexer<'src>() -> impl Parser<'src, &'src str, Vec<Sp<Token>>, LexErr<'src>> {
Expand Down
44 changes: 44 additions & 0 deletions frontend/tests/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
]);
}
13 changes: 13 additions & 0 deletions frontend/tests/lsp_diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Int> = run { x <- measure(qubit()) }\n";
Expand Down
26 changes: 26 additions & 0 deletions frontend/tests/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:?}");
}
51 changes: 51 additions & 0 deletions website/src/content/docs/language/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading