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
39 changes: 31 additions & 8 deletions crates/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,37 @@ The parser covers a subset of §16, not the whole grammar. Treat anything outsid
the accepted list as **not yet implemented** — add a corpus fixture when it lands.

- **Accepted:** every item form (`fn`, `struct`, `enum` incl. tuple/struct
variants, `actor`, `mod`, `use`, `const`, `type`, `trait`, `impl`,
`onchain mod`, `extern`); types incl. generics, references, arrays/slices,
tuples, `fn`/`dyn`; expressions incl. calls, turbofish, field/index,
unary/binary (incl. `/`), `?`, `.await`, `|>`, `as`, struct literals (with the
§5.1 block-head restriction), `vec!`, and `if`/`else`; `let`, `return`,
`break`, `continue`, `send`, and expression/tail statements.
- **Not yet implemented:** `match`, `while`, `for`, `loop`, and closures (their
keywords lex but have no parse production); generic parameters and
variants, `actor`, `mod`, `use`, `const`, `type`, `trait` incl. generics,
supertrait bounds, and `where` clauses (bounds parsed but not stored),
`impl Type` and `impl Trait for Type` (trait ref recorded in the AST),
`onchain mod`, `extern`), with §16 modifier placement enforced —
`offchain`/`async` only on `fn` items, and `pub` rejected on `impl`,
`actor`, `onchain`, and `extern` items; types incl. generics, references,
arrays/slices, tuples, `fn`, and `dyn Trait<Args>` incl. associated-type
bindings; expressions incl. calls, turbofish, field/index,
unary/binary (incl. `/`; `..`/`..=` are non-associative — chained ranges
are a parse error), assignment (targets validated per §16
`assign_target`), `?`, `.await`, `as`, struct literals (with the
§5.1 block-head restriction), `vec!` (square brackets required —
`vec!(...)` is a parse error), and `if`/`else`; `|>` with §16
`pipe_stage` stages (`callee [args] [?]` — a stage's trailing `?` wraps the
accumulated pipe application per §5.7); `let`, `return`,
`break`, `continue`, `send` (statement-head rule per §2.7 — opens a
send-statement only when the next token can begin an expression, and the
operand must be a method call), and expression/tail statements.
- **Not yet implemented:** `match`, `while`, `for`, `loop`, `if let`, and
closures (their keywords lex but have no parse production); `spawn`, `select`,
and `emit` (reserved keywords with no expression/statement production yet, so
§8 actor spawns and §11.5 event emission do not parse); patterns — `let` binds
a single identifier only, no destructuring (`let (a, b) = ...` and
`let Some(x) = ...` are rejected); `#[...]` compiler directives are not parsed
in any position (`#[cfg(test)]`, `#[target(...)]`, `#[indexed]` all fail);
`storage { }` blocks inside `onchain mod` are consumed but discarded, not
stored in the AST; closure pipe stages (`x |> (|v| ...)`) and turbofish on a
non-final pipe-stage segment are rejected with explicit
not-yet-implemented parse errors; literal-overflow checking (a parse-time
error per `docs/reference/grammar.md`) is deferred to semantic analysis,
where literals gain types; generic parameters and
`trait`/`impl` bodies are skipped, not stored in the AST; `let mut` / `&mut`
mutability and the `send` keyword are parsed but not preserved; a block-like
expression (`if`/block) used as a non-tail statement needs a trailing `;`.
Expand Down
11 changes: 10 additions & 1 deletion crates/sploosh-ast/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ pub struct Trait {

#[derive(Debug, Clone, PartialEq)]
pub struct ImplBlock {
/// `Some` for `impl Trait for Type`, `None` for an inherent `impl Type`.
pub trait_ref: Option<TraitRef>,
pub target: Type,
}

Expand All @@ -195,7 +197,14 @@ pub enum Type {
Slice(Box<Type>),
Tuple(Vec<Type>),
Function { params: Vec<Type>, ret: Box<Type> },
Dyn(Path),
Dyn(TraitRef),
}

/// `trait_ref = type_path [ type_args ]` (§16).
#[derive(Debug, Clone, PartialEq)]
pub struct TraitRef {
pub path: Path,
pub args: Vec<TypeArg>,
}

#[derive(Debug, Clone, PartialEq)]
Expand Down
101 changes: 92 additions & 9 deletions crates/sploosh-lexer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,20 @@ impl Lexer<'_> {
self.pos += 2;
}

