Archive Rust bootstrap — El compiler is now self-hosting
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "el-lexer"
|
||||
description = "Engram language tokenizer"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
@@ -0,0 +1,35 @@
|
||||
//! Lexer error types.
|
||||
|
||||
use thiserror::Error;
|
||||
use crate::token::Span;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Error)]
|
||||
#[error("{kind} at {span}")]
|
||||
pub struct LexError {
|
||||
pub kind: LexErrorKind,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
impl LexError {
|
||||
pub fn new(kind: LexErrorKind, span: Span) -> Self {
|
||||
Self { kind, span }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Error)]
|
||||
pub enum LexErrorKind {
|
||||
#[error("unexpected character {0:?}")]
|
||||
UnexpectedChar(char),
|
||||
|
||||
#[error("unterminated string literal")]
|
||||
UnterminatedString,
|
||||
|
||||
#[error("invalid escape sequence \\{0}")]
|
||||
InvalidEscape(char),
|
||||
|
||||
#[error("invalid numeric literal")]
|
||||
InvalidNumeric,
|
||||
|
||||
#[error("integer literal out of range")]
|
||||
IntegerOverflow,
|
||||
}
|
||||
@@ -0,0 +1,602 @@
|
||||
//! Single-pass lexer implementation.
|
||||
|
||||
use crate::error::{LexError, LexErrorKind};
|
||||
use crate::token::{Span, Spanned, Token};
|
||||
|
||||
/// Tokenize an Engram source string.
|
||||
///
|
||||
/// Returns a `Vec` ending with a single [`Token::Eof`]. On the first
|
||||
/// unrecognised character the function returns an error; partial output
|
||||
/// is discarded.
|
||||
pub fn tokenize(source: &str) -> Result<Vec<Spanned<Token>>, LexError> {
|
||||
let mut lex = Lexer::new(source);
|
||||
lex.scan_all()
|
||||
}
|
||||
|
||||
// ── Lexer state ──────────────────────────────────────────────────────────────
|
||||
|
||||
struct Lexer<'src> {
|
||||
src: &'src str,
|
||||
/// Current byte position into `src`.
|
||||
pos: usize,
|
||||
line: u32,
|
||||
/// Byte position of the start of the current line (for computing columns).
|
||||
line_start: usize,
|
||||
}
|
||||
|
||||
impl<'src> Lexer<'src> {
|
||||
fn new(src: &'src str) -> Self {
|
||||
Self { src, pos: 0, line: 1, line_start: 0 }
|
||||
}
|
||||
|
||||
// ── Utilities ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn col_at(&self, byte_pos: usize) -> u32 {
|
||||
(byte_pos - self.line_start + 1) as u32
|
||||
}
|
||||
|
||||
fn span_from(&self, start: usize) -> Span {
|
||||
Span::new(start, self.pos, self.line, self.col_at(start))
|
||||
}
|
||||
|
||||
fn peek(&self) -> Option<char> {
|
||||
self.src[self.pos..].chars().next()
|
||||
}
|
||||
|
||||
fn peek2(&self) -> Option<char> {
|
||||
let mut chars = self.src[self.pos..].chars();
|
||||
chars.next();
|
||||
chars.next()
|
||||
}
|
||||
|
||||
fn advance(&mut self) -> Option<char> {
|
||||
let ch = self.peek()?;
|
||||
self.pos += ch.len_utf8();
|
||||
if ch == '\n' {
|
||||
self.line += 1;
|
||||
self.line_start = self.pos;
|
||||
}
|
||||
Some(ch)
|
||||
}
|
||||
|
||||
/// Consume the next character only if it matches `expected`.
|
||||
fn eat(&mut self, expected: char) -> bool {
|
||||
if self.peek() == Some(expected) {
|
||||
self.advance();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn at_end(&self) -> bool {
|
||||
self.pos >= self.src.len()
|
||||
}
|
||||
|
||||
fn spanned(&self, tok: Token, start: usize) -> Spanned<Token> {
|
||||
Spanned::new(tok, self.span_from(start))
|
||||
}
|
||||
|
||||
// ── Main scan loop ────────────────────────────────────────────────────────
|
||||
|
||||
fn scan_all(&mut self) -> Result<Vec<Spanned<Token>>, LexError> {
|
||||
let mut tokens = Vec::new();
|
||||
loop {
|
||||
self.skip_whitespace_and_comments();
|
||||
if self.at_end() {
|
||||
let span = Span::point(self.pos, self.line, self.col_at(self.pos));
|
||||
tokens.push(Spanned::new(Token::Eof, span));
|
||||
break;
|
||||
}
|
||||
let tok = self.scan_token()?;
|
||||
tokens.push(tok);
|
||||
}
|
||||
Ok(tokens)
|
||||
}
|
||||
|
||||
fn skip_whitespace_and_comments(&mut self) {
|
||||
loop {
|
||||
// Skip whitespace
|
||||
while let Some(ch) = self.peek() {
|
||||
if ch.is_whitespace() {
|
||||
self.advance();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Skip line comments `// ...`
|
||||
if self.peek() == Some('/') && self.peek2() == Some('/') {
|
||||
self.advance(); // first /
|
||||
self.advance(); // second /
|
||||
while let Some(ch) = self.peek() {
|
||||
self.advance();
|
||||
if ch == '\n' {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn scan_token(&mut self) -> Result<Spanned<Token>, LexError> {
|
||||
let start = self.pos;
|
||||
let ch = self.advance().unwrap();
|
||||
|
||||
let tok = match ch {
|
||||
// ── Delimiters ──────────────────────────────────────────────────
|
||||
'(' => Token::LParen,
|
||||
')' => Token::RParen,
|
||||
'{' => Token::LBrace,
|
||||
'}' => Token::RBrace,
|
||||
'[' => Token::LBracket,
|
||||
']' => Token::RBracket,
|
||||
',' => Token::Comma,
|
||||
'.' => Token::Dot,
|
||||
';' => Token::Semicolon,
|
||||
|
||||
// ── Operators that may be multi-char ────────────────────────────
|
||||
'+' => Token::Plus,
|
||||
'*' => Token::Star,
|
||||
'%' => Token::Percent,
|
||||
'^' => Token::Caret,
|
||||
'~' => Token::Tilde,
|
||||
'-' => {
|
||||
if self.eat('>') {
|
||||
Token::Arrow
|
||||
} else {
|
||||
Token::Minus
|
||||
}
|
||||
}
|
||||
'/' => Token::Slash,
|
||||
'=' => {
|
||||
if self.eat('=') {
|
||||
Token::EqEq
|
||||
} else if self.eat('>') {
|
||||
Token::FatArrow
|
||||
} else {
|
||||
Token::Eq
|
||||
}
|
||||
}
|
||||
'!' => {
|
||||
if self.eat('=') {
|
||||
Token::NotEq
|
||||
} else {
|
||||
Token::Not
|
||||
}
|
||||
}
|
||||
'<' => {
|
||||
if self.eat('<') {
|
||||
Token::Shl
|
||||
} else if self.eat('=') {
|
||||
Token::LtEq
|
||||
} else {
|
||||
Token::Lt
|
||||
}
|
||||
}
|
||||
'>' => {
|
||||
if self.eat('>') {
|
||||
Token::Shr
|
||||
} else if self.eat('=') {
|
||||
Token::GtEq
|
||||
} else {
|
||||
Token::Gt
|
||||
}
|
||||
}
|
||||
'&' => {
|
||||
if self.eat('&') {
|
||||
Token::And
|
||||
} else {
|
||||
Token::Ampersand
|
||||
}
|
||||
}
|
||||
'|' => {
|
||||
if self.eat('|') {
|
||||
Token::Or
|
||||
} else if self.eat('>') {
|
||||
Token::PipeOp
|
||||
} else {
|
||||
Token::Pipe
|
||||
}
|
||||
}
|
||||
'@' => Token::At,
|
||||
'#' => Token::Hash,
|
||||
'?' => {
|
||||
if self.eat('?') { Token::NullCoalesce } else { Token::QuestionMark }
|
||||
}
|
||||
':' => {
|
||||
if self.eat(':') {
|
||||
Token::ColonColon
|
||||
} else {
|
||||
Token::Colon
|
||||
}
|
||||
}
|
||||
|
||||
// ── String literals ──────────────────────────────────────────────
|
||||
'"' => self.scan_string(start)?,
|
||||
|
||||
// ── Numeric literals ─────────────────────────────────────────────
|
||||
c if c.is_ascii_digit() => self.scan_number(start, c)?,
|
||||
|
||||
// ── Identifiers and keywords ─────────────────────────────────────
|
||||
c if c.is_alphabetic() || c == '_' => self.scan_ident_or_keyword(start, c),
|
||||
|
||||
other => {
|
||||
// Emit Unknown token instead of erroring — allows JSX text with
|
||||
// Unicode / special characters to pass through the lexer gracefully.
|
||||
Token::Unknown(other)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(self.spanned(tok, start))
|
||||
}
|
||||
|
||||
// ── String scanning ───────────────────────────────────────────────────────
|
||||
|
||||
fn scan_string(&mut self, start: usize) -> Result<Token, LexError> {
|
||||
let mut s = String::new();
|
||||
loop {
|
||||
match self.peek() {
|
||||
None => {
|
||||
return Err(LexError::new(
|
||||
LexErrorKind::UnterminatedString,
|
||||
self.span_from(start),
|
||||
))
|
||||
}
|
||||
Some('"') => {
|
||||
self.advance();
|
||||
return Ok(Token::StringLiteral(s));
|
||||
}
|
||||
Some('\\') => {
|
||||
self.advance(); // consume backslash
|
||||
match self.peek() {
|
||||
Some('n') => { self.advance(); s.push('\n'); }
|
||||
Some('t') => { self.advance(); s.push('\t'); }
|
||||
Some('r') => { self.advance(); s.push('\r'); }
|
||||
Some('"') => { self.advance(); s.push('"'); }
|
||||
Some('\\') => { self.advance(); s.push('\\'); }
|
||||
Some('0') => { self.advance(); s.push('\0'); }
|
||||
Some(c) => {
|
||||
let esc = c;
|
||||
self.advance();
|
||||
return Err(LexError::new(
|
||||
LexErrorKind::InvalidEscape(esc),
|
||||
self.span_from(start),
|
||||
));
|
||||
}
|
||||
None => {
|
||||
return Err(LexError::new(
|
||||
LexErrorKind::UnterminatedString,
|
||||
self.span_from(start),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(c) => {
|
||||
s.push(c);
|
||||
self.advance();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Numeric scanning ──────────────────────────────────────────────────────
|
||||
|
||||
fn scan_number(&mut self, start: usize, first: char) -> Result<Token, LexError> {
|
||||
let mut raw = String::new();
|
||||
raw.push(first);
|
||||
|
||||
let mut is_float = false;
|
||||
|
||||
// Collect digits
|
||||
while let Some(c) = self.peek() {
|
||||
if c.is_ascii_digit() || c == '_' {
|
||||
raw.push(c);
|
||||
self.advance();
|
||||
} else if c == '.' && self.peek2().is_some_and(|d| d.is_ascii_digit()) {
|
||||
is_float = true;
|
||||
raw.push(c);
|
||||
self.advance();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if is_float {
|
||||
let clean: String = raw.chars().filter(|&c| c != '_').collect();
|
||||
let v: f64 = clean.parse().map_err(|_| LexError::new(
|
||||
LexErrorKind::InvalidNumeric,
|
||||
self.span_from(start),
|
||||
))?;
|
||||
Ok(Token::FloatLiteral(v))
|
||||
} else {
|
||||
let clean: String = raw.chars().filter(|&c| c != '_').collect();
|
||||
let v: i64 = clean.parse().map_err(|_| LexError::new(
|
||||
LexErrorKind::IntegerOverflow,
|
||||
self.span_from(start),
|
||||
))?;
|
||||
Ok(Token::IntLiteral(v))
|
||||
}
|
||||
}
|
||||
|
||||
// ── Identifier / keyword scanning ─────────────────────────────────────────
|
||||
|
||||
fn scan_ident_or_keyword(&mut self, _start: usize, first: char) -> Token {
|
||||
let mut s = String::new();
|
||||
s.push(first);
|
||||
while let Some(c) = self.peek() {
|
||||
if c.is_alphanumeric() || c == '_' {
|
||||
s.push(c);
|
||||
self.advance();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
keyword_or_ident(s)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Keyword table ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn keyword_or_ident(s: String) -> Token {
|
||||
match s.as_str() {
|
||||
"let" => Token::Let,
|
||||
"fn" => Token::Fn,
|
||||
"type" => Token::Type,
|
||||
"enum" => Token::Enum,
|
||||
"match" => Token::Match,
|
||||
"return" => Token::Return,
|
||||
"activate" => Token::Activate,
|
||||
"where" => Token::Where,
|
||||
"sealed" => Token::Sealed,
|
||||
"if" => Token::If,
|
||||
"else" => Token::Else,
|
||||
"for" => Token::For,
|
||||
"in" => Token::In,
|
||||
"while" => Token::While,
|
||||
"test" => Token::Test,
|
||||
"seed" => Token::Seed,
|
||||
"assert" => Token::Assert,
|
||||
"target" => Token::Target,
|
||||
"protocol" => Token::Protocol,
|
||||
"impl" => Token::Impl,
|
||||
"import" => Token::Import,
|
||||
"from" => Token::From,
|
||||
"as" => Token::As,
|
||||
"true" => Token::BoolLiteral(true),
|
||||
"false" => Token::BoolLiteral(false),
|
||||
"with" => Token::With,
|
||||
"retry" => Token::Retry,
|
||||
"times" => Token::Times,
|
||||
"fallback" => Token::Fallback,
|
||||
"reason" => Token::Reason,
|
||||
"parallel" => Token::Parallel,
|
||||
"trace" => Token::Trace,
|
||||
"requires" => Token::Requires,
|
||||
"deploy" => Token::Deploy,
|
||||
"to" => Token::To,
|
||||
"via" => Token::Via,
|
||||
"component" => Token::Component,
|
||||
"props" => Token::Props,
|
||||
"state" => Token::State,
|
||||
"template" => Token::Template,
|
||||
_ => Token::Ident(s),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::token::Token;
|
||||
|
||||
fn toks(src: &str) -> Vec<Token> {
|
||||
tokenize(src).unwrap().into_iter().map(|s| s.node).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_source() {
|
||||
let result = tokenize("").unwrap();
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].node, Token::Eof);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keywords() {
|
||||
let src = "let fn type enum match return activate where sealed if else for in";
|
||||
let tokens = toks(src);
|
||||
assert_eq!(tokens[0], Token::Let);
|
||||
assert_eq!(tokens[1], Token::Fn);
|
||||
assert_eq!(tokens[2], Token::Type);
|
||||
assert_eq!(tokens[3], Token::Enum);
|
||||
assert_eq!(tokens[4], Token::Match);
|
||||
assert_eq!(tokens[5], Token::Return);
|
||||
assert_eq!(tokens[6], Token::Activate);
|
||||
assert_eq!(tokens[7], Token::Where);
|
||||
assert_eq!(tokens[8], Token::Sealed);
|
||||
assert_eq!(tokens[9], Token::If);
|
||||
assert_eq!(tokens[10], Token::Else);
|
||||
assert_eq!(tokens[11], Token::For);
|
||||
assert_eq!(tokens[12], Token::In);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bool_literals() {
|
||||
let tokens = toks("true false");
|
||||
assert_eq!(tokens[0], Token::BoolLiteral(true));
|
||||
assert_eq!(tokens[1], Token::BoolLiteral(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_int_literal() {
|
||||
let tokens = toks("42 0 1_000_000");
|
||||
assert_eq!(tokens[0], Token::IntLiteral(42));
|
||||
assert_eq!(tokens[1], Token::IntLiteral(0));
|
||||
assert_eq!(tokens[2], Token::IntLiteral(1_000_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_float_literal() {
|
||||
let tokens = toks("3.14 0.5");
|
||||
assert_eq!(tokens[0], Token::FloatLiteral(3.14));
|
||||
assert_eq!(tokens[1], Token::FloatLiteral(0.5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_literal() {
|
||||
let tokens = toks(r#""hello" "world\n""#);
|
||||
assert_eq!(tokens[0], Token::StringLiteral("hello".into()));
|
||||
assert_eq!(tokens[1], Token::StringLiteral("world\n".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_operators() {
|
||||
let src = "+ - * / = == != < > <= >= && || ! -> =>";
|
||||
let tokens = toks(src);
|
||||
assert_eq!(tokens[0], Token::Plus);
|
||||
assert_eq!(tokens[1], Token::Minus);
|
||||
assert_eq!(tokens[2], Token::Star);
|
||||
assert_eq!(tokens[3], Token::Slash);
|
||||
assert_eq!(tokens[4], Token::Eq);
|
||||
assert_eq!(tokens[5], Token::EqEq);
|
||||
assert_eq!(tokens[6], Token::NotEq);
|
||||
assert_eq!(tokens[7], Token::Lt);
|
||||
assert_eq!(tokens[8], Token::Gt);
|
||||
assert_eq!(tokens[9], Token::LtEq);
|
||||
assert_eq!(tokens[10], Token::GtEq);
|
||||
assert_eq!(tokens[11], Token::And);
|
||||
assert_eq!(tokens[12], Token::Or);
|
||||
assert_eq!(tokens[13], Token::Not);
|
||||
assert_eq!(tokens[14], Token::Arrow);
|
||||
assert_eq!(tokens[15], Token::FatArrow);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delimiters() {
|
||||
let src = "( ) { } [ ] , : :: . ;";
|
||||
let tokens = toks(src);
|
||||
assert_eq!(tokens[0], Token::LParen);
|
||||
assert_eq!(tokens[1], Token::RParen);
|
||||
assert_eq!(tokens[2], Token::LBrace);
|
||||
assert_eq!(tokens[3], Token::RBrace);
|
||||
assert_eq!(tokens[4], Token::LBracket);
|
||||
assert_eq!(tokens[5], Token::RBracket);
|
||||
assert_eq!(tokens[6], Token::Comma);
|
||||
assert_eq!(tokens[7], Token::Colon);
|
||||
assert_eq!(tokens[8], Token::ColonColon);
|
||||
assert_eq!(tokens[9], Token::Dot);
|
||||
assert_eq!(tokens[10], Token::Semicolon);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_line_comment_skipped() {
|
||||
let tokens = toks("let // this is a comment\nfn");
|
||||
assert_eq!(tokens[0], Token::Let);
|
||||
assert_eq!(tokens[1], Token::Fn);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_span_line_col() {
|
||||
let src = "let\nfn";
|
||||
let tokens = tokenize(src).unwrap();
|
||||
assert_eq!(tokens[0].span.line, 1);
|
||||
assert_eq!(tokens[0].span.col, 1);
|
||||
assert_eq!(tokens[1].span.line, 2);
|
||||
assert_eq!(tokens[1].span.col, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unterminated_string_error() {
|
||||
let result = tokenize(r#""unterminated"#);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_at_token() {
|
||||
let tokens = toks("@public");
|
||||
assert_eq!(tokens[0], Token::At);
|
||||
assert_eq!(tokens[1], Token::Ident("public".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hello_world_program() {
|
||||
let src = r#"
|
||||
fn greet(name: String) -> String {
|
||||
return "Hello, " + name
|
||||
}
|
||||
|
||||
let msg: String = greet("Will")
|
||||
"#;
|
||||
let tokens = tokenize(src).unwrap();
|
||||
// Verify it tokenizes without error and the last token is EOF
|
||||
assert_eq!(tokens.last().unwrap().node, Token::Eof);
|
||||
// Should have a reasonable number of tokens
|
||||
assert!(tokens.len() > 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_activate_syntax() {
|
||||
let src = r#"activate User where "customer who purchased recently""#;
|
||||
let tokens = toks(src);
|
||||
assert_eq!(tokens[0], Token::Activate);
|
||||
assert_eq!(tokens[1], Token::Ident("User".into()));
|
||||
assert_eq!(tokens[2], Token::Where);
|
||||
assert_eq!(tokens[3], Token::StringLiteral("customer who purchased recently".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_colon_colon_path() {
|
||||
let tokens = toks("Status::Active");
|
||||
assert_eq!(tokens[0], Token::Ident("Status".into()));
|
||||
assert_eq!(tokens[1], Token::ColonColon);
|
||||
assert_eq!(tokens[2], Token::Ident("Active".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_keywords() {
|
||||
let tokens = toks("protocol impl import from as");
|
||||
assert_eq!(tokens[0], Token::Protocol);
|
||||
assert_eq!(tokens[1], Token::Impl);
|
||||
assert_eq!(tokens[2], Token::Import);
|
||||
assert_eq!(tokens[3], Token::From);
|
||||
assert_eq!(tokens[4], Token::As);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pipe_token() {
|
||||
let tokens = toks("|x: Int|");
|
||||
assert_eq!(tokens[0], Token::Pipe);
|
||||
assert_eq!(tokens[1], Token::Ident("x".into()));
|
||||
assert_eq!(tokens[4], Token::Pipe);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_question_mark_token() {
|
||||
let tokens = toks("x?");
|
||||
assert_eq!(tokens[0], Token::Ident("x".into()));
|
||||
assert_eq!(tokens[1], Token::QuestionMark);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ident_with_underscore() {
|
||||
let tokens = toks("my_var _private __double");
|
||||
assert_eq!(tokens[0], Token::Ident("my_var".into()));
|
||||
assert_eq!(tokens[1], Token::Ident("_private".into()));
|
||||
assert_eq!(tokens[2], Token::Ident("__double".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sealed_block_tokens() {
|
||||
// sealed { let x: String = "secret" }
|
||||
// [0]=Sealed [1]={ [2]=let [3]=x [4]=: [5]=String [6]== [7]="secret" [8]=}
|
||||
let src = "sealed { let x: String = \"secret\" }";
|
||||
let tokens = toks(src);
|
||||
assert_eq!(tokens[0], Token::Sealed);
|
||||
assert_eq!(tokens[1], Token::LBrace);
|
||||
assert_eq!(tokens[2], Token::Let);
|
||||
assert_eq!(tokens[7], Token::StringLiteral("secret".into()));
|
||||
assert_eq!(tokens[8], Token::RBrace);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//! el-lexer — Engram language tokenizer.
|
||||
//!
|
||||
//! Converts source text into a flat stream of [`Spanned<Token>`] values.
|
||||
//! All spans carry byte offsets, line, and column so that the parser and
|
||||
//! diagnostics engine can point back to exact source positions.
|
||||
//!
|
||||
//! # Design
|
||||
//! - Single-pass; O(n) in source length.
|
||||
//! - No heap allocation per character — only allocates when producing
|
||||
//! string / identifier token payloads.
|
||||
//! - All errors carry a [`Span`] so the caller can produce good diagnostics.
|
||||
|
||||
mod error;
|
||||
mod lexer;
|
||||
mod token;
|
||||
|
||||
pub use error::{LexError, LexErrorKind};
|
||||
pub use lexer::tokenize;
|
||||
pub use token::{Span, Spanned, Token};
|
||||
@@ -0,0 +1,318 @@
|
||||
//! Token definitions and span types.
|
||||
|
||||
/// A span in the source file — byte offsets plus human-readable location.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Span {
|
||||
/// Byte offset of the first character of this token.
|
||||
pub start: usize,
|
||||
/// Byte offset one past the last character of this token.
|
||||
pub end: usize,
|
||||
/// 1-based line number.
|
||||
pub line: u32,
|
||||
/// 1-based column number (byte column within the line).
|
||||
pub col: u32,
|
||||
}
|
||||
|
||||
impl Span {
|
||||
pub fn new(start: usize, end: usize, line: u32, col: u32) -> Self {
|
||||
Self { start, end, line, col }
|
||||
}
|
||||
|
||||
/// A zero-width span at the given position (used for EOF).
|
||||
pub fn point(pos: usize, line: u32, col: u32) -> Self {
|
||||
Self { start: pos, end: pos, line, col }
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Span {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}:{}", self.line, self.col)
|
||||
}
|
||||
}
|
||||
|
||||
/// A value annotated with its source location.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Spanned<T> {
|
||||
pub node: T,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
impl<T> Spanned<T> {
|
||||
pub fn new(node: T, span: Span) -> Self {
|
||||
Self { node, span }
|
||||
}
|
||||
}
|
||||
|
||||
/// All tokens the Engram language lexer can produce.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Token {
|
||||
// ── Keywords ──────────────────────────────────────────────────────────────
|
||||
/// `let`
|
||||
Let,
|
||||
/// `fn`
|
||||
Fn,
|
||||
/// `type`
|
||||
Type,
|
||||
/// `enum`
|
||||
Enum,
|
||||
/// `match`
|
||||
Match,
|
||||
/// `return`
|
||||
Return,
|
||||
/// `activate` — the spreading-activation query construct
|
||||
Activate,
|
||||
/// `where` — used in `activate T where "query"`
|
||||
Where,
|
||||
/// `sealed` — quantum-sealed block
|
||||
Sealed,
|
||||
/// `if`
|
||||
If,
|
||||
/// `else`
|
||||
Else,
|
||||
/// `for`
|
||||
For,
|
||||
/// `in`
|
||||
In,
|
||||
/// `while`
|
||||
While,
|
||||
/// `test` — test block definition
|
||||
Test,
|
||||
/// `seed` — graph seeding statement inside a test
|
||||
Seed,
|
||||
/// `assert` — assertion statement inside a test
|
||||
Assert,
|
||||
/// `target` — test target annotation (`target: e2e`)
|
||||
Target,
|
||||
/// `protocol` — protocol definition
|
||||
Protocol,
|
||||
/// `impl` — protocol implementation block
|
||||
Impl,
|
||||
/// `import` — import statement
|
||||
Import,
|
||||
/// `from` — `from package import { ... }`
|
||||
From,
|
||||
/// `as` — alias in import (`import X as Y`)
|
||||
As,
|
||||
/// `with` — record update syntax
|
||||
With,
|
||||
/// `retry` — retry block
|
||||
Retry,
|
||||
/// `times` — used in `retry N times`
|
||||
Times,
|
||||
/// `fallback` — fallback block in retry
|
||||
Fallback,
|
||||
/// `reason` — AI inference primitive
|
||||
Reason,
|
||||
/// `parallel` — concurrent execution block
|
||||
Parallel,
|
||||
/// `trace` — zero-cost observability block
|
||||
Trace,
|
||||
/// `requires` — precondition annotation on fn
|
||||
Requires,
|
||||
/// `deploy` — deployment primitive
|
||||
Deploy,
|
||||
/// `to` — used in `deploy fn to "/route"`
|
||||
To,
|
||||
/// `via` — used in `deploy fn to "/route" via soma`
|
||||
Via,
|
||||
/// `component` — component definition
|
||||
Component,
|
||||
/// `props` — props block inside a component
|
||||
Props,
|
||||
/// `state` — state block inside a component
|
||||
State,
|
||||
/// `template` — template block inside a component
|
||||
Template,
|
||||
/// `|>` — pipe operator
|
||||
PipeOp,
|
||||
/// `true` / `false`
|
||||
BoolLiteral(bool),
|
||||
|
||||
// ── Literals ──────────────────────────────────────────────────────────────
|
||||
IntLiteral(i64),
|
||||
FloatLiteral(f64),
|
||||
/// String literal with escape sequences already resolved.
|
||||
StringLiteral(String),
|
||||
|
||||
// ── Identifiers ───────────────────────────────────────────────────────────
|
||||
Ident(String),
|
||||
|
||||
// ── Operators ─────────────────────────────────────────────────────────────
|
||||
/// `+`
|
||||
Plus,
|
||||
/// `-`
|
||||
Minus,
|
||||
/// `*`
|
||||
Star,
|
||||
/// `/`
|
||||
Slash,
|
||||
/// `=`
|
||||
Eq,
|
||||
/// `==`
|
||||
EqEq,
|
||||
/// `!=`
|
||||
NotEq,
|
||||
/// `<`
|
||||
Lt,
|
||||
/// `>`
|
||||
Gt,
|
||||
/// `<=`
|
||||
LtEq,
|
||||
/// `>=`
|
||||
GtEq,
|
||||
/// `&&`
|
||||
And,
|
||||
/// `||`
|
||||
Or,
|
||||
/// `!`
|
||||
Not,
|
||||
/// `->` (function return type arrow)
|
||||
Arrow,
|
||||
/// `=>` (match arm)
|
||||
FatArrow,
|
||||
|
||||
// ── Delimiters ────────────────────────────────────────────────────────────
|
||||
/// `(`
|
||||
LParen,
|
||||
/// `)`
|
||||
RParen,
|
||||
/// `{`
|
||||
LBrace,
|
||||
/// `}`
|
||||
RBrace,
|
||||
/// `[`
|
||||
LBracket,
|
||||
/// `]`
|
||||
RBracket,
|
||||
/// `,`
|
||||
Comma,
|
||||
/// `:`
|
||||
Colon,
|
||||
/// `::`
|
||||
ColonColon,
|
||||
/// `.`
|
||||
Dot,
|
||||
/// `;`
|
||||
Semicolon,
|
||||
|
||||
// ── New single-char tokens ────────────────────────────────────────────────
|
||||
/// `@` — decorator prefix
|
||||
At,
|
||||
/// `|` — closure param delimiter (single pipe, not `||`)
|
||||
Pipe,
|
||||
/// `?` — used both for Optional types and the Try operator
|
||||
QuestionMark,
|
||||
/// `??` — null-coalescing operator: `a ?? b` returns `b` when `a` is nil/empty
|
||||
NullCoalesce,
|
||||
/// `%` — modulo
|
||||
Percent,
|
||||
/// `&` — bitwise AND
|
||||
Ampersand,
|
||||
/// `^` — bitwise XOR
|
||||
Caret,
|
||||
/// `~` — bitwise NOT
|
||||
Tilde,
|
||||
/// `<<` — left shift
|
||||
Shl,
|
||||
/// `>>` — right shift
|
||||
Shr,
|
||||
/// `#` — hash character (used in JSX text like `#tag`)
|
||||
Hash,
|
||||
/// Any other character not recognized by the lexer (emitted instead of erroring,
|
||||
/// so JSX text with Unicode / special characters can be skipped gracefully).
|
||||
Unknown(char),
|
||||
|
||||
// ── Special ───────────────────────────────────────────────────────────────
|
||||
Eof,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Token {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Token::Let => write!(f, "let"),
|
||||
Token::Fn => write!(f, "fn"),
|
||||
Token::Type => write!(f, "type"),
|
||||
Token::Enum => write!(f, "enum"),
|
||||
Token::Match => write!(f, "match"),
|
||||
Token::Return => write!(f, "return"),
|
||||
Token::Activate => write!(f, "activate"),
|
||||
Token::Where => write!(f, "where"),
|
||||
Token::Sealed => write!(f, "sealed"),
|
||||
Token::If => write!(f, "if"),
|
||||
Token::Else => write!(f, "else"),
|
||||
Token::For => write!(f, "for"),
|
||||
Token::While => write!(f, "while"),
|
||||
Token::In => write!(f, "in"),
|
||||
Token::Test => write!(f, "test"),
|
||||
Token::Seed => write!(f, "seed"),
|
||||
Token::Assert => write!(f, "assert"),
|
||||
Token::Target => write!(f, "target"),
|
||||
Token::Protocol => write!(f, "protocol"),
|
||||
Token::Impl => write!(f, "impl"),
|
||||
Token::Import => write!(f, "import"),
|
||||
Token::From => write!(f, "from"),
|
||||
Token::As => write!(f, "as"),
|
||||
Token::With => write!(f, "with"),
|
||||
Token::Retry => write!(f, "retry"),
|
||||
Token::Times => write!(f, "times"),
|
||||
Token::Fallback => write!(f, "fallback"),
|
||||
Token::Reason => write!(f, "reason"),
|
||||
Token::Parallel => write!(f, "parallel"),
|
||||
Token::Trace => write!(f, "trace"),
|
||||
Token::Requires => write!(f, "requires"),
|
||||
Token::Deploy => write!(f, "deploy"),
|
||||
Token::To => write!(f, "to"),
|
||||
Token::Via => write!(f, "via"),
|
||||
Token::Component => write!(f, "component"),
|
||||
Token::Props => write!(f, "props"),
|
||||
Token::State => write!(f, "state"),
|
||||
Token::Template => write!(f, "template"),
|
||||
Token::PipeOp => write!(f, "|>"),
|
||||
Token::At => write!(f, "@"),
|
||||
Token::Pipe => write!(f, "|"),
|
||||
Token::QuestionMark => write!(f, "?"),
|
||||
Token::NullCoalesce => write!(f, "??"),
|
||||
Token::Percent => write!(f, "%"),
|
||||
Token::Ampersand => write!(f, "&"),
|
||||
Token::Caret => write!(f, "^"),
|
||||
Token::Tilde => write!(f, "~"),
|
||||
Token::Shl => write!(f, "<<"),
|
||||
Token::Shr => write!(f, ">>"),
|
||||
Token::Hash => write!(f, "#"),
|
||||
Token::Unknown(c) => write!(f, "{c}"),
|
||||
Token::BoolLiteral(b) => write!(f, "{b}"),
|
||||
Token::IntLiteral(n) => write!(f, "{n}"),
|
||||
Token::FloatLiteral(n) => write!(f, "{n}"),
|
||||
Token::StringLiteral(s) => write!(f, "\"{s}\""),
|
||||
Token::Ident(s) => write!(f, "{s}"),
|
||||
Token::Plus => write!(f, "+"),
|
||||
Token::Minus => write!(f, "-"),
|
||||
Token::Star => write!(f, "*"),
|
||||
Token::Slash => write!(f, "/"),
|
||||
Token::Eq => write!(f, "="),
|
||||
Token::EqEq => write!(f, "=="),
|
||||
Token::NotEq => write!(f, "!="),
|
||||
Token::Lt => write!(f, "<"),
|
||||
Token::Gt => write!(f, ">"),
|
||||
Token::LtEq => write!(f, "<="),
|
||||
Token::GtEq => write!(f, ">="),
|
||||
Token::And => write!(f, "&&"),
|
||||
Token::Or => write!(f, "||"),
|
||||
Token::Not => write!(f, "!"),
|
||||
Token::Arrow => write!(f, "->"),
|
||||
Token::FatArrow => write!(f, "=>"),
|
||||
Token::LParen => write!(f, "("),
|
||||
Token::RParen => write!(f, ")"),
|
||||
Token::LBrace => write!(f, "{{"),
|
||||
Token::RBrace => write!(f, "}}"),
|
||||
Token::LBracket => write!(f, "["),
|
||||
Token::RBracket => write!(f, "]"),
|
||||
Token::Comma => write!(f, ","),
|
||||
Token::Colon => write!(f, ":"),
|
||||
Token::ColonColon => write!(f, "::"),
|
||||
Token::Dot => write!(f, "."),
|
||||
Token::Semicolon => write!(f, ";"),
|
||||
Token::Eof => write!(f, "<eof>"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user