Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nanopass AST-with-holes for testing #156

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions typhon/nano/scopes.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,8 +438,10 @@ def visitIfExpr(self, test, consq, alt):
origLayout = self.layout
self.layout = layout1 = ScopeBox(origLayout)
origLayout.addChild(layout1)
# The test and if-branch go in the same scope box.
t = self.visitExpr(test)
c = self.visitExpr(consq)
# The else-branch does not get access to names bound in the test.
self.layout = layout2 = ScopeBox(origLayout)
origLayout.addChild(layout2)
e = self.visitExpr(alt)
Expand Down
95 changes: 95 additions & 0 deletions typhon/nanopass.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,3 +238,98 @@ def selfPass(self):
irAttrs["selfPass"] = selfPass

return type(name + "IR", (object,), irAttrs)()

def withHoles(irClass):
"""
Create an object similar to the IR class but for building an AST
pattern-matching template rather than building an AST. Designed for use in
unit tests.

Note that this currently doesn't check the schema when constructing a
quasi-AST so it's possible to construct a template that will not match any
valid AST.
"""
class IRMatcherInstance(object):
def __init__(self, quasi, name, args, cls):
self.quasi = quasi
self.name = name
self.args = args
self.cls = cls

def match(self, specimen):
if not isinstance(specimen, self.cls):
raise ValueError("Expected %s, got %s" % (
self.cls.__name__, type(specimen).__name__))
for p, (argname, argtype) in zip(self.args,
self.quasi.schema[self.name]):
val = getattr(specimen, argname)
if argtype is None or argtype in irClass.terminals:
if isinstance(p, IRHole):
p.match(val)
continue
elif val != p:
raise ValueError("Expected %r, got %r" % (p, val))
else:
continue
if (argtype.endswith('*') and isinstance(p, list)):
for subp, item in zip(p, val):
subp.match(item)
else:
p.match(val)
return self.quasi.holeMatches

class IRMatcher(object):
def __init__(self, name, quasi, original):
self.quasi = quasi
self.name = name
self.original = original

def __call__(self, *a):
if len(a) != len(self.quasi.schema[self.name]):
raise ValueError("Expected %d arguments, got %d" % (
len(a), len(self.quasi.schema[self.name])))
return IRMatcherInstance(self.quasi, self.name, a, self.original)

class IRHole(object):
def __init__(self, name, quasi, typeConstructor):
self.name = name
self.quasi = quasi
self.typeConstructor = typeConstructor

def match(self, specimen):
if (self.typeConstructor is not None and
not isinstance(specimen, self.typeConstructor)):
raise ValueError("Expected %s, got %s" % (
self.typeConstructor.__name__, type(specimen).__name__))
self.quasi.holeMatches[self.name] = specimen


class QuasiIR(object):
def __init__(self):
self.holes = []
self.holeMatches = {}

self.schema = {}
for nonterm, constructors in irClass.nonterms.iteritems():
for constructor, pieces in constructors.iteritems():
self.schema[constructor] = pieces

def __getattr__(self, name):
return IRMatcher(name, self, getattr(irClass, name))

def HOLE(self, name, typeConstructor=None):
if typeConstructor is not None:
if isinstance(typeConstructor, IRMatcher):
tycon = typeConstructor.original
elif isinstance(typeConstructor, irClass._NonTerminal):
tycon = typeConstructor
else:
raise ValueError("%s is not a type constructor for "
"either %s or %s" % (typeConstructor, irClass, self))
else:
tycon = None
h = IRHole(name, self, tycon)
self.holes.append(h)
return h

return QuasiIR()
127 changes: 127 additions & 0 deletions typhon/test/test_mast.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
from unittest import TestCase

from typhon.nanopass import CompilerFailed, withHoles
from typhon.nano.mast import MastIR, SanityCheck, SaveScriptIR as ssi
from typhon.nano.slots import NoAssignIR as nai, RemoveAssign
from typhon.nano.scopes import LayoutIR as li, LayOutScopes, ScopeBox, ScopeItem

def assertAstSame(left, right):
if left == right:
return
if isinstance(left, list):
if not isinstance(right, list):
raise AssertionError("%r != %r" % (left, right))
for l, r in zip(left, right):
assertAstSame(l, r)
return

if type(left) != type(right):
raise AssertionError("%r instance expected, %s found" %
(type(left), type(right)))
if left._immutable_fields_ and right._immutable_fields_:
leftR, rightR = left.__reduce__()[2], right.__reduce__()[2]
for k, v in leftR.iteritems():
assertAstSame(v, rightR[k])


class SanityCheckTests(TestCase):
def test_viaPattObjects(self):
oAst = MastIR.ObjectExpr(
None,
MastIR.ViaPatt(MastIR.CallExpr(
MastIR.NounExpr(u"foo"),
u"run",
[MastIR.NounExpr(u"x")], []),
MastIR.FinalPatt(u"x", None), None),
[], [], [])

self.assertRaises(CompilerFailed, SanityCheck().visitExpr, oAst)


class RemoveAssignTests(TestCase):
def test_rewriteAssign(self):
ast1 = ssi.SeqExpr([
ssi.AssignExpr(u"blee", ssi.IntExpr(1)),
ssi.NounExpr(u"blee")
])
ast2 = nai.SeqExpr([
nai.HideExpr(nai.SeqExpr([
nai.DefExpr(nai.TempPatt(u"_tempAssign"), nai.NullExpr(),
nai.IntExpr(1)),
nai.CallExpr(nai.SlotExpr(u"blee"), u"put",
[nai.TempNounExpr(u"_tempAssign")], []),
nai.TempNounExpr(u"_tempAssign")])),
nai.NounExpr(u"blee")
])