let digits_start = self.pos;
self.digits(base);
if base != 10 && self.pos == digits_start {
let name = match base {
16 => "hexadecimal",
8 => "octal",
_ => "binary",
};
self.error(
format!("{name} literal needs at least one digit"),
start,
self.pos,
);
}
let mut kind = TokenKind::IntLit;
if base == 10 && self.peek() == Some('.') && self.peek_next() != Some('.') {
kind = TokenKind::FloatLit;
Expand All @@ -300,13 +313,23 @@ impl Lexer<'_> {
}
}
if let Some(suffix) = self.numeric_suffix() {
kind = if suffix.starts_with('f') {
TokenKind::FloatLit
} else {
kind
};
if suffix.starts_with('f') {
// §16.1 FLOAT_LIT: a float suffix only combines with `dec_lit`.
if base != 10 {
self.error("float suffixes require a decimal literal", start, self.pos);
}
kind = TokenKind::FloatLit;
} else if kind == TokenKind::FloatLit {
// §16.1: float_suffix = "f32" | "f64" — integer suffixes on a
// literal with a `.` or exponent are a compile error.
self.error(
"float literals only accept `f32`/`f64` suffixes",
start,
self.pos,
);
}
}
self.validate_numeric_body(start);
self.validate_numeric_body(start, base);
self.push(kind, start, self.pos);
}

Expand All @@ -331,7 +354,7 @@ impl Lexer<'_> {
None
}

fn validate_numeric_body(&mut self, start: usize) {
fn validate_numeric_body(&mut self, start: usize, base: u8) {
let text = &self.source[start..self.pos];
let suffixes = [
"i128", "u128", "u256", "i64", "u64", "f64", "i32", "u32", "f32", "i16", "u16", "i8",
Expand All @@ -346,10 +369,13 @@ impl Lexer<'_> {
if *ch != '_' {
continue;
}
let valid_prev = index > 0 && chars[index - 1].is_ascii_hexdigit();
// Neighbors must be digits *of this base* — `is_ascii_hexdigit`
// would wrongly accept `1_e5` in a decimal literal because `e` is
// a hex digit character.
let valid_prev = index > 0 && valid_digit(chars[index - 1], base);
let valid_next = chars
.get(index + 1)
.is_some_and(|next| next.is_ascii_hexdigit());
.is_some_and(|next| valid_digit(*next, base));
if !valid_prev || !valid_next {
self.error(
"numeric separators must appear between digits",
Expand Down Expand Up @@ -404,16 +430,23 @@ impl Lexer<'_> {
self.push(TokenKind::CharLit, start, self.pos);
return;
}
let mut has_scalar = true;
if self.peek() == Some('\\') {
self.escape(start);
} else if self
.peek()
.is_some_and(|c| c != '\'' && c != '\n' && c != '\r')
{
self.bump();
} else {
has_scalar = false;
}
if self.peek() == Some('\'') {
self.bump();
if !has_scalar {
// §16.1 CHAR_LIT: exactly one Unicode scalar value.
self.error("empty character literal", start, self.pos);
}
self.push(TokenKind::CharLit, start, self.pos);
} else {
self.error("unterminated character literal", start, self.pos);
Expand Down Expand Up @@ -609,4 +642,54 @@ mod tests {
assert!(lex(r#""\u{D800}""#).is_err());
assert!(lex(r#""\u{110000}""#).is_err());
}

#[test]
fn rejects_empty_char_literal() {
let err = lex("''").unwrap_err();
assert!(err[0].message.contains("empty character literal"));
assert!(lex("' '").is_ok());
assert!(lex(r"'\''").is_ok());
}

#[test]
fn rejects_bare_base_prefixes() {
for (source, name) in [("0x", "hexadecimal"), ("0o", "octal"), ("0b", "binary")] {
let err = lex(source).unwrap_err();
assert!(err[0].message.contains(name), "{source}: {err:?}");
}
// A prefix followed only by separators trips the separator rule instead.
assert!(lex("0x_").is_err());
assert!(lex("0xFF").is_ok());
}

#[test]
fn rejects_int_suffix_on_float_literals() {
let err = lex("3.14u32").unwrap_err();
assert!(err[0].message.contains("f32"));
assert!(lex("1e10u8").is_err());
assert!(lex("3.14f32").is_ok());
assert!(lex("1e10f64").is_ok());
}

#[test]
fn rejects_float_suffix_on_based_literals() {
let err = lex("0b1f32").unwrap_err();
assert!(err[0].message.contains("decimal"));
assert!(lex("0o7f64").is_err());
// Hex swallows `f` as a digit, so `0xFf32` stays a hex int literal.
let tokens = lex("0xFf32").unwrap();
assert_eq!(tokens[0].kind, TokenKind::IntLit);
}

#[test]
fn separator_validation_is_base_aware() {
// `e` is a hex digit character but not a decimal digit: a separator
// touching the exponent marker must be rejected.
assert!(lex("1_e5").is_err());
assert!(lex("1e_5").is_err());
assert!(lex("1.5_e3").is_err());
assert!(lex("1_000.5e10").is_ok());
assert!(lex("0xF_F").is_ok());
assert!(lex("0xd_e").is_ok());
}
}
Loading