Skip to content

Commit

Permalink
Use graph-stringify and separate out parser
Browse files Browse the repository at this point in the history
  • Loading branch information
rtoal committed Feb 24, 2024
1 parent f7af657 commit aee4b91
Show file tree
Hide file tree
Showing 10 changed files with 60 additions and 37 deletions.
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"mocha": "^10.2.0"
},
"dependencies": {
"graph-stringify": "^1.1.0",
"ohm-js": "^17.0.4"
}
}
12 changes: 3 additions & 9 deletions src/analyzer.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import fs from "fs"
import * as ohm from "ohm-js"
import * as core from "./core.js"

const astroGrammar = ohm.grammar(fs.readFileSync("src/astro.ohm"))

// Throw an error message that takes advantage of Ohm's messaging.
// If you supply an Ohm tree node as the second parameter, this will
// use Ohm's cool reporting mechanism.
function error(message, node) {
const prefix = `${node?.source.getLineAndColumnMessage() ?? ""}`
const prefix = `${node?.source.getLineAndColumnMessage()}`
throw new Error(`${prefix}${message}`)
}

Expand Down Expand Up @@ -53,14 +49,14 @@ class Context {
}
}

export default function analyze(sourceCode) {
export default function analyze(match) {
// Astro is so trivial that the only required contextual information is
// to keep track of the identifiers that have been declared.
const context = new Context()

// The compiler front end analyzes the source code and produces a graph of
// entities (defined in the core module) "rooted" at the Program entity.
const analyzer = astroGrammar.createSemantics().addOperation("rep", {
const analyzer = match.matcher.grammar.createSemantics().addOperation("rep", {
Program(statements) {
return core.program(statements.rep())
},
Expand Down Expand Up @@ -126,7 +122,5 @@ export default function analyze(sourceCode) {
for (const [name, entity] of Object.entries(core.standardLibrary)) {
context.add(name, entity)
}
const match = astroGrammar.match(sourceCode)
if (!match.succeeded()) error(match.message)
return analyzer(match).rep()
}
12 changes: 7 additions & 5 deletions src/astro.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#! /usr/bin/env node

import fs from "fs/promises"
import process from "process"
import fs from "node:fs/promises"
import stringify from "graph-stringify"
import compile from "./compiler.js"

const help = `Astro compiler
Expand All @@ -10,15 +10,17 @@ Syntax: node astro.js <filename> <outputType>
Prints to stdout according to <outputType>, which must be one of:
analyzed the analyzed representation
optimized the optimized analyzed representation
parsed a message that the program was matched ok by the grammar
analyzed the statically analyzed representation
optimized the optimized semantically analyzed representation
js the translation to JavaScript
`

async function compileFromFile(filename, outputType) {
try {
const buffer = await fs.readFile(filename)
console.dir(compile(buffer.toString(), outputType), { depth: null })
const compiled = compile(buffer.toString(), outputType)
console.log(stringify(compiled, "kind") || compiled)
} catch (e) {
console.error(`\u001b[31m${e}\u001b[39m`)
process.exitCode = 1
Expand Down
7 changes: 5 additions & 2 deletions src/compiler.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import parse from "./parser.js"
import analyze from "./analyzer.js"
import optimize from "./optimizer.js"
import generate from "./generator.js"

export default function compile(source, outputType) {
if (!["analyzed", "optimized", "js"].includes(outputType)) {
if (!["parsed", "analyzed", "optimized", "js"].includes(outputType)) {
throw new Error("Unknown output type")
}
const analyzed = analyze(source)
const match = parse(source)
if (outputType === "parsed") return "Syntax is ok"
const analyzed = analyze(match)
if (outputType === "analyzed") return analyzed
const optimized = optimize(analyzed)
if (outputType === "optimized") return optimized
Expand Down
16 changes: 16 additions & 0 deletions src/parser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// PARSER
//
// The parse() function uses Ohm to produce a match object for a given
// source code program, using the grammar in the bella.ohm.

import * as fs from "node:fs"
import * as ohm from "ohm-js"

const grammar = ohm.grammar(fs.readFileSync("src/astro.ohm"))

// Returns the Ohm match if successful, otherwise throws an error
export default function parse(sourceCode) {
const match = grammar.match(sourceCode)
if (!match.succeeded()) throw new Error(match.message)
return match
}
14 changes: 6 additions & 8 deletions test/analyzer.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import assert from "assert/strict"
import * as core from "../src/core.js"
import assert from "node:assert/strict"
import parse from "../src/parser.js"
import analyze from "../src/analyzer.js"
import * as core from "../src/core.js"

const semanticChecks = [
["variables can be printed", "x = 1; print(x);"],
Expand Down Expand Up @@ -43,18 +44,15 @@ const expected = core.program([
describe("The analyzer", () => {
for (const [scenario, source] of semanticChecks) {
it(`recognizes ${scenario}`, () => {
assert.ok(analyze(source))
assert.ok(analyze(parse(source)))
})
}
for (const [scenario, source, errorMessagePattern] of semanticErrors) {
it(`throws on ${scenario}`, () => {
assert.throws(() => analyze(source), errorMessagePattern)
assert.throws(() => analyze(parse(source)), errorMessagePattern)
})
}
it(`throws on syntax errors`, () => {
assert.throws(() => analyze("this Haz #@ SYntAx Errz))$"))
})
it(`produces the expected graph for the simple sample program`, () => {
assert.deepEqual(analyze(sample), expected)
assert.deepEqual(analyze(parse(sample)), expected)
})
})
5 changes: 3 additions & 2 deletions test/generator.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import assert from "assert/strict"
import assert from "node:assert/strict"
import parse from "../src/parser.js"
import analyze from "../src/analyzer.js"
import optimize from "../src/optimizer.js"
import generate from "../src/generator.js"
Expand All @@ -24,7 +25,7 @@ const expected = dedent`

describe("The code generator", () => {
it(`produces the expected output for the small program`, done => {
const actual = generate(optimize(analyze(sample)))
const actual = generate(optimize(analyze(parse(sample))))
assert.deepEqual(actual, expected)
done()
})
Expand Down
5 changes: 3 additions & 2 deletions test/optimizer.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import assert from "assert/strict"
import parse from "../src/parser.js"
import analyze from "../src/analyzer.js"
import optimize from "../src/optimizer.js"
import * as core from "../src/core.js"
Expand All @@ -13,7 +14,7 @@ const negate = x => core.unary("-", x)
const program = (...statements) => core.program(statements)

function expression(e) {
return analyze(`x=1; print(${e});`).statements[1].args[0]
return analyze(parse(`x=1; print(${e});`)).statements[1].args[0]
}

const tests = [
Expand Down Expand Up @@ -52,7 +53,7 @@ const tests = [
],
[
"removes x=x",
analyze("x=1; x=x; print(x);"),
analyze(parse("x=1; x=x; print(x);")),
program(core.assignment(x, 1), core.procedureCall(print, [x], true)),
],
]
Expand Down
14 changes: 5 additions & 9 deletions test/grammar.test.js → test/parser.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import assert from "assert/strict"
import fs from "fs"
import * as ohm from "ohm-js"
import assert from "node:assert/strict"
import parse from "../src/parser.js"

const syntaxChecks = [
["all numeric literal forms", "print(8 * 89.123);"],
Expand All @@ -25,18 +24,15 @@ const syntaxErrors = [
["an expression starting with a *", "x = * 71;", /Line 1, col 5/],
]

describe("The grammar", () => {
const grammar = ohm.grammar(fs.readFileSync("src/astro.ohm"))
describe("The parser", () => {
for (const [scenario, source] of syntaxChecks) {
it(`properly specifies ${scenario}`, () => {
assert(grammar.match(source).succeeded())
assert(parse(source).succeeded())
})
}
for (const [scenario, source, errorMessagePattern] of syntaxErrors) {
it(`does not permit ${scenario}`, () => {
const match = grammar.match(source)
assert(!match.succeeded())
assert(new RegExp(errorMessagePattern).test(match.message))
assert.throws(() => parse(source), errorMessagePattern)
})
}
})

0 comments on commit aee4b91

Please sign in to comment.