Skip to content
Closed
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
5 changes: 3 additions & 2 deletions maltoolbox/language/lexer_parser/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .mal_lexer import malLexer
from .mal_parser import malParser
from .mal_visitor import malVisitor
from .mal_analyzer import malAnalyzer

import sys
import os
Expand All @@ -14,6 +15,7 @@ class MalCompiler:
def __init__(self):
self.path = None
self.current_file = None
self._analyzer = malAnalyzer()

def compile(self, malfile: str = None):
if not self.path:
Expand All @@ -28,8 +30,7 @@ def compile(self, malfile: str = None):
stream = CommonTokenStream(lexer)
parser = malParser(stream)
tree = parser.mal()

return malVisitor(compiler=self).visit(tree)
return malVisitor(compiler=self, analyzer=self._analyzer).visit(tree)


if __name__ == "__main__":
Expand Down
560 changes: 560 additions & 0 deletions maltoolbox/language/lexer_parser/mal_analyzer.py

Large diffs are not rendered by default.

28 changes: 26 additions & 2 deletions maltoolbox/language/lexer_parser/mal_visitor.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from antlr4 import ParseTreeVisitor

from .mal_parser import malParser
from .mal_analyzer import malAnalyzerInterface

from collections.abc import MutableMapping, MutableSequence

Expand All @@ -10,12 +11,35 @@


class malVisitor(ParseTreeVisitor):
def __init__(self, compiler, *args, **kwargs):
def __init__(self, compiler, analyzer: malAnalyzerInterface, *args, **kwargs):
self.compiler = compiler
self.analyzer = analyzer
self.current_file = compiler.current_file # for debug purposes

super().__init__(*args, **kwargs)

def visit(self, ctx):
data = super().visit(ctx)

if (not ctx):
return data

ctx_class_name: str = type(ctx).__name__
analyzer_method_name: str = f'check{ctx_class_name.replace("Context", "")}'
analyzer_method: function | None = getattr(self.analyzer, analyzer_method_name, None)

if analyzer_method:
arguments = analyzer_method.__code__.co_argcount
if arguments in [2, 3]:
{
3: lambda: analyzer_method(ctx, data),
2: lambda: analyzer_method(ctx)
}[arguments]()
else:
raise ValueError(f'Unexpected number of arguments: {arguments}')

return data

def visitMal(self, ctx):
langspec = {
"formatVersion": "1.0.0",
Expand Down Expand Up @@ -255,7 +279,7 @@ def visitExpr(self, ctx):
ret["lhs"] = lhs
ret["rhs"] = self.visit(ctx.parts()[i])
lhs = ret.copy()

return ret

def visitParts(self, ctx):
Expand Down
Empty file.
63 changes: 63 additions & 0 deletions maltoolbox/language/tests/mal_analyzer_test_wrapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@

from antlr4 import FileStream, InputStream, CommonTokenStream

from lexer_parser.mal_lexer import malLexer
from lexer_parser.mal_parser import malParser
from lexer_parser.mal_visitor import malVisitor
from lexer_parser.mal_analyzer import malAnalyzer

import os

class MockCompiler():
def __init__(self, analyzer: malAnalyzer):
self._analyzer = analyzer
self.path = None
self.current_file = None

def compile(self, malfile: str = None):
if not self.path:
self.path = os.path.dirname(malfile)

self.current_file = os.path.basename(malfile)

input_stream = FileStream(
os.path.join(self.path, self.current_file), encoding="utf-8"
)
lexer = malLexer(input_stream)
stream = CommonTokenStream(lexer)
parser = malParser(stream)
tree = parser.mal()
return malVisitor(compiler=self, analyzer=self._analyzer).visit(tree)

class AnalyzerTestWrapper(malAnalyzer):
def __init__(self, input_string: str) -> None:
super().__init__()
input_stream = InputStream(input_string)
lexer = malLexer(input_stream)
stream = CommonTokenStream(lexer)
parser = malParser(stream)
tree = parser.mal()
compiler = MockCompiler(self)
try:
self._result = malVisitor(compiler=compiler, analyzer=self).visit(tree)
except SyntaxError:
self._error = True
except RuntimeError:
self._error = True

def test(self, error:bool=False, defines:list=[], categories:list=[], assets:list=[], lets:list=[]):
assert(self.has_error() == error)
if (defines):
assert(set(defines) == set(self._defines.keys()))
if (categories):
assert(set(categories) == set(self._category.keys()))
if (assets):
assert(set(assets) == set(self._assets.keys()))
if (lets):
for let in lets:
where, name = let
if (not (self._vars[where] and self._vars[where][name])):
assert(False)



47 changes: 47 additions & 0 deletions maltoolbox/language/tests/test_mal_abstract_asset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from .mal_analyzer_test_wrapper import AnalyzerTestWrapper

'''
A file to test different cases of the `abstract` instruction in MAL.
'''

def test_abstract_assets_1() -> None:
'''
Defines correct version and ID.
Defines category with name.
Defines abstract asset without name.
'''
AnalyzerTestWrapper('''
#id: "org.mal-lang.testAnalyzer"
#version:"0.0.0"

category System {
abstract asset {}
}

''').test(
error=True,
defines=['id', 'version'],
categories=['System']
)

def test_abstract_assets_2() -> None:
'''
Defines correct version and ID.
Defines category with name.
Defines abstract asset with name.
Extends asset with abstract asset.
'''
AnalyzerTestWrapper('''
#id: "org.mal-lang.testAnalyzer"
#version:"0.0.0"

category System {
abstract asset Foo {}
asset Bar extends Foo {}
}

''').test(
defines=['id', 'version'],
categories=['System'],
assets=['Foo', 'Bar']
)
91 changes: 91 additions & 0 deletions maltoolbox/language/tests/test_mal_assets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from .mal_analyzer_test_wrapper import AnalyzerTestWrapper

'''
A file to test different cases of the `asset` instruction in MAL.
'''

def test_assets_1() -> None:
'''
Defines correct version and ID.
Defines category with name.
Defines asset without name.
'''
AnalyzerTestWrapper('''
#id: "org.mal-lang.testAnalyzer"
#version:"0.0.0"

category System {
asset {}
}

''').test(
error=True,
defines=['id', 'version'],
categories=['System']
)

def test_assets_2() -> None:
'''
Defines correct version and ID.
Defines category with name.
Defines asset with name.
'''
AnalyzerTestWrapper('''
#id: "org.mal-lang.testAnalyzer"
#version:"0.0.0"

category System {
asset Test {}
}

''').test(
defines=['id', 'version'],
categories=['System'],
assets=['Test']
)

def test_assets_3() -> None:
'''
Defines correct version and ID.
Defines category with name.
Defines asset twice.
'''
AnalyzerTestWrapper('''
#id: "org.mal-lang.testAnalyzer"
#version:"0.0.0"

category System {
asset Test {}
asset Test {}
}

''').test(
error=True,
defines=['id', 'version'],
categories=['System'],
assets=['Test']
)

def test_assets_4() -> None:
'''
Defines correct version and ID.
Defines category with name.
Defines asset twice.
'''
AnalyzerTestWrapper('''
#id: "org.mal-lang.testAnalyzer"
#version:"0.0.0"

category System {
asset Test {}
}
category System {
asset Test {}
}

''').test(
error=True,
defines=['id', 'version'],
categories=['System'],
assets=['Test']
)
Loading