| const TokenType = { |
| NUMBER: "NUMBER", |
| IDENT: "IDENT", |
| LET: "LET", |
| PRINT: "PRINT", |
| EQUAL: "EQUAL", |
| PLUS: "PLUS", |
| MINUS: "MINUS", |
| STAR: "STAR", |
| SLASH: "SLASH", |
| SEMICOLON: "SEMICOLON", |
| LPAREN: "LPAREN", |
| RPAREN: "RPAREN", |
| EOF: "EOF", |
| }; |
|
|
| const keywords = { |
| let: TokenType.LET, |
| print: TokenType.PRINT, |
| }; |
|
|
| class Token { |
| constructor(type, lexeme, literal, position) { |
| this.type = type; |
| this.lexeme = lexeme; |
| this.literal = literal; |
| this.position = position; |
| } |
| } |
|
|
| class Lexer { |
| constructor(source) { |
| this.source = source; |
| this.tokens = []; |
| this.start = 0; |
| this.current = 0; |
| this.line = 1; |
| this.column = 1; |
| } |
|
|
| scanTokens() { |
| while (!this.isAtEnd()) { |
| this.start = this.current; |
| this.scanToken(); |
| } |
| this.tokens.push(new Token(TokenType.EOF, "", null, { line: this.line, column: this.column })); |
| return this.tokens; |
| } |
|
|
| isAtEnd() { |
| return this.current >= this.source.length; |
| } |
|
|
| scanToken() { |
| const c = this.advance(); |
| if (this.isDigit(c)) { |
| this.number(); |
| return; |
| } |
| if (this.isAlpha(c)) { |
| this.identifier(); |
| return; |
| } |
| switch (c) { |
| case "+": |
| this.addToken(TokenType.PLUS); |
| break; |
| case "-": |
| this.addToken(TokenType.MINUS); |
| break; |
| case "*": |
| this.addToken(TokenType.STAR); |
| break; |
| case "/": |
| if (this.match("/")) { |
| while (!this.isAtEnd() && this.peek() !== "\n") { |
| this.advance(); |
| } |
| } else { |
| this.addToken(TokenType.SLASH); |
| } |
| break; |
| case "=": |
| this.addToken(TokenType.EQUAL); |
| break; |
| case ";": |
| this.addToken(TokenType.SEMICOLON); |
| break; |
| case "(": |
| this.addToken(TokenType.LPAREN); |
| break; |
| case ")": |
| this.addToken(TokenType.RPAREN); |
| break; |
| case " ": |
| case "\r": |
| case "\t": |
| break; |
| case "\n": |
| this.line += 1; |
| this.column = 1; |
| break; |
| default: |
| throw new Error("无法识别的字符: '" + c + "' 在第 " + this.line + " 行"); |
| } |
| } |
|
|
| advance() { |
| const ch = this.source[this.current]; |
| this.current += 1; |
| this.column += 1; |
| return ch; |
| } |
|
|
| addToken(type, literal = null) { |
| const text = this.source.slice(this.start, this.current); |
| this.tokens.push(new Token(type, text, literal, { line: this.line, column: this.column })); |
| } |
|
|
| match(expected) { |
| if (this.isAtEnd()) return false; |
| if (this.source[this.current] !== expected) return false; |
| this.current += 1; |
| this.column += 1; |
| return true; |
| } |
|
|
| peek() { |
| if (this.isAtEnd()) return "\0"; |
| return this.source[this.current]; |
| } |
|
|
| isDigit(c) { |
| return c >= "0" && c <= "9"; |
| } |
|
|
| isAlpha(c) { |
| return (c >= "a" && c <= "z") || (c >= "A" && c <= "Z") || c === "_"; |
| } |
|
|
| isAlphaNumeric(c) { |
| return this.isAlpha(c) || this.isDigit(c); |
| } |
|
|
| number() { |
| while (this.isDigit(this.peek())) { |
| this.advance(); |
| } |
| if (this.peek() === "." && this.isDigit(this.peekNext())) { |
| this.advance(); |
| while (this.isDigit(this.peek())) { |
| this.advance(); |
| } |
| } |
| const text = this.source.slice(this.start, this.current); |
| const value = Number(text); |
| this.addToken(TokenType.NUMBER, value); |
| } |
|
|
| peekNext() { |
| if (this.current + 1 >= this.source.length) return "\0"; |
| return this.source[this.current + 1]; |
| } |
|
|
| identifier() { |
| while (this.isAlphaNumeric(this.peek())) { |
| this.advance(); |
| } |
| const text = this.source.slice(this.start, this.current); |
| const type = keywords[text] || TokenType.IDENT; |
| this.addToken(type); |
| } |
| } |
|
|
| class Parser { |
| constructor(tokens) { |
| this.tokens = tokens; |
| this.current = 0; |
| } |
|
|
| parseProgram() { |
| const statements = []; |
| while (!this.isAtEnd()) { |
| statements.push(this.statement()); |
| } |
| return { type: "Program", statements }; |
| } |
|
|
| statement() { |
| if (this.match(TokenType.LET)) return this.letStatement(); |
| if (this.match(TokenType.PRINT)) return this.printStatement(); |
| const expr = this.expression(); |
| this.consume(TokenType.SEMICOLON, "期望在表达式后面出现分号"); |
| return { type: "ExprStmt", expression: expr }; |
| } |
|
|
| letStatement() { |
| const nameToken = this.consume(TokenType.IDENT, "let 后面需要变量名"); |
| this.consume(TokenType.EQUAL, "变量声明需要等号"); |
| const initializer = this.expression(); |
| this.consume(TokenType.SEMICOLON, "变量声明后需要分号"); |
| return { type: "LetStmt", name: nameToken.lexeme, initializer }; |
| } |
|
|
| printStatement() { |
| const value = this.expression(); |
| this.consume(TokenType.SEMICOLON, "print 后需要分号"); |
| return { type: "PrintStmt", expression: value }; |
| } |
|
|
| expression() { |
| return this.term(); |
| } |
|
|
| term() { |
| let expr = this.factor(); |
| while (this.match(TokenType.PLUS) || this.match(TokenType.MINUS)) { |
| const operator = this.previous(); |
| const right = this.factor(); |
| expr = { type: "Binary", left: expr, operator: operator.type, right }; |
| } |
| return expr; |
| } |
|
|
| factor() { |
| let expr = this.unary(); |
| while (this.match(TokenType.STAR) || this.match(TokenType.SLASH)) { |
| const operator = this.previous(); |
| const right = this.unary(); |
| expr = { type: "Binary", left: expr, operator: operator.type, right }; |
| } |
| return expr; |
| } |
|
|
| unary() { |
| if (this.match(TokenType.MINUS)) { |
| const operator = this.previous(); |
| const right = this.unary(); |
| return { type: "Unary", operator: operator.type, right }; |
| } |
| return this.primary(); |
| } |
|
|
| primary() { |
| if (this.match(TokenType.NUMBER)) { |
| return { type: "Literal", value: this.previous().literal }; |
| } |
| if (this.match(TokenType.IDENT)) { |
| return { type: "Variable", name: this.previous().lexeme }; |
| } |
| if (this.match(TokenType.LPAREN)) { |
| const expr = this.expression(); |
| this.consume(TokenType.RPAREN, "缺少右括号"); |
| return expr; |
| } |
| throw new Error("无法解析的表达式起始符号"); |
| } |
|
|
| match(type) { |
| if (this.check(type)) { |
| this.advance(); |
| return true; |
| } |
| return false; |
| } |
|
|
| consume(type, message) { |
| if (this.check(type)) return this.advance(); |
| throw new Error(message); |
| } |
|
|
| check(type) { |
| if (this.isAtEnd()) return false; |
| return this.peek().type === type; |
| } |
|
|
| advance() { |
| if (!this.isAtEnd()) this.current += 1; |
| return this.previous(); |
| } |
|
|
| isAtEnd() { |
| return this.peek().type === TokenType.EOF; |
| } |
|
|
| peek() { |
| return this.tokens[this.current]; |
| } |
|
|
| previous() { |
| return this.tokens[this.current - 1]; |
| } |
| } |
|
|
| class Interpreter { |
| constructor() { |
| this.globals = Object.create(null); |
| } |
|
|
| execute(program, outputCallback = console.log) { |
| for (const stmt of program.statements) { |
| this.executeStatement(stmt, outputCallback); |
| } |
| } |
|
|
| executeStatement(stmt, outputCallback) { |
| switch (stmt.type) { |
| case "LetStmt": |
| this.globals[stmt.name] = this.evaluate(stmt.initializer); |
| break; |
| case "PrintStmt": |
| const value = this.evaluate(stmt.expression); |
| outputCallback(value); |
| break; |
| case "ExprStmt": |
| this.evaluate(stmt.expression); |
| break; |
| default: |
| throw new Error("未知语句类型: " + stmt.type); |
| } |
| } |
|
|
| evaluate(expr) { |
| switch (expr.type) { |
| case "Literal": |
| return expr.value; |
| case "Variable": |
| if (Object.prototype.hasOwnProperty.call(this.globals, expr.name)) { |
| return this.globals[expr.name]; |
| } |
| throw new Error("未定义的变量: " + expr.name); |
| case "Unary": |
| const right = this.evaluate(expr.right); |
| if (expr.operator === TokenType.MINUS) { |
| this.ensureNumber(right); |
| return -right; |
| } |
| throw new Error("未知一元运算符"); |
| case "Binary": |
| const left = this.evaluate(expr.left); |
| const r = this.evaluate(expr.right); |
| if (expr.operator === TokenType.PLUS) { |
| this.ensureNumber(left); |
| this.ensureNumber(r); |
| return left + r; |
| } |
| if (expr.operator === TokenType.MINUS) { |
| this.ensureNumber(left); |
| this.ensureNumber(r); |
| return left - r; |
| } |
| if (expr.operator === TokenType.STAR) { |
| this.ensureNumber(left); |
| this.ensureNumber(r); |
| return left * r; |
| } |
| if (expr.operator === TokenType.SLASH) { |
| this.ensureNumber(left); |
| this.ensureNumber(r); |
| return left / r; |
| } |
| throw new Error("未知二元运算符"); |
| default: |
| throw new Error("未知表达式类型: " + expr.type); |
| } |
| } |
|
|
| ensureNumber(value) { |
| if (typeof value !== "number" || Number.isNaN(value)) { |
| throw new Error("期望数字类型"); |
| } |
| } |
| } |
|
|
| function run(source, outputCallback = console.log) { |
| const lexer = new Lexer(source); |
| const tokens = lexer.scanTokens(); |
| const parser = new Parser(tokens); |
| const program = parser.parseProgram(); |
| const interpreter = new Interpreter(); |
| interpreter.execute(program, outputCallback); |
| } |
|
|
| if (typeof module !== "undefined" && module.exports) { |
| module.exports = { run }; |
| } else { |
| |
| window.MiniLang = { run }; |
| } |
|
|