diff --git a/.vscodeignore b/.vscodeignore new file mode 100644 index 0000000..74f1da1 --- /dev/null +++ b/.vscodeignore @@ -0,0 +1,5 @@ +.vscode/** +.vscode-test/** +docs/** +.gitignore +vsc-extension-quickstart.md \ No newline at end of file diff --git a/browser.js b/browser.js index 8df49a0..1937c0a 100644 --- a/browser.js +++ b/browser.js @@ -33,7 +33,7 @@ exampleProgram.onchange = e => { } let defaultProgram = 0 if (window.location.hash) { - defaultProgram = +(window.location.hash.replace(/[^\w\s]/gi, '').trim()) + defaultProgram = +window.location.hash.replace(/[^\w\s]/gi, '').trim() } code.value = examplePrograms[defaultProgram].program diff --git a/cli.js b/cli.js index 985b425..f3ce3f0 100755 --- a/cli.js +++ b/cli.js @@ -10,6 +10,11 @@ let options = { prompt: '>' } +const printReturnValue = lastLine => + JSON.stringify( + lastLine && typeof lastLine.toString === 'function' ? lastLine.toString() : lastLine + ) + const runPrompt = () => { const prompt = options.prompt + ' ' process.stdout.write(prompt) @@ -27,8 +32,8 @@ const runPrompt = () => { let code = line if (!line.endsWith(';') && !line.endsWith('}')) code += ';' // TODO: Support multi-line block statements - const lastLine = run(code, env, options.debug) - console.log(JSON.stringify(lastLine)) + const lastLine = run(code, env, console.log, options.debug) + console.log(printReturnValue(lastLine)) process.stdout.write(prompt) }) } @@ -36,7 +41,7 @@ const runPrompt = () => { const runFile = filename => { try { const file = fs.readFileSync(filename, 'utf8') - run(file, undefined, options.debug) + run(file, undefined, undefined, options.debug) } catch (e) { console.error(`YALI could not read the file ${filename}`) console.error(e) diff --git a/examples/classExample.lox b/examples/classExample.lox new file mode 100644 index 0000000..a663737 --- /dev/null +++ b/examples/classExample.lox @@ -0,0 +1,24 @@ +class Lox { + breakfast() { + print "Toast"; + } +} + +fun hello () { + print "Would you like some breakfast?"; +} + +print Lox; + +print hello; +var loxInstance = Lox(); + +loxInstance.category = "Breakfast"; +print loxInstance.category; + +print Lox(); + +// class Bagel {} +// var bagel = Bagel(); +// print bagel; // Prints "Bagel instance". +// print bagel.lox; \ No newline at end of file diff --git a/interpreter.js b/interpreter.js index adafd0c..ccf0ece 100644 --- a/interpreter.js +++ b/interpreter.js @@ -5,6 +5,9 @@ const { Call, Literal, Logical, + Class, + Get, + Set, Var, Grouping, Return, @@ -53,6 +56,51 @@ class LoxCallable { } return null } + + toString() { + return `<${this.declaration.name.lexeme}()>` + } +} + +class LoxClass extends LoxCallable { + constructor(name) { + super() + this.name = name + } + + call() { + return new LoxInstance(this) + } + + toString() { + return `<${this.name}>` + } +} + +class LoxInstance { + constructor(klass) { + this.klass = klass + this.fields = new Map() + } + + get (token) { + const name = token.lexeme + if (this.fields.has(name)) { + return this.fields.get(name) + } + + throw runtimeError(`Undefined property ${name}`, token) + // return null + } + + set (token, value) { + const name = token.lexeme + this.fields.set(name, value) + } + + toString () { + return `<+${this.klass.name}>` + } } class Interpreter { @@ -75,6 +123,9 @@ class Interpreter { if (expr instanceof Block) return this.visitBlock(expr) else if (expr instanceof LoxFunction) return this.visitFunction(expr) else if (expr instanceof Assignment) return this.visitAssignment(expr) + else if (expr instanceof Class) return this.visitClass(expr) + else if (expr instanceof Get) return this.visitGet(expr) + else if (expr instanceof Set) return this.visitSet(expr) else if (expr instanceof Logical) return this.visitLogical(expr) else if (expr instanceof Call) return this.visitCall(expr) else if (expr instanceof While) return this.visitWhile(expr) @@ -146,6 +197,35 @@ class Interpreter { return null } + visitClass(stmt) { + // We set the name before initializing it so classes can self-reference + this.environment.set(stmt.name, null) + const klass = new LoxClass(stmt.name.lexeme) + // console.log(klass) + this.environment.assign(stmt.name, klass) + return null + } + + visitGet(expr) { + const object = this.evaluate(expr.object) + if (object instanceof LoxInstance) { + return object.get(expr.name) + } + + throw runtimeError('Only instances have properties', expr.name) + } + + visitSet(expr) { + const object = this.evaluate(expr.object) + if (!(object instanceof LoxInstance)) { + throw runtimeError('Only instances have fields', expr.name) + } + + var val = this.evaluate(expr.value) + + return object.set(expr.name, val) + } + visitBlock(expr) { this.interpretBlock(expr.statements, new Environment(this.environment)) return null diff --git a/parser.js b/parser.js index d77e629..9206d13 100644 --- a/parser.js +++ b/parser.js @@ -6,6 +6,9 @@ const { Call, Literal, While, + Class, + Get, + Set, Grouping, Return, LoxFunction, @@ -21,6 +24,7 @@ const { parseError: ParseError } = require('./errors') const token = tokenizer.tokenEnum const FUNCTION_TYPE = 'function' +const METHOD_TYPE = 'method' class Parser { constructor(tokens) { @@ -43,15 +47,29 @@ class Parser { declaration() { if (this.match(token.FUN)) return this.fun(FUNCTION_TYPE) + if (this.match(token.CLASS)) return this.classDeclaration() if (this.match(token.VAR)) return this.varDeclaration() return this.statement() } + classDeclaration() { + const name = this.consume(token.IDENTIFIER, `Expected class name`) + this.consume(token.LEFT_BRACE, 'expected "{" before class body') + + let methods = [] + while (!this.check(token.RIGHT_BRACE)) { + methods.push(this.fun(METHOD_TYPE)) + } + + this.consume(token.RIGHT_BRACE, 'expected "}" after class body') + return new Class(name, methods) + } + fun(type) { const name = this.consume(token.IDENTIFIER, `Expected ${type} name`) - const params = [] + let params = [] this.consume(token.LEFT_PAREN, `Expected paren after ${type} name`) if (!this.check(token.RIGHT_PAREN)) { do { @@ -104,6 +122,8 @@ class Parser { 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) } @@ -243,6 +263,9 @@ class Parser { while (true) { if (this.match(token.LEFT_PAREN)) { expr = this.finishCall(expr) + } else if (this.match(token.DOT)) { + const name = this.consume(token.IDENTIFIER, 'Expected property name after "."') + expr = new Get(expr, name) } else { break } @@ -275,6 +298,8 @@ class Parser { return new Grouping(expr) } + console.log('primary') + throw ParseError('Expected Expression', this.peek()) } diff --git a/types.js b/types.js index 4c06e14..91e4cca 100644 --- a/types.js +++ b/types.js @@ -27,6 +27,21 @@ class Var { } } +class Get { + constructor(object, name) { + this.object = object + this.name = name + } +} + +class Set { + constructor(object, name, value) { + this.object = object + this.name = name + this.value = value + } +} + class Grouping { constructor(expression) { this.expression = expression @@ -73,11 +88,13 @@ class While { class Call { constructor(callee, paren, args) { - ;(this.callee = callee), (this.paren = paren) + this.callee = callee + this.paren = paren this.arguments = args } } +// Runtime Classes class Callable { constructor(name, func) { this.lexeme = name @@ -96,6 +113,13 @@ class Return { } } +class Class { + constructor(name, methods) { + this.name = name + this.methods = methods + } +} + class LoxFunction { constructor(name, params, bodyStatements) { this.name = name @@ -112,6 +136,9 @@ module.exports = { Call, Callable, While, + Class, + Get, + Set, Literal, Return, Logical,