Skip to content

Commit

Permalink
WIP: static analysis
Browse files Browse the repository at this point in the history
  • Loading branch information
rtoal committed Apr 6, 2023
1 parent 5526ed9 commit be6922f
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 19 deletions.
12 changes: 12 additions & 0 deletions examples/small.pigeon
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
let x = 3;
print(x * 5);

x = 100; // ok
x = false; // error

print(y); // error
print(x); // ok

let a = ["xyz", "c"];

print(a[0])
108 changes: 94 additions & 14 deletions src/analyzer.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,58 @@
import * as core from "./core.js";

function must(condition, errorMessage) {
if (!condition) {
throw new Error(errorMessage);
}
}

function mustBeANumber(e) {
must(e.type === core.Type.NUMBER, "Number expected");
}

function mustBeABoolean(e) {
must(e.type === core.Type.BOOLEAN, "Boolean expected");
}

function mustBeAnArray(e) {
must(e.type instanceof core.ArrayType, "Array expected");
}

function mustBeDeclared(e, id) {
must(!!e, `${id.sourceString} not declared`);
}

function mustBeAssignable(source, targetType) {
must(
source.type === targetType ||
(source.type instanceof core.ArrayType &&
targetType instanceof core.ArrayType &&
source.type.baseType === targetType.baseType),
"Type mismatch"
);
}

function mustAllBeSameType(elements) {
const firstType = elements[0].type;
const allSameType = elements.slice(1).every((e) => e.type === firstType);
must(allSameType, "Mixed types in array");
}

class Context {
constructor() {
this.locals = new Map();
}
add(name, entity) {
this.locals.set(name, entity);
}
lookup(name) {
return this.locals.get(name);
}
}

