From 53e237e796a7e6f2677606576fc581bc3b06e2a3 Mon Sep 17 00:00:00 2001 From: Daniel Berezin Date: Mon, 10 Jun 2019 23:11:01 -0700 Subject: [PATCH] Started python transpiler, cleaned up parser code. Added LL example --- .vscode/settings.json | 3 + browser.js | 5 +- examples/kitchenSink.lox | 9 ++- lox2python.js | 70 +++++++++++++++++++++ parser.js | 90 +++++++++++++-------------- transpilers/python.js | 131 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 259 insertions(+), 49 deletions(-) create mode 100644 .vscode/settings.json create mode 100755 lox2python.js create mode 100644 transpilers/python.js diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..20d15cb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.linting.pylintEnabled": false +} \ No newline at end of file diff --git a/browser.js b/browser.js index 47be46c..39117e5 100644 --- a/browser.js +++ b/browser.js @@ -12,6 +12,7 @@ const exampleProgramSource = [ readFileSync(__dirname + '/examples/interactiveFibonacci.lox', 'utf-8'), readFileSync(__dirname + '/examples/closureLinkedList.lox', 'utf-8'), readFileSync(__dirname + '/examples/kitchenSink.lox', 'utf-8'), + readFileSync(__dirname + '/examples/linkedList.lox', 'utf-8'), readFileSync(__dirname + '/examples/classExample.lox', 'utf-8') ] const examplePrograms = exampleProgramSource.map(program => { @@ -94,7 +95,9 @@ button.onclick = () => { const formatButton = document.getElementById('format') formatButton.onclick = () => { try { - code.value = parse(code.value) + const ast = parse(code.value) + global.ast = ast + code.value = ast .map(stmt => printLoxAST(stmt)) .join('\n') handleError(null) diff --git a/examples/kitchenSink.lox b/examples/kitchenSink.lox index a579ca5..45af56b 100644 --- a/examples/kitchenSink.lox +++ b/examples/kitchenSink.lox @@ -7,8 +7,11 @@ fun hello (arg1, arg2) { var maxAge = 18; var age; age = maxAge + 3; + { + print 23; + } if (age < 21) { - alert("Too young"); + print "Too young"; while(age < 21) { age = age + 1; } @@ -23,6 +26,6 @@ fun hello (arg1, arg2) { print "hello source: "; -print printFunctionBody(hello); +// print printFunctionBody(hello); -// hello("boss", "man"); \ No newline at end of file +hello("boss", "man"); \ No newline at end of file diff --git a/lox2python.js b/lox2python.js new file mode 100755 index 0000000..e8f4ac4 --- /dev/null +++ b/lox2python.js @@ -0,0 +1,70 @@ +#! /usr/bin/env node +const fs = require('fs') +const chalk = require('chalk') +const { parse } = require('./index') +const { loxToPython2 } = require('./transpilers/python') +const { formatLoxError } = require('./errors') + +let options = {} + +const printErrorMessage = (e, code) => { + const { oneLiner, preErrorSection, errorSection, postErrorSection } = formatLoxError(e, code) + console.error(oneLiner) + if (errorSection) { + console.error(preErrorSection + chalk.bgRed(errorSection) + postErrorSection) + } +} + +const fmtFile = (filename, outputfile = 'a.py') => { + try { + const file = fs.readFileSync(filename, 'utf8') + try { + const newText = parse(file) + .map(stmt => loxToPython2(stmt, 0, options)) + .join('\n') + if (!options.silent) console.log(newText) + if (options.write) fs.writeFileSync(outputfile, newText) + } catch (e) { + printErrorMessage(e, file) + } + } catch (e) { + console.error(`YALI could not read the file ${filename}`) + console.error(e) + } +} + +const optionRegex = /--(\w+)(?:=(.+))?/ +const processOptions = args => + args + .map(arg => { + const match = optionRegex.exec(arg) + if (match) { + const [, option, value] = match + if (!value) { + options[option] = !options[option] + } else { + options[option] = value + } + } else { + return arg + } + }) + .filter(Boolean) + +const main = argv => { + process.title = 'lox2python' + const args = processOptions(argv.slice(2)) + if (options.help) { + console.log('Usage: lox2python [script] --out="a.py"') + return 0 + } + + if (args.length === 1) { + fmtFile(args[0]) + } else { + console.error('Usage: lox2python [script]') + return 64 + } +} + +main(process.argv) diff --git a/parser.js b/parser.js index ca38f29..6d7d520 100644 --- a/parser.js +++ b/parser.js @@ -34,10 +34,6 @@ class Parser { this.current = 0 } - expression() { - return this.assignment() - } - parse() { let statements = [] while (!this.isAtEnd) { @@ -106,41 +102,6 @@ class Parser { return this.expressionStatement() } - returnStatement() { - const prev = this.previous() - let value = null - if (!this.check(token.SEMICOLON)) { - value = this.expression() - } - this.consume(token.SEMICOLON, 'Expected ";" after return value') - return new Return(prev, value) - } - - assignment() { - const expr = this.or() - if (this.match(token.EQUAL)) { - const equalToken = this.previous() - const value = this.assignment() - if (expr instanceof Var) { - const nameToken = expr.name - return new Assignment(nameToken, value) - } else if (expr instanceof Get) { - return new Set(expr.object, expr.name, value) - } - throw ParseError('Expected Expression', equalToken) - } - - return expr - } - - or() { - return this.matchBinary('and', Logical, token.OR) - } - - and() { - return this.matchBinary('equality', Logical, token.AND) - } - forStatement() { this.consume(token.LEFT_PAREN, 'Expected "(" after "for"') @@ -206,16 +167,56 @@ class Parser { return statements } - expressionStatement() { + printStatement() { const val = this.expression() this.consume(token.SEMICOLON, 'Expect ; after value.') - return new ExpressionStatement(val) + return new PrintStatement(val) } - printStatement() { + returnStatement() { + const prev = this.previous() + let value = null + if (!this.check(token.SEMICOLON)) { + value = this.expression() + } + this.consume(token.SEMICOLON, 'Expected ";" after return value') + return new Return(prev, value) + } + + + expressionStatement() { const val = this.expression() this.consume(token.SEMICOLON, 'Expect ; after value.') - return new PrintStatement(val) + return new ExpressionStatement(val) + } + + expression() { + return this.assignment() + } + + assignment() { + const expr = this.or() + if (this.match(token.EQUAL)) { + const equalToken = this.previous() + const value = this.assignment() + if (expr instanceof Var) { + const nameToken = expr.name + return new Assignment(nameToken, value) + } else if (expr instanceof Get) { + return new Set(expr.object, expr.name, value) + } + throw ParseError('Expected Expression', equalToken) + } + + return expr + } + + or() { + return this.matchBinary('and', Logical, token.OR) + } + + and() { + return this.matchBinary('equality', Logical, token.AND) } matchBinary(method, Class, ...operators) { @@ -262,8 +263,7 @@ class Parser { call() { let expr = this.primary() - while (true) { - //eslint-disable-line + while (true) { //eslint-disable-line if (this.match(token.LEFT_PAREN)) { expr = this.finishCall(expr) } else if (this.match(token.DOT)) { diff --git a/transpilers/python.js b/transpilers/python.js new file mode 100644 index 0000000..b295272 --- /dev/null +++ b/transpilers/python.js @@ -0,0 +1,131 @@ +const { + Binary, + Unary, + Var, + Call, + Literal, + While, + // @TODO: Support Classes + // Class, + // Get, + // Set, + Grouping, + Return, + LoxFunction, + PrintStatement, + ExpressionStatement, + VarStatement, + Assignment, + Logical, + Block, + Condition +} = require('../types') + +// const condChar = (condition, replacer = ' ') => condition ? replacer : '' + +const ASTNodeMap = new Map() + +// Declarations +ASTNodeMap.set(ExpressionStatement, (node, scope, options, initialIndent) => { + // console.log(initialIndent) + return loxToPython2(node.expression, 0, options, initialIndent) +}) + +ASTNodeMap.set(PrintStatement, node => 'print ' + loxToPython2(node.expression)) + +ASTNodeMap.set(Return, node => 'return ' + loxToPython2(node.value)) + +ASTNodeMap.set(VarStatement, (node) => { + const name = node.name.lexeme + const initializer = node.initializer ? loxToPython2(node.initializer) : null + // Python doesn't have plain declarations... + if (!node.initializer) return '' + return `${name}` + (initializer ? ` = ${initializer}` : '') +}) + +ASTNodeMap.set(Condition, ({ condition, thenBranch, elseBranch }, scope, options) => { + const cond = loxToPython2(condition) + const conditionSection = `if ${cond}:\n` + const thenSection = loxToPython2(thenBranch, scope + 1, options, false) + const elseSection = elseBranch && loxToPython2(elseBranch, scope + 1, options, false) + return conditionSection + thenSection + (elseSection ? `\n${options.indent.repeat(scope)}else:\n${elseSection}` : '') +}) + +ASTNodeMap.set(LoxFunction, ({ bodyStatements: body, name: { lexeme: name }, params}, scope, options) => { + const parameters = params.map(token => token.lexeme) + const head = `def ${name}(${parameters.join(', ')}):` + const fnBody = body.map(stmt => loxToPython2(stmt, scope + 1, options)) + // console.log(fnBody) + return [head, ...fnBody].join('\n') +}) + +ASTNodeMap.set(While, ({ body, condition }, scope, options) => { + const cond = loxToPython2(condition) + const conditionSection = `while ${cond}:\n` + const bodySection = loxToPython2(body, scope + 1, options, false) + return conditionSection + bodySection +}) + +ASTNodeMap.set(Block, ({ statements }, scope, options, initialIndent) => statements.map(stmt => loxToPython2(stmt, scope, options, initialIndent)).join('\n')) + +// Expressions +ASTNodeMap.set(Var, ({ name: { lexeme } }) => lexeme) + +ASTNodeMap.set(Grouping, ({ expression }) => '(' + loxToPython2(expression) + ')') + + +const handleBinary = node => { + const left = loxToPython2(node.left) + const operator = node.operator.lexeme + const right = loxToPython2(node.right) + return [left, operator, right].join(' ') +} + +ASTNodeMap.set(Binary, handleBinary) + +ASTNodeMap.set(Logical, handleBinary) + +ASTNodeMap.set(Unary, node => { + const operator = node.operator.lexeme + const right = loxToPython2(node.right) + return operator + right +}) + + +ASTNodeMap.set(Call, node => { + const args = node.arguments.map(args => loxToPython2(args)).join(', ') + const callee = loxToPython2(node.callee) + return `${callee}(${args})` +}) + +ASTNodeMap.set(Assignment, node => { + const name = node.name.lexeme + const value = loxToPython2(node.value) + return name + ' = ' + value +}) + +ASTNodeMap.set(Literal, ({ value }) => { + if (typeof value === 'string') { + return `"${value}"` + } else if (value === null) { + return 'nil' + } else { + return value + } +}) + +const loxToPython2 = (node, scope = 0, optionsOverride = {}, initialIndent = true) => { + const options = Object.assign({}, { + indent: ' ', + }, optionsOverride) + + if (ASTNodeMap.has(node.constructor)) { + const indentation = (initialIndent ? options.indent.repeat(scope) : '') + return indentation + ASTNodeMap.get(node.constructor)(node, scope, options) + } + throw new Error(`Don't support that context yet`, node.constructor) +} + +module.exports = { + loxToPython2 +} \ No newline at end of file