Repository: wolfgang42/rockstar-js Branch: master Commit: 29a14768db98 Files: 15 Total size: 19.4 KB Directory structure: gitextract_6u9na67n/ ├── .gitignore ├── LICENSE ├── README.md ├── examples/ │ ├── fizzbuzz-idiomatic.out.txt │ ├── fizzbuzz-idiomatic.rock │ ├── fizzbuzz-minimalist.out.txt │ ├── fizzbuzz-minimalist.rock │ ├── listen.rock │ ├── treesort.out.txt │ └── treesort.rock ├── package.json ├── rockstar ├── src/ │ ├── rockstar-parser.peg │ └── rockstar.js └── test/ └── integration.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ node_modules/ examples/*.js rockstar-parser.js package-lock.json ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 Wolfgang Faust Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ This is a work-in-progress implementation of the [Rockstar](https://github.com/dylanbeattie/rockstar) language. It transpiles Rockstar code to JavaScript. Usage: ``` yarn install ./rockstar program.rock node program.js ``` **Note:** Due to the extremely fast speed of updates to Rockstar, this implementation may not always match the current spec. [wolfgang42/rockstar](https://github.com/wolfgang42/rockstar) has the version of this spec targeted by this implementation. View differences between the two here: [wolfgang42/master...dylanbeattie/master](https://github.com/wolfgang42/rockstar/compare/master...dylanbeattie:master) Also, since this is a WIP not all of Rockstar works properly yet. See the [Spec Complete milestone](https://github.com/wolfgang42/rockstar-js/milestone/1) for the list of unimplemented features. Contributions welcome! # Design Transpilation is broken up into three stages: parsing, block grouping, and code generation. ## Parsing First, the text of the program is parsed into statements and expressions. The resulting tokens are objects which have a `t` property containing the type of the token, plus other properties (generally a single mnemonic letter) with additional information about the token. Parsing is currently implemented using [PEG.js](https://pegjs.org/), a JS Parser Generator. I'm not entirely convinced that this was the best choice but it seems to work OK so far. For example, the poetic string literal: ``` Billy says hello world! ``` is parsed by this expression: ``` PoeticString = v:Variable _ 'says' ' ' t:$[^\n]* { return {t: 'Set', v: v, e: {t: 'Literal', v: t}} } ``` into this token: ```javascript {t: "Set", v: {t: "Variable", n: "Billy"}, e: {t: "Literal", v: "hello world!"}} ``` (Notice that this token contains two other tokens, `v` and `e`.) **Note for developers:** After changing `rockstar-parser.peg`, make sure you run `yarn build` to regenerate the parser code. ## Block grouping The parsing step returns a series of statements, but does not know about blocks. This step (implemented by the `groupBlocks` function) finds statements which begin blocks (`If`, `While`, and so on) and groups the statements together inside a `Block` token, removing `BlankLine` tokens. ## Code generation This stage takes the tokens and emits JavaScript code. Each token has a function in the `generators` object which takes a token and returns a string. Many of these operate recursively, calling `expr()` on a token to generate code to be included. For example, the `Set` token generator: ```javascript Set: s => `${expr(s.v)}=${expr(s.e)};`, ``` takes a Token like this: ```javascript {t: "Set", v: {t: "Variable", n: "Billy"}, e: {t: "Literal", v: "hello world!"}} ``` and returns this code: ```javascript Billy="hello world!"; ``` ================================================ FILE: examples/fizzbuzz-idiomatic.out.txt ================================================ 1 2 Fizz! 4 Buzz! Fizz! 7 8 Fizz! Buzz! 11 Fizz! 13 14 FizzBuzz! 16 17 Fizz! 19 Buzz! Fizz! 22 23 Fizz! Buzz! 26 Fizz! 28 29 FizzBuzz! 31 32 Fizz! 34 Buzz! Fizz! 37 38 Fizz! Buzz! 41 Fizz! 43 44 FizzBuzz! 46 47 Fizz! 49 Buzz! Fizz! 52 53 Fizz! Buzz! 56 Fizz! 58 59 FizzBuzz! 61 62 Fizz! 64 Buzz! Fizz! 67 68 Fizz! Buzz! 71 Fizz! 73 74 FizzBuzz! 76 77 Fizz! 79 Buzz! Fizz! 82 83 Fizz! Buzz! 86 Fizz! 88 89 FizzBuzz! 91 92 Fizz! 94 Buzz! Fizz! 97 98 Fizz! Buzz! ================================================ FILE: examples/fizzbuzz-idiomatic.rock ================================================ Midnight takes your heart and your soul While your heart is as high as your soul Put your heart without your soul into your heart Give back your heart Desire is a lovestruck ladykiller My world is nothing Fire is ice Hate is water Until my world is Desire, Build my world up If Midnight taking my world, Fire is nothing and Midnight taking my world, Hate is nothing Shout "FizzBuzz!" Take it to the top If Midnight taking my world, Fire is nothing Shout "Fizz!" Take it to the top If Midnight taking my world, Hate is nothing Say "Buzz!" Take it to the top Whisper my world ================================================ FILE: examples/fizzbuzz-minimalist.out.txt ================================================ 1 2 Fizz! 4 Buzz! Fizz! 7 8 Fizz! Buzz! 11 Fizz! 13 14 FizzBuzz! 16 17 Fizz! 19 Buzz! Fizz! 22 23 Fizz! Buzz! 26 Fizz! 28 29 FizzBuzz! 31 32 Fizz! 34 Buzz! Fizz! 37 38 Fizz! Buzz! 41 Fizz! 43 44 FizzBuzz! 46 47 Fizz! 49 Buzz! Fizz! 52 53 Fizz! Buzz! 56 Fizz! 58 59 FizzBuzz! 61 62 Fizz! 64 Buzz! Fizz! 67 68 Fizz! Buzz! 71 Fizz! 73 74 FizzBuzz! 76 77 Fizz! 79 Buzz! Fizz! 82 83 Fizz! Buzz! 86 Fizz! 88 89 FizzBuzz! 91 92 Fizz! 94 Buzz! Fizz! 97 98 Fizz! Buzz! ================================================ FILE: examples/fizzbuzz-minimalist.rock ================================================ Modulus takes Number and Divisor While Number is as high as Divisor Put Number minus Divisor into Number (blank line ending While block) Give back Number (blank line ending function declaration) Limit is 100 Counter is 0 Fizz is 3 Buzz is 5 Until Counter is Limit Build Counter up If Modulus taking Counter, Fizz is 0 and Modulus taking Counter, Buzz is 0 Say "FizzBuzz!" Continue (blank line ending 'If' Block) If Modulus taking Counter and Fizz is 0 Say "Fizz!" Continue (blank line ending 'If' Block) If Modulus taking Counter and Buzz is 0 Say "Buzz!" Continue (blank line ending 'If' Block) Say Counter (EOL ending Until block) ================================================ FILE: examples/listen.rock ================================================ Listen to my voice Build my voice up Scream my voice ================================================ FILE: examples/treesort.out.txt ================================================ 822 4805 9117 10097 10402 13586 20636 24037 24805 32533 34673 37542 39292 54876 64894 74296 74945 76520 80959 91665 ================================================ FILE: examples/treesort.rock ================================================ Nodeoperationread is 0 Nodeoperationwrite is 1 Nodevalueignored is 0 Nodevariablevalue is 0 Nodevariableleft is 1 Nodevariableright is 2 Makenode takes Value, Left, Right Node takes Operation, Variable, Newvalue If Operation is Nodeoperationread If Variable is Nodevariablevalue Give back Value (end if) If Variable is Nodevariableleft Give back Left (end if) If Variable is Nodevariableright Give back Right (end if) (end if) If Operation is Nodeoperationwrite If Variable is Nodevariablevalue Put Newvalue into Value (end if) If Variable is Nodevariableleft Put Newvalue into Left (end if) If Variable is Nodevariableright Put Newvalue into Right (end if) (end if) (end function) Give back Node (end function) Insertnode takes Node, Newvalue Put Makenode taking Newvalue, nothing, nothing into Newnode If Node is nothing Give back Newnode (end if) Put Node into Originalroot Put nothing into Parentnode Put Nodevariableright into Direction While Node is not nothing Put Node taking Nodeoperationread, Nodevariablevalue, Nodevalueignored into Value Put Nodevariableright into Direction If Newvalue is less than Value Put Nodevariableleft into Direction (end if) Put Node into Parentnode Put Node taking Nodeoperationread, Direction, Nodevalueignored into Node (end while) Put Parentnode taking Nodeoperationwrite, Direction, Newnode into Unusedreturnvalue Give back Originalroot (end function) Inorder takes Node If Node is nothing Give back nothing (end if) Put Node taking Nodeoperationread, Nodevariableleft, Nodevalueignored into Left Put Inorder taking Left into Unusedreturnvalue Put Node taking Nodeoperationread, Nodevariablevalue, Nodevalueignored into Value Say Value Put Node taking Nodeoperationread, Nodevariableright, Nodevalueignored into Right Put Inorder taking Right into Unusedreturnvalue Give back nothing (end function) Main takes Root Put Insertnode taking Root, 10097 into Root Put Insertnode taking Root, 32533 into Root Put Insertnode taking Root, 76520 into Root Put Insertnode taking Root, 13586 into Root Put Insertnode taking Root, 34673 into Root Put Insertnode taking Root, 54876 into Root Put Insertnode taking Root, 80959 into Root Put Insertnode taking Root, 9117 into Root Put Insertnode taking Root, 39292 into Root Put Insertnode taking Root, 74945 into Root Put Insertnode taking Root, 37542 into Root Put Insertnode taking Root, 4805 into Root Put Insertnode taking Root, 64894 into Root Put Insertnode taking Root, 74296 into Root Put Insertnode taking Root, 24805 into Root Put Insertnode taking Root, 24037 into Root Put Insertnode taking Root, 20636 into Root Put Insertnode taking Root, 10402 into Root Put Insertnode taking Root, 822 into Root Put Insertnode taking Root, 91665 into Root Put Inorder taking Root into Unusedreturnvalue (end function) Put Main taking nothing into Unusedreturnvalue ================================================ FILE: package.json ================================================ { "name": "rockstar-js", "version": "0.0.1", "description": "JavaScript transpiler for the esoteric language 'Rockstar'", "homepage": "https://github.com/wolfgang42/rockstar-js", "bugs": "https://github.com/wolfgang42/rockstar-js/issues", "repository": "github:wolfgang42/rockstar-js", "license": "MIT", "author": "Wolfgang Faust", "main": "src/rockstar.js", "bin": "./rockstar", "directories": { "lib": "src/", "example": "examples/" }, "dependencies": {}, "devDependencies": { "mocha": "^5.2.0", "pegjs": "^0.10.0" }, "scripts": { "build": "pegjs src/rockstar-parser.peg", "test": "mocha", "prepublish": "npm run build" } } ================================================ FILE: rockstar ================================================ #!/usr/bin/env node const rockstar = require('./src/rockstar') const fs = require('fs') function promisify(fn, ...args) { return new Promise((resolve, reject) => fn(...args, (err, result) => { if (err) { reject(err) } else { resolve(result) } })) } const filename = process.argv[2] promisify(fs.readFile, filename, 'utf-8') .then(rockstar.compile) .then(code => promisify(fs.writeFile, filename.replace('.rock', '.js'), code)) .then(null, e => { console.error(e) process.exit(1) }) ================================================ FILE: src/rockstar-parser.peg ================================================ Program = Statement * Statement = _? s:(FunctionDeclaration/Operation/BlankLine) ','? _* '\n' {return s} Operation = Loop / If / ArithmeticStatement / GiveBack / Set / Put / Listen / Say / Continue / Break / PoeticString _ = ((' ' / '\t')+) / Comment Comment = '(' [^)]* ')' Variable = (n:(CommonVariable / ProperVariable) {return {t: 'Variable', n}}) / Pronoun {return {t: 'Pronoun'}} CommonVariable = p:('the'i/'my'i/'your'i) _ v:$([a-z]+) {return p+v} ProperWord = $([A-Z][A-Za-z]+) ProperVariable = $(ProperWord _ ProperVariable) / $ProperWord Pronoun = ( 'it'i /'he'i/'she'i/'him'i/'her'i /'them'i/'they'i /'ze'i/'hir'i/'zie'i/'zir'i/'xe'i/'xem'i/'ve'i/'ver'i ) TypeLiteral = v:TypeLiteralValue { return {t: 'Literal', v} } TypeLiteralValue = ('nothing'/'nobody'/'nowhere'/'empty'/'gone') {return 0} // TODO null? / ('true'/'right'/'yes'/'ok') {return true} / ('false'/'wrong'/'no'/'lies') {return false} / 'mysterious' { return undefined} String = '"' v:$[^"]+ '"' { return {t: 'Literal', v}} Number = n:$([0-9]+ ('.' [0-9]+)?) { return {t: 'Literal', v: parseFloat(n)} } ArithmeticStatement = BuildUp / KnockDown BuildUp = 'Build' _ v:Variable _ 'up' { return {t: 'Rement', v: v, o: '++'} } KnockDown = 'Knock' _ v:Variable _ 'down' { return {t: 'Rement', v: v, o: '--'} } PoeticString = v:Variable _ 'says' ' ' t:$[^\n]* { return {t: 'Set', v: v, e: {t: 'Literal', v: t}} } PoeticNumber = n:PoeticDigits d:PoeticDecimal? {return {t: 'Literal', v: parseFloat(d?n+'.'+d:n)}} PoeticDecimal = '.' _ d:PoeticDigits {return d} PoeticDigits = l:PoeticDigit ( _ / [\',;:?!] )+ r:PoeticDigits { return l+r } / d: PoeticDigit { return d } PoeticDigit = t:[A-Za-z]+ {return (t.length%10).toString()} ArithmeticExpression = l:SimpleExpression _ o:ArithmeticOperator _ r:SimpleExpression { return {t: 'Arithmetic', l, o, r} } ArithmeticOperator = ('minus'/'without') {return '-'} / ('plus'/'with') {return '+'} / ('times'/'of') {return '*'} / ('over') {return '/'} Comparison = l:SimpleExpression _ b:BoolCheck c:Comparator? r:SimpleExpression { return {t: 'Comparison', l, r, b, c} } BoolCheck = (('is'_'not'_)/"ain't"_) { return false } / 'is'_ { return true } Comparator = ('higher'/'greater'/'bigger'/'stronger')_'than'_ { return 'gt' } / ('lower'/'less'/'smaller'/'weaker')_'than'_ { return 'lt' } / 'as'_('high'/'great'/'big'/'strong')_'as'_ {return 'ge'} / 'as'_('low'/'little'/'small'/'weak')_'as'_ {return 'le'} Listen = 'Listen' _ 'to' _ v:Variable {return {t:'Listen', v}} Say = ('Say'/'Shout'/'Whisper'/'Scream') _ e:Expression {return {t:'Say', e}} If = 'If' _ e:Expression { return {t: 'If', e} } Else = 'Else' {return {t: 'Else'}} Loop = c:('While'/'Until') _ e:Expression { return {t: 'Loop', c, e} } Continue = ('Continue' / ('Take'_'it'_'to'_'the'_'top')) {return {t: 'Continue'}} Break = ('Break' / ('Break'_'it'_'down')) {return {t: 'Break'}} FunctionDeclaration = n:Variable _ 'takes' _ a:FunctionDeclarationArguments { return {t: 'FunctionDeclaration', n, a: a.map(a => a.n)} } FunctionDeclarationArguments = a:Variable (_'and'_ / _?','_?) b:FunctionDeclarationArguments { return [a].concat(b) } / a:Variable { return [a] } GiveBack = 'Give back' _ e:Expression { return {t: 'GiveBack', e} } BlankLine = '' {return {t: 'BlankLine'}} FunctionCall = f:Variable _ 'taking' _ a:FunctionCallArguments { return {t: 'FunctionCall', f, a} } // TODO 'and' is overloaded, so can't use full Expression syntax FunctionCallArguments = a:SimpleExpression (_'and'_ / _?','_?) b:FunctionCallArguments { return [a].concat(b) } / a:SimpleExpression { return [a] } /////////////////////// // TODO Everything below here is never explicitly defined in the spec Expression = ArithmeticExpression / BooleanOperation SimpleExpression = FunctionCall / TypeLiteral / Variable / Number / String / PoeticNumber BooleanOperation = ( l:(Comparison / SimpleExpression) _ b:('and'/'or') _ r:Expression { return {t: 'BooleanOperation', l, b, r} } ) / l: (Comparison / SimpleExpression) { return l } Set = v:Variable _ ('is'/'was'/'were') _ e:Expression { return {t: 'Set', v: v, e} } Put = 'Put' _ e:Expression _ 'into' _ v:Variable { return {t: 'Set', v: v, e} } ================================================ FILE: src/rockstar.js ================================================ const parser = require('./rockstar-parser') let it const generators = { Block: b => `{${b.s.map(expr).join('')}}`, FunctionDeclaration: f => `function ${expr(f.n)}(${f.a.map(varname).join(',')})`, FunctionCall: f => `${expr(f.f)}(${f.a.map(expr).join(',')})`, Loop: w => { let cond = expr(w.e) if (w.c === 'Until') cond = `!(${cond})` return `while(${cond})` }, Continue: _ => 'continue;', Break: _ => 'break;', If: i => `if(${expr(i.e)})`, Else: _ => 'else', Comparison: c => { let ret = expr(c.l) if (c.c) { const comp = { gt: '>', lt: '<', ge: '>=', le: '<=', } ret += comp[c.c] } else { if (c.b) { ret += '===' } else { ret += '!==' } } ret += expr(c.r) if (!c && !b) ret = `!(${ret})` return ret }, BooleanOperation: b => `${expr(b.l)}${b.b=='and'?'&&':'||'}${expr(b.r)}`, Variable: v => { it = v return varname(v.n) }, Pronoun: p => expr(it), Rement: r => `${expr(r.v)}${r.o};`, Arithmetic: a => `${expr(a.l)}${a.o}${expr(a.r)}`, Set: s => `${expr(s.v)}=${expr(s.e)};`, Literal: l => JSON.stringify(l.v), GiveBack: g => `return ${expr(g.e)};`, Say: s=>`console.log(${expr(s.e)});`, Listen: ({ v }) => `${varname(v.n)} = $readLineSync();` } const dependencies = { addReadLine: ` function $readLineSync() { const line = []; const buffer = Buffer.alloc(1); while (true) { const bytes = $fs.readSync(1, buffer, 0, 1, null); if (!bytes) break; if (buffer[0] === 10 || buffer[0] === 13) break; line.push(buffer[0]); } return Buffer.from(line).toString('utf-8'); } `, fs: `const $fs = require('fs');`, } function varname(v) { return v.replace(/ /g, '') } function expr(e) { if (!(e.t in generators)) { console.log(e) throw new Error('Unknown statement type: '+e.t) } return generators[e.t](e) } function _groupBlocks(statements) { let ret = [] let stmt while (stmt = statements.shift()) { if (stmt.t == 'BlankLine') return ret ret.push(stmt) if (stmt.t == 'If' || stmt.t == 'Else' || stmt.t == 'Loop' || stmt.t == 'FunctionDeclaration') { ret.push({ t: 'Block', s: _groupBlocks(statements), }) } } return ret } function groupBlocks(statements) { const ret = [] while (statements.length !== 0) { _groupBlocks(statements).forEach(s => ret.push(s)) } return ret } function computeDependencies(statements) { const deps = []; if (statements.some(s => s.t === 'Listen')) { deps.push('fs', 'addReadLine'); } // TODO: (eventually) remove dup `deps` return deps; } function generateDependencies(deps) { return deps.map(d => dependencies[d]); } function parse(programText) { // Parser requires newline before EOF, add it in case there wasn't one already. return parser.parse(programText+'\n') } function compile(programText) { const statements = parse(programText) const dependencies = generateDependencies(computeDependencies(statements)); const program = groupBlocks(statements) return [ ...dependencies, ...program.map(expr) ].join('') } module.exports = { varname, expr, groupBlocks, parse, compile, computeDependencies, generateDependencies, } ================================================ FILE: test/integration.js ================================================ const test_examples = [ 'fizzbuzz-idiomatic', 'fizzbuzz-minimalist', 'listen', 'treesort', ] const rockstar = require('../src/rockstar') const fs = require('fs') const child_process = require('child_process') function promisify(fn, ...args) { return new Promise((resolve, reject) => fn(...args, (...result) => resolve(result))) } async function promisify_one(fn, ...args) { const r = await promisify(fn, ...args) if (r.length !== 1) throw new Error('promisify_one called with returning '+r.length+' args') return r[0] } async function promisify_err(fn, ...args) { const r = await promisify(fn, ...args) if (r.length !== 2) throw new Error('promisify_one called with returning '+r.length+' args') if (r[0]) throw r[0] return r[1] } const readFile = filename => promisify_err(fs.readFile, filename, 'utf-8') const fileExists = filename => promisify_one(fs.exists, filename) function runJs(code) { return new Promise((resolve, reject) => { let output = '' const node = child_process.spawn('node') node.stdout.on('data', data => output += data) node.on('close', () => resolve(output)) node.stdin.end(code) }) } describe('Examples', () => { test_examples.forEach(example => it(example, async () => { example = `${__dirname}/../examples/${example}` const rsCode = await readFile(example+'.rock') const jsCode = rockstar.compile(rsCode) if (await fileExists(example+'.out.txt')) { const expectOutput = await readFile(example+'.out.txt') const realOutput = await runJs(jsCode) if (expectOutput != realOutput) throw new Error('Actual output does not match expected output') } })) })