export default function analyzer(match) {
const context = new Context();

const analyzer = match.matcher.grammar.createSemantics().addOperation("rep", {
Program(stmts) {
return new core.Program(stmts.children.map((s) => s.rep()));
Expand All @@ -9,15 +61,20 @@ export default function analyzer(match) {
return new core.PrintStmt(exp.rep());
},
AssignStmt(variable, _eq, exp, _semi) {
return new core.AssignStmt(variable.rep(), exp.rep());
const target = variable.rep();
const source = exp.rep();
mustBeAssignable(source, target.type);
return new core.AssignStmt(target, source);
},
VarDec(_let, id, _eq, exp, _semi) {
const initializer = exp.rep();
const variable = new core.Variable(id.sourceString);
const variable = new core.Variable(id.sourceString, initializer.type);
context.add(variable.name, variable);
return new core.VarDec(variable, initializer);
},
WhileStmt(_while, exp, block) {
const test = exp.rep();
mustBeABoolean(test);
const body = block.rep();
return new core.WhileStmt(test, body);
},
Expand Down Expand Up @@ -47,39 +104,62 @@ export default function analyzer(match) {
},
Exp_binary(cond1, relop, cond2) {
const left = cond1.rep();
operator = relop.sourceString;
mustBeANumber(left);
const operator = relop.sourceString;
const right = cond2.rep();
return new core.BinaryExp(left, operator, right);
mustBeANumber(right);
return new core.BinaryExp(left, operator, right, core.Type.BOOLEAN);
},
Condition_binary(exp, op, term) {
const left = exp.rep();
operator = op.sourceString;
mustBeANumber(left);
const operator = op.sourceString;
const right = term.rep();
return new core.BinaryExp(left, operator, right);
mustBeANumber(right);
return new core.BinaryExp(left, operator, right, core.Type.NUMBER);
},
Term_binary(term, op, factor) {
const left = term.rep();
operator = op.sourceString;
mustBeANumber(left);
const operator = op.sourceString;
const right = factor.rep();
return new core.BinaryExp(left, operator, right);
mustBeANumber(right);
return new core.BinaryExp(left, operator, right, core.Type.NUMBER);
},
Factor_binary(primary, op, factor) {
const left = primary.rep();
operator = op.sourceString;
mustBeANumber(left);
const operator = op.sourceString;
const right = factor.rep();
return new core.BinaryExp(left, operator, right);
mustBeANumber(right);
return new core.BinaryExp(left, operator, right, core.Type.NUMBER);
},
Factor_negation(op, primary) {
const operator = op.sourceString;
const operand = primary.rep();
return new core.UnaryExp(operator, operand);
mustBeANumber(operand);
return new core.UnaryExp(operator, operand, core.Type.NUMBER);
},
Primary_parens(_open, exp, _close) {
return exp.rep();
},
Primary_array(_open, exps, _close) {
const elements = exps.asIteration().children.map((e) => e.rep());
mustAllBeSameType(elements);
const arrayType = new core.ArrayType(elements[0].type);
return new core.ArrayExp(elements, arrayType);
},
Var_subscript(variable, _open, exp, _close) {
return new core.SubscriptExp(variable.rep(), exp.rep());
const array = variable.rep();
mustBeAnArray(array);
const subscript = exp.rep();
mustBeANumber(subscript);
return new core.SubscriptExp(array, subscript, array.type.baseType);
},
Var_id(id) {
// TODO: LOOKUP!
return id.sourceString;
const entity = context.lookup(id.sourceString);
mustBeDeclared(entity, id);
return entity;
},
Call(id, _open, exps, _close) {},
num(_int, _dot, _frac, _e, _sign, _exp) {
Expand Down
15 changes: 11 additions & 4 deletions src/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ export class ArrayType {

export class Type {
static VOID = new Type("void");
static BOOLEAN = new Type("boolean");
static NUMBER = new Type("number");
static STRING = new Type("string");
constructor(description) {
Object.assign(this, { description });
}
Expand All @@ -90,14 +93,14 @@ export class UnaryExp {
}

export class ArrayExp {
constructor(elements) {
Object.assign(this, elements);
constructor(elements, type) {
Object.assign(this, { elements, type });
}
}

export class SubscriptExp {
constructor(array, subscript) {
Object.assign(this, { array, subscript });
constructor(array, subscript, type) {
Object.assign(this, { array, subscript, type });
}
}

Expand All @@ -106,3 +109,7 @@ export class Call {
Object.assign(this, { fun, args });
}
}

Number.prototype.type = Type.NUMBER;
Boolean.prototype.type = Type.BOOLEAN;
String.prototype.type = Type.STRING;
2 changes: 1 addition & 1 deletion src/pigeon.ohm
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Pigeon {
| strlit
| true
| false
| "[" ListOf<Exp, ","> "]" --array
| "[" NonemptyListOf<Exp, ","> "]" --array
| "(" Exp ")" --parens

Var = Var "[" Exp "]" --subscript
Expand Down
39 changes: 39 additions & 0 deletions test/analyzer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,46 @@ import parse from "../src/parser.js";
import analyze from "../src/analyzer.js";
import * as core from "../src/core.js";

const goodPrograms = [
["comparisons", "print(3 < 5);"],
["additions", "print(7 - 2 + 5);"],
["exponentiations", "print(7 ** 3 ** 2.5 ** 5);"],
["negations", "print(7 * (-3));"],
["declared variables", "let x = 3; print(x * 5);"],
["assign nums", "let x = 3; x = 10 ** (7-2);"],
["assign bools", "let x = 3; x = 10 ** (7-2);"],
["assign arrays", "let x = [3,1]; x = [10];"],
["subscripts", "let a=[true, false]; print(a[0]);"],
["subscripted is a number", "let a=[1,2,3]; print(a[0]-5);"],
["subscripted is a bool", "let a=[false]; while a[0] {}"],
];

const badPrograms = [
["bad types in addition", "print(false + 1);", /Number expected/],
["bad types in multiplication", 'print("x" * 5);', /Number expected/],
["non-boolean while test", "while 3 {}", /Boolean expected/],
["undeclared in print", "print(x);", /x not declared/],
["undeclared in add", "print(x + 5);", /x not declared/],
["undeclared in negate", "print(-z);", /z not declared/],
["assign bool to a number", "let x = 1; x = false;", /Type mismatch/],
["arrays of mixed types", `let a = [2, "dog"];`, /Mixed types in array/],
["subscripting a number", "let a=2; print(a[1]);", /Array expected/],
["non-numeric sub", "let a=[1,2,3]; print(a[false]);", /Number expected/],
];

describe("The analyzer", () => {
for (const [scenario, source] of goodPrograms) {
it(`recognizes ${scenario}`, () => {
assert.ok(analyze(parse(source)));
});
}

for (const [scenario, source, errorMessagePattern] of badPrograms) {
it(`throws on ${scenario}`, () => {
assert.throws(() => analyze(parse(source)), errorMessagePattern);
});
}

it("builds a proper representation of the simplest program", () => {
const rep = analyze(parse("print 0;"));
assert.deepEqual(rep, new core.Program([new core.PrintStmt(0)]));
Expand Down

0 comments on commit be6922f

Please sign in to comment.