|
| 1 | +import { any, not, seq, skipMany, surrounded } from "./combinators.ts"; |
| 2 | +import type { Parser } from "./Parser.ts"; |
| 3 | +import { regex, space, str } from "./parsers.ts"; |
| 4 | +import { map } from "./utility.ts"; |
| 5 | + |
| 6 | +export type TriviaParser = Parser<null>; |
| 7 | + |
| 8 | +/** |
| 9 | + * Match and skip a line comment: `// ...` until (but not including) `\n`. |
| 10 | + */ |
| 11 | +export const lineComment = (): Parser<null> => { |
| 12 | + return map(regex(/\/\/[^\n]*/, "line comment"), () => null); |
| 13 | +}; |
| 14 | + |
| 15 | +/** |
| 16 | + * Match and skip a block comment (non-greedy). |
| 17 | + */ |
| 18 | +export const blockComment = (): Parser<null> => { |
| 19 | + return map(regex(/\/\*[\s\S]*?\*\//, "block comment"), () => null); |
| 20 | +}; |
| 21 | + |
| 22 | +/** |
| 23 | + * Default "trivia" parser: whitespace and line/block comments. |
| 24 | + * |
| 25 | + * Designed to be used with `lexeme(...)` so most parsers don't need to handle |
| 26 | + * trivia explicitly. |
| 27 | + */ |
| 28 | +export const defaultTrivia = (): TriviaParser => { |
| 29 | + const piece = any(space(), lineComment(), blockComment()); |
| 30 | + return skipMany(piece); |
| 31 | +}; |
| 32 | + |
| 33 | +/** |
| 34 | + * Parse `p` and then consume trailing trivia. |
| 35 | + */ |
| 36 | +export const lexeme = <T>( |
| 37 | + p: Parser<T>, |
| 38 | + trivia: TriviaParser = defaultTrivia(), |
| 39 | +): Parser<T> => { |
| 40 | + return map(seq(p, trivia), ([v]) => v); |
| 41 | +}; |
| 42 | + |
| 43 | +/** |
| 44 | + * Parse a fixed string token and consume trailing trivia. |
| 45 | + */ |
| 46 | +export const symbol = ( |
| 47 | + s: string, |
| 48 | + trivia: TriviaParser = defaultTrivia(), |
| 49 | +): Parser<string> => { |
| 50 | + return lexeme(str(s), trivia); |
| 51 | +}; |
| 52 | + |
| 53 | +const identContinueChar = (): Parser<string> => { |
| 54 | + return regex(/[a-zA-Z0-9_]/, "identifier char"); |
| 55 | +}; |
| 56 | + |
| 57 | +/** |
| 58 | + * Parse a keyword and consume trailing trivia. |
| 59 | + * |
| 60 | + * Ensures the keyword is not immediately followed by an identifier character. |
| 61 | + */ |
| 62 | +export const keyword = ( |
| 63 | + s: string, |
| 64 | + trivia: TriviaParser = defaultTrivia(), |
| 65 | +): Parser<string> => { |
| 66 | + return lexeme( |
| 67 | + map(seq(str(s), not(identContinueChar())), ([kw]) => kw), |
| 68 | + trivia, |
| 69 | + ); |
| 70 | +}; |
| 71 | + |
| 72 | +export type Lexer = Readonly<{ |
| 73 | + trivia: TriviaParser; |
| 74 | + lexeme: <T>(p: Parser<T>) => Parser<T>; |
| 75 | + symbol: (s: string) => Parser<string>; |
| 76 | + keyword: (s: string) => Parser<string>; |
| 77 | + parens: <T>(p: Parser<T>) => Parser<T>; |
| 78 | +}>; |
| 79 | + |
| 80 | +/** |
| 81 | + * Create a small "lexer layer" around a trivia parser. |
| 82 | + * |
| 83 | + * This keeps grammars readable by centralizing whitespace/comment handling. |
| 84 | + */ |
| 85 | +export const createLexer = (opts?: { trivia?: TriviaParser }): Lexer => { |
| 86 | + const trivia = opts?.trivia ?? defaultTrivia(); |
| 87 | + return { |
| 88 | + trivia, |
| 89 | + lexeme: <T>(p: Parser<T>): Parser<T> => lexeme(p, trivia), |
| 90 | + symbol: (s: string): Parser<string> => symbol(s, trivia), |
| 91 | + keyword: (s: string): Parser<string> => keyword(s, trivia), |
| 92 | + parens: <T>(p: Parser<T>): Parser<T> => |
| 93 | + surrounded(symbol("(", trivia), p, symbol(")", trivia)), |
| 94 | + }; |
| 95 | +}; |
0 commit comments