1//! Lexer for the j language (§3.1).
5#[derive(Debug, Clone, PartialEq)]
11 IdLit(String), // existing id, full or unique prefix
15 Selector(String), // `.name` with no whitespace
16 Op(&'static str), // symbolic operator
17 // keywords / punctuation
39 Newline, // layout marker
43#[derive(Debug, Clone, PartialEq)]
46 pub line: usize, // 1-based
47 pub col: usize, // 1-based
48 /// byte offsets of the token in the source: `start..end`
59const SYMBOLIC_OPS: &[&str] = &[
60 "++", "::", "==", "/=", "<=", ">=", "&&", "||", ".", "*", "+", "-", "<", ">",
63fn is_ident_start(c: char) -> bool {
64 c.is_ascii_lowercase() || c == '_'
66fn is_ident_char(c: char) -> bool {
67 c.is_ascii_alphanumeric() || c == '_' || c == '\''
69fn is_type_start(c: char) -> bool {
70 c.is_ascii_uppercase()
72fn is_id_char(c: char) -> bool {
73 ('k'..='z').contains(&c)
76fn valid_label(s: &str) -> bool {
79 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '/' | '-'))
80 && !s.starts_with('.')
81 && !s.starts_with('-')
84 && !s.ends_with(".lock")
87pub fn lex(src: &str) -> Result<Vec<SpTok>, LexError> {
88 let mut toks: Vec<SpTok> = Vec::new();
89 let chars: Vec<char> = src.chars().collect();
90 // byte offset of each char, and of the end: tokens carry byte spans so
91 // the parser can keep a lambda's source text (§5.2)
92 let bytes: Vec<usize> = src
95 .chain(std::iter::once(src.len()))
98 let mut line = 1usize;
99 let mut col = 1usize; // column of next char
103 return Err(LexError {
110 // push helper; `$s..$e` are the token's char indices
112 ($t:expr, $l:expr, $c:expr, $s:expr, $e:expr) => {
123 while i < chars.len() {
126 push!(Tok::Newline, line, col, i, i + 1);
132 if c == ' ' || c == '\t' || c == '\r' {
137 let start_line = line;
141 if c == '-' && i + 1 < chars.len() && chars[i + 1] == '-' {
142 // could be `--` comment, but `->`? no: `->` is dash-gt, distinct.
143 while i < chars.len() && chars[i] != '\n' {
149 if c == '{' && i + 1 < chars.len() && chars[i + 1] == '-' {
150 let mut depth = 1usize;
153 while i < chars.len() && depth > 0 {
154 if chars[i] == '{' && i + 1 < chars.len() && chars[i + 1] == '-' {
158 } else if chars[i] == '-' && i + 1 < chars.len() && chars[i + 1] == '}' {
162 } else if chars[i] == '\n' {
163 push!(Tok::Newline, line, col, i, i + 1);
173 err!("unterminated block comment");
175 // a comment is whitespace (§3.1): one spanning lines has already
176 // ended the line it started on, with the Newlines pushed above,
177 // and one that does not must leave its line unbroken
180 // arrow `->` (check before operator `-`)
181 if c == '-' && i + 1 < chars.len() && chars[i + 1] == '>' {
182 push!(Tok::Arrow, start_line, start_col, i, i + 2);
187 // path literal: `./` at start of token
188 if c == '.' && i + 1 < chars.len() && chars[i + 1] == '/' {
191 let mut comp = String::new();
192 let mut comps: Vec<String> = Vec::new();
193 while i < chars.len() {
195 if d.is_whitespace() || matches!(d, '(' | ')' | '[' | ']' | '{' | '}' | ',' | ';') {
199 comps.push(std::mem::take(&mut comp));
202 // trailing slash: peek—if next is terminator, stop (drop empty comp)
204 || chars[i].is_whitespace()
205 || matches!(chars[i], '(' | ')' | '[' | ']' | '{' | '}' | ',' | ';')
215 if !comp.is_empty() {
218 push!(Tok::PathLit(comps), start_line, start_col, start, i);
221 // selector: `.` immediately followed by identifier-start
222 if c == '.' && i + 1 < chars.len() && is_ident_start(chars[i + 1]) {
223 let mut name = String::new();
226 while i < chars.len() && is_ident_char(chars[i]) {
231 push!(Tok::Selector(name), start_line, start_col, start, i);
236 if i + 1 >= chars.len() || !is_ident_char(chars[i + 1]) {
237 push!(Tok::NewId, start_line, start_col, i, i + 1);
242 if is_id_char(chars[i + 1]) {
243 let mut s = String::new();
246 while i < chars.len() && is_id_char(chars[i]) {
251 // if the next char is still an identifier char, it's a lexical error
252 if i < chars.len() && is_ident_char(chars[i]) {
253 err!("invalid id literal");
255 push!(Tok::IdLit(s), start_line, start_col, start, i);
258 err!("`@` followed by an identifier character outside k-z is not a valid id literal");
260 // `%` label literals
262 let mut s = String::new();
265 while i < chars.len() {
267 if d.is_ascii_alphanumeric() || matches!(d, '.' | '_' | '/' | '-') {
275 if !valid_label(&s) {
276 err!("invalid label literal");
278 push!(Tok::LabelLit(s), start_line, start_col, start, i);
281 // text literal, double- or single-quoted
282 if c == '"' || c == '\'' {
286 let mut s = String::new();
288 if i >= chars.len() {
289 err!("unterminated text literal");
299 if i + 1 >= chars.len() {
300 err!("unterminated text literal");
302 let e = chars[i + 1];
310 _ => err!("unknown escape in text literal"),
316 '\n' => err!("newline in text literal"),
324 push!(Tok::Text(s), start_line, start_col, start, i);
328 if c.is_ascii_digit() {
329 let mut s = String::new();
330 while i < chars.len() && chars[i].is_ascii_digit() {
335 push!(Tok::Int(s.parse::<BigInt>().unwrap()), start_line, start_col, start, i);
338 // identifier / keyword
339 if is_ident_start(c) {
340 let mut s = String::new();
341 while i < chars.len() && is_ident_char(chars[i]) {
346 let t = match s.as_str() {
354 "false" => Tok::False,
364 push!(t, start_line, start_col, start, i);
368 if is_type_start(c) {
369 let mut s = String::new();
370 while i < chars.len() {
372 if d.is_ascii_alphanumeric() || d == '_' {
380 push!(Tok::TypeName(s), start_line, start_col, start, i);
383 // symbolic operators: longest match
384 let rest: String = chars[i..].iter().take(2).collect();
385 let mut matched: Option<&'static str> = None;
386 for op in SYMBOLIC_OPS {
387 if op.len() == 2 && rest == *op {
392 if matched.is_none() {
393 // compare by character: `rest` is built from chars, so slicing it
394 // by byte would panic on any non-ASCII character reaching here
395 for op in SYMBOLIC_OPS {
396 if op.len() == 1 && op.starts_with(c) {
402 if let Some(op) = matched {
403 push!(Tok::Op(op), start_line, start_col, i, i + op.len());
412 '[' => Tok::LBracket,
413 ']' => Tok::RBracket,
419 '\\' => Tok::Backslash,
421 _ => err!(format!("unexpected character `{}`", c)),
423 push!(t, start_line, start_col, i, i + 1);
427 push!(Tok::Eof, line, col, i, i);
432 pub fn describe(&self) -> String {
434 Tok::Ident(s) => format!("`{}`", s),
435 Tok::TypeName(s) => format!("`{}`", s),
436 Tok::Int(n) => format!("`{}`", n),
437 Tok::Text(_) => "a text literal".into(),
438 Tok::IdLit(s) => format!("`@{}`", s),
439 Tok::NewId => "`@`".into(),
440 Tok::LabelLit(s) => format!("`%{}`", s),
441 Tok::PathLit(_) => "a path literal".into(),
442 Tok::Selector(s) => format!("`.{}`", s),
443 Tok::Op(o) => format!("`{}`", o),
444 other => format!("`{:?}`", other).to_lowercase(),
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list