assertAstSame(ast2, RemoveAssign().visitExpr(ast1))


class LayoutScopesTests(TestCase):
def test_ifExprSeparateBoxes(self):
"""
The branches of an 'if' expression create their own scope boxes.
"""
layouter = LayOutScopes([], "test", False)
qli = withHoles(li)
top = layouter.top
ast1 = nai.IfExpr(
nai.DefExpr(nai.FinalPatt(u"a", nai.NullExpr()), nai.NullExpr(),
nai.IntExpr(1)),
nai.SeqExpr([nai.DefExpr(nai.FinalPatt(u"b", nai.NullExpr()),
nai.NullExpr(), nai.IntExpr(2)),
nai.CallExpr(nai.NounExpr(u"a"), u"add",
[nai.NounExpr("b")], [])]),
nai.SeqExpr([nai.DefExpr(nai.FinalPatt(u"b", nai.NullExpr()),
nai.NullExpr(), nai.IntExpr(3)),
nai.CallExpr(nai.NounExpr(u"a"), u"add",
[nai.NounExpr("b")], [])]))
qast2 = qli.IfExpr(
qli.DefExpr(
qli.FinalPatt(u"a", qli.NullExpr(), qli.HOLE("APattern")),
qli.NullExpr(), qli.IntExpr(1)),
qli.SeqExpr([
qli.DefExpr(
qli.FinalPatt(u"b", qli.NullExpr(),
qli.HOLE("leftBPattern")),
qli.NullExpr(), qli.IntExpr(2)),
qli.CallExpr(qli.NounExpr(u"a", qli.HOLE("leftANoun")),
"add", [qli.NounExpr("b", qli.HOLE("leftBNoun"))],
[])]),
qli.SeqExpr([qli.DefExpr(qli.FinalPatt(u"b", qli.NullExpr(),
qli.HOLE("rightBPattern")),
qli.NullExpr(), qli.IntExpr(3)),
qli.CallExpr(
qli.NounExpr(u"a", qli.HOLE("rightANoun")),
"add", [qli.NounExpr("b",
qli.HOLE("rightBNoun"))],
[])]))
scopes = qast2.match(layouter.visitExpr(ast1))
self.assertIsInstance(scopes["APattern"], ScopeBox)
self.assertIsInstance(scopes["leftBPattern"], ScopeItem)
self.assertIsInstance(scopes["leftANoun"], ScopeItem)

self.assertIs(scopes["APattern"].next, top)
self.assertIs(scopes["leftBPattern"].next, scopes["APattern"])
self.assertIs(scopes["leftANoun"].next, scopes["leftBPattern"])

self.assertIsInstance(scopes["rightBPattern"], ScopeBox)
self.assertIsInstance(scopes["rightANoun"], ScopeItem)

self.assertIs(scopes["rightBPattern"].next, top)
self.assertIs(scopes["rightANoun"].next, scopes["rightBPattern"])

def test_escapeExpr(self):
"""
Escape exprs create scope boxes for their expr and catcher, and
include the ejector and caught value in them, respectively.
"""

class InterpTests(TestCase):

def test_disabledEjector(self):
"""
Ejectors that escape their dynamic context throw when invoked.
"""
#

62 changes: 62 additions & 0 deletions typhon/test/test_nanopass.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from unittest import TestCase

from typhon.nanopass import makeIR, withHoles

ir = makeIR(
"Test",
["Noun", "Literal"],
{
"Expr": {
"DefExpr": [("patt", "Patt"), ("rvalue", "Expr")],
"NounExpr": [("name", "Noun")],
"LiteralExpr": [("val", "Literal")],
"SeqExpr": [("exprs", "Expr*")]
},
"Patt": {
"FinalPatt": [("name", "Noun")],
"VarPatt": [("name", "Noun")],
}
})

class IRDestructuringTests(TestCase):
def test_terminalHole(self):
qir = withHoles(ir)
ast1 = ir.NounExpr("blee")
qast = qir.NounExpr(qir.HOLE("name"))

self.assertEqual(qast.match(ast1), {"name": "blee"})

def test_nontermHole(self):
qir = withHoles(ir)
p = ir.FinalPatt("foo")
ast1 = ir.DefExpr(p, ir.NounExpr("blee"))
qast = qir.DefExpr(qir.HOLE("patt"), qir.NounExpr("blee"))

self.assertEqual(qast.match(ast1), {"patt": p})

def test_nonmatchTerminal(self):
qir = withHoles(ir)
p = ir.FinalPatt("foo")
ast1 = ir.DefExpr(p, ir.NounExpr("baz"))
qast = qir.DefExpr(qir.HOLE("patt"), qir.NounExpr("blee"))
with self.assertRaises(ValueError) as ve:
qast.match(ast1)
self.assertEqual(ve.exception.message, "Expected 'blee', got 'baz'")

def test_nonmatchNonterm(self):
qir = withHoles(ir)
p = ir.FinalPatt("foo")
ast1 = ir.DefExpr(p, ir.LiteralExpr(1))
qast = qir.DefExpr(qir.HOLE("patt"), qir.NounExpr("blee"))
with self.assertRaises(ValueError) as ve:
qast.match(ast1)
self.assertEqual(ve.exception.message, "Expected NounExpr3, got LiteralExpr5")

def test_nonmatchNontermType(self):
qir = withHoles(ir)
p = ir.FinalPatt("foo")
ast1 = ir.DefExpr(p, ir.LiteralExpr(1))
qast = qir.DefExpr(qir.HOLE("patt", qir.VarPatt), ir.LiteralExpr(1))
with self.assertRaises(ValueError) as ve:
qast.match(ast1)
self.assertEqual(ve.exception.message, "Expected VarPatt8, got FinalPatt7")