Compare commits
No commits in common. "v2" and "master" have entirely different histories.
26 changed files with 1095 additions and 0 deletions
10
Justfile
Normal file
10
Justfile
Normal file
|
@ -0,0 +1,10 @@
|
|||
watch:
|
||||
watchexec -ce py,lark,ag -i gen 'just run'
|
||||
|
||||
doc:
|
||||
pipenv run make -C docs html
|
||||
|
||||
run:
|
||||
mypy *.py
|
||||
python agtest
|
||||
mypy gen/*.py
|
2
MANIFEST.in
Normal file
2
MANIFEST.in
Normal file
|
@ -0,0 +1,2 @@
|
|||
include src/agtest/*.lark
|
||||
include src/agtest/runtime.tmpl.py
|
20
default.nix
Normal file
20
default.nix
Normal file
|
@ -0,0 +1,20 @@
|
|||
{ python, buildPythonApplication, nix-gitignore, lark-parser, click, mypy }:
|
||||
|
||||
let
|
||||
pythonBuildInputs = [ click lark-parser ];
|
||||
pythonWithBuildInputs = python.withPackages (_: pythonBuildInputs);
|
||||
in
|
||||
buildPythonApplication {
|
||||
pname = "agtest";
|
||||
version = "0.1.0";
|
||||
|
||||
propagatedBuildInputs = pythonBuildInputs;
|
||||
checkInputs = [ mypy ];
|
||||
src = nix-gitignore.gitignoreSource [] ./.;
|
||||
|
||||
checkPhase = ''
|
||||
(cd src; mypy \
|
||||
--python-executable ${pythonWithBuildInputs}/bin/python \
|
||||
--strict agtest)
|
||||
'';
|
||||
}
|
1
docs/.gitignore
vendored
Normal file
1
docs/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
_build
|
20
docs/Makefile
Normal file
20
docs/Makefile
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line, and also
|
||||
# from the environment for the first two.
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
0
docs/_static/.gitkeep
vendored
Normal file
0
docs/_static/.gitkeep
vendored
Normal file
29
docs/agtest.rst
Normal file
29
docs/agtest.rst
Normal file
|
@ -0,0 +1,29 @@
|
|||
agtest package
|
||||
==============
|
||||
|
||||
Submodules
|
||||
----------
|
||||
|
||||
agtest.gen module
|
||||
-----------------
|
||||
|
||||
.. automodule:: agtest.gen
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
agtest.driver module
|
||||
--------------------
|
||||
|
||||
.. automodule:: agtest.driver
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
.. automodule:: agtest
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
52
docs/conf.py
Normal file
52
docs/conf.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# This file only contains a selection of the most common options. For a full
|
||||
# list see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
# -- Path setup --------------------------------------------------------------
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.abspath(".."))
|
||||
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = "agtest"
|
||||
copyright = "2021, Michael Zhang"
|
||||
author = "Michael Zhang <mail@mzhang.io>"
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon"]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ["_templates"]
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This pattern also affects html_static_path and html_extra_path.
|
||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = "furo"
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ["_static"]
|
15
docs/default.nix
Normal file
15
docs/default.nix
Normal file
|
@ -0,0 +1,15 @@
|
|||
{ stdenvNoCC, nix-gitignore, sphinx, furo }:
|
||||
|
||||
stdenvNoCC.mkDerivation {
|
||||
name = "agtest-docs";
|
||||
src = nix-gitignore.gitignoreSource [ ../.gitignore ] ./..;
|
||||
|
||||
buildInputs = [ sphinx furo ];
|
||||
buildPhase = ''
|
||||
make -C docs html
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
cp -r docs/_build/html $out
|
||||
'';
|
||||
}
|
22
docs/index.rst
Normal file
22
docs/index.rst
Normal file
|
@ -0,0 +1,22 @@
|
|||
.. agtest documentation master file, created by
|
||||
sphinx-quickstart on Thu Sep 30 22:35:23 2021.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to agtest's documentation!
|
||||
==================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
agtest
|
||||
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
35
docs/make.bat
Normal file
35
docs/make.bat
Normal file
|
@ -0,0 +1,35 @@
|
|||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=.
|
||||
set BUILDDIR=_build
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.https://www.sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
|
||||
:end
|
||||
popd
|
41
flake.lock
Normal file
41
flake.lock
Normal file
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1631561581,
|
||||
"narHash": "sha256-3VQMV5zvxaVLvqqUrNz3iJelLw30mIVSfZmAaauM3dA=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "7e5bf3925f6fbdfaf50a2a7ca0be2879c4261d19",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1633106717,
|
||||
"narHash": "sha256-gsUiiFFYelpOrJvPlZ/sJ1ucAM/kLiaqY7xdaA0TMo0=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "107a5943bdd61e89a45b40ee81aa51675d1d9bcc",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
30
flake.nix
Normal file
30
flake.nix
Normal file
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
description = "A very basic flake";
|
||||
|
||||
inputs = {
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
pythonPkgs = pkgs.python39Packages;
|
||||
|
||||
myPkgs = rec {
|
||||
agtest = pythonPkgs.callPackage ./. {};
|
||||
agtest-docs = pythonPkgs.callPackage ./docs {};
|
||||
};
|
||||
in
|
||||
rec {
|
||||
packages = flake-utils.lib.flattenTree myPkgs;
|
||||
|
||||
defaultPackage = packages.agtest;
|
||||
|
||||
devShell = pkgs.mkShell {
|
||||
packages = with pythonPkgs; [
|
||||
black
|
||||
];
|
||||
};
|
||||
});
|
||||
}
|
0
gen/__init__.py
Normal file
0
gen/__init__.py
Normal file
2
mypy.ini
Normal file
2
mypy.ini
Normal file
|
@ -0,0 +1,2 @@
|
|||
[mypy]
|
||||
strict = True
|
15
setup.cfg
Normal file
15
setup.cfg
Normal file
|
@ -0,0 +1,15 @@
|
|||
[metadata]
|
||||
name = click
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
agtest = agtest.driver:run
|
||||
|
||||
[options]
|
||||
packages = find:
|
||||
package_dir = = src
|
||||
include_package_data = true
|
||||
python_requires = >= 3.6
|
||||
|
||||
[options.packages.find]
|
||||
where = src
|
7
setup.py
Normal file
7
setup.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name="agtest",
|
||||
version="0.1.0",
|
||||
include_package_data=True,
|
||||
)
|
0
src/agtest/__init__.py
Normal file
0
src/agtest/__init__.py
Normal file
277
src/agtest/ast.py
Normal file
277
src/agtest/ast.py
Normal file
|
@ -0,0 +1,277 @@
|
|||
from typing import *
|
||||
import lark
|
||||
import re
|
||||
from re import Pattern
|
||||
|
||||
|
||||
def unescape(s: str) -> str:
|
||||
""" Extracts the contents out of a quoted string """
|
||||
q = s[0]
|
||||
t = ""
|
||||
i = 1
|
||||
escaped = False
|
||||
while i < len(s):
|
||||
c = s[i]
|
||||
if escaped:
|
||||
if c == q:
|
||||
t += q
|
||||
elif c == "n":
|
||||
t += "\n"
|
||||
elif c == "t":
|
||||
t += "\t"
|
||||
if c == q:
|
||||
break
|
||||
if c == "\\":
|
||||
escaped = True
|
||||
i += 1
|
||||
continue
|
||||
t += c
|
||||
i += 1
|
||||
return t
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class Ast:
|
||||
# def __new__(cls: Type[Ast], name: str, bases: Tuple[type], namespace: Dict[str, Any]) -> Ast:
|
||||
# x = super().__new__(cls, name, bases, namespace)
|
||||
# x.id = cls.__gen()
|
||||
# return x
|
||||
id: str
|
||||
n = 0
|
||||
|
||||
@classmethod
|
||||
def __gen(cls, name: str = "") -> str:
|
||||
newid = cls.n
|
||||
cls.n += 1
|
||||
return f"_a{newid}{name}"
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.id = self.__gen()
|
||||
|
||||
|
||||
class Decl:
|
||||
name: str
|
||||
|
||||
|
||||
class IfaceRef(str):
|
||||
pass
|
||||
|
||||
|
||||
class IfaceField:
|
||||
def __init__(self, name: str, ty: str):
|
||||
self.name = name
|
||||
self.ty = ty
|
||||
|
||||
|
||||
class Iface(Decl):
|
||||
def __init__(self, name: str, fields: List[IfaceField]):
|
||||
self.name = name
|
||||
self.fields = fields
|
||||
|
||||
|
||||
class Expr(Ast):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
|
||||
class NodeRef:
|
||||
pass
|
||||
|
||||
|
||||
class NodeRefByName(NodeRef, str):
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"NodeRefByName({self.name})"
|
||||
|
||||
|
||||
class NodeRegex(NodeRef):
|
||||
def __init__(self, pat: str):
|
||||
self.pat = re.compile(unescape(pat))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"NodeRegex({self.pat.pattern})"
|
||||
|
||||
|
||||
class Sym:
|
||||
pass
|
||||
|
||||
|
||||
class SymLit(Sym):
|
||||
def __init__(self, s: str):
|
||||
self.lit = unescape(s)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"SymLit({repr(self.lit)})"
|
||||
|
||||
|
||||
class SymRename(Sym):
|
||||
def __init__(self, name: str, ty: NodeRef):
|
||||
self.name = name
|
||||
self.ty = ty
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"SymRename({self.name} : {self.ty})"
|
||||
|
||||
|
||||
class Equation:
|
||||
def __init__(self, lhs: Expr, rhs: Expr):
|
||||
self.lhs = lhs
|
||||
self.rhs = rhs
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.lhs} = {self.rhs}"
|
||||
|
||||
|
||||
class Variant:
|
||||
def __init__(self, prod: List[Sym], equations: List[Equation]):
|
||||
self.prod = prod
|
||||
self.equations = equations
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Variant({self.prod}, {self.equations})"
|
||||
|
||||
|
||||
class Node(Decl):
|
||||
def __init__(self, name: str, ifaces: List[IfaceRef], variants: List[Variant]):
|
||||
self.name = name
|
||||
self.ifaces = ifaces
|
||||
self.variants = variants
|
||||
|
||||
|
||||
class ExprDot(Expr):
|
||||
def __init__(self, left: Expr, right: str):
|
||||
super().__init__()
|
||||
self.left = left
|
||||
self.right = right
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.left}.{self.right}"
|
||||
|
||||
|
||||
class ExprAdd(Expr):
|
||||
def __init__(self, left: Expr, right: Expr):
|
||||
super().__init__()
|
||||
self.left = left
|
||||
self.right = right
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.left} + {self.right}"
|
||||
|
||||
|
||||
class ExprMul(Expr):
|
||||
def __init__(self, left: Expr, right: Expr):
|
||||
super().__init__()
|
||||
self.left = left
|
||||
self.right = right
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.left} * {self.right}"
|
||||
|
||||
|
||||
class ExprCall(Expr):
|
||||
def __init__(self, func: Expr, args: List[Expr]):
|
||||
super().__init__()
|
||||
self.func = func
|
||||
self.args = args
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.func}({self.args})"
|
||||
|
||||
|
||||
class ExprName(Expr):
|
||||
def __init__(self, name: str):
|
||||
super().__init__()
|
||||
self.name = name
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.name}"
|
||||
|
||||
|
||||
class Parser(lark.Transformer[List[Decl]]):
|
||||
def program(self, items: List[Decl]) -> List[Decl]:
|
||||
return items
|
||||
|
||||
# interfaces
|
||||
def iface(self, items: List[Any]) -> Iface:
|
||||
[name, fields] = items
|
||||
return Iface(name, fields)
|
||||
|
||||
def iface_field(self, items: List[str]) -> IfaceField:
|
||||
[name, ty] = items
|
||||
return IfaceField(name, ty)
|
||||
|
||||
def iface_ref(self, items: List[str]) -> str:
|
||||
return items[0]
|
||||
|
||||
def iface_refs(self, items: List[IfaceRef]) -> List[IfaceRef]:
|
||||
return items
|
||||
|
||||
# nodes
|
||||
def node(self, items: List[Any]) -> Node:
|
||||
[name, ifaces, variants] = items
|
||||
return Node(name, ifaces, variants)
|
||||
|
||||
def node_ref_name(self, items: List[str]) -> NodeRefByName:
|
||||
return NodeRefByName(items[0])
|
||||
|
||||
def node_regex(self, items: List[str]) -> NodeRegex:
|
||||
return NodeRegex(items[0])
|
||||
|
||||
# variants
|
||||
def variants(self, items: List[Variant]) -> List[Variant]:
|
||||
return items
|
||||
|
||||
def variant(self, items: List[Any]) -> Variant:
|
||||
[prod, equations] = items
|
||||
return Variant(prod, equations)
|
||||
|
||||
def prod(self, items: List[Sym]) -> List[Sym]:
|
||||
return items
|
||||
|
||||
# symbols in productions
|
||||
def sym_lit(self, items: List[str]) -> Sym:
|
||||
return SymLit(items[0])
|
||||
|
||||
def sym_rename(self, items: List[Any]) -> Sym:
|
||||
return SymRename(items[0], items[1])
|
||||
|
||||
# equations
|
||||
def equations(self, items: List[Equation]) -> List[Equation]:
|
||||
return items
|
||||
|
||||
def equation_semi(self, items: List[Equation]) -> Equation:
|
||||
return items[0]
|
||||
|
||||
def equation(self, items: List[Expr]) -> Equation:
|
||||
return Equation(items[0], items[1])
|
||||
|
||||
# expr
|
||||
def expr_dot(self, items: List[Any]) -> Expr:
|
||||
[left, right] = items
|
||||
return ExprDot(left, right)
|
||||
|
||||
def expr_add(self, items: List[Expr]) -> Expr:
|
||||
[left, right] = items
|
||||
return ExprAdd(left, right)
|
||||
|
||||
def expr_mul(self, items: List[Expr]) -> Expr:
|
||||
[left, right] = items
|
||||
return ExprMul(left, right)
|
||||
|
||||
def expr_call(self, items: List[Expr]) -> Expr:
|
||||
[func, args] = items
|
||||
# TODO: args should be a list of exprs -_ -
|
||||
return ExprCall(func, [args])
|
||||
|
||||
def expr_name(self, items: List[str]) -> Expr:
|
||||
return ExprName(items[0])
|
||||
|
||||
def sep_trail(self, items: List[lark.Tree]) -> List[T]:
|
||||
return list(map(lambda it: cast(T, it), items))
|
||||
|
||||
def ident(self, items: List[lark.Token]) -> str:
|
||||
return cast(str, items[0].value)
|
63
src/agtest/driver.py
Normal file
63
src/agtest/driver.py
Normal file
|
@ -0,0 +1,63 @@
|
|||
import json
|
||||
import sys
|
||||
from typing import TextIO
|
||||
from io import StringIO
|
||||
from os import path
|
||||
|
||||
import lark
|
||||
import click
|
||||
|
||||
from agtest.ast import Parser
|
||||
from agtest.gen import GenResult
|
||||
|
||||
src_dir = path.dirname(path.realpath(__file__))
|
||||
|
||||
grammar_path = path.join(src_dir, "grammar.lark")
|
||||
runtime_path = path.join(src_dir, "runtime.tmpl.py")
|
||||
p = lark.Lark(open(grammar_path).read(), start="program", parser="lalr")
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option("--show-only", is_flag=True)
|
||||
@click.argument("input", type=click.File("r"))
|
||||
def run(input: TextIO, show_only: bool) -> None:
|
||||
data = input.read()
|
||||
input.close()
|
||||
|
||||
cst = p.parse(data)
|
||||
|
||||
trans = Parser()
|
||||
ast = trans.transform(cst)
|
||||
|
||||
res = GenResult(ast)
|
||||
res.build()
|
||||
|
||||
with open(runtime_path, "r") as f:
|
||||
fmt_str = f.read()
|
||||
|
||||
s = StringIO()
|
||||
s.write(
|
||||
fmt_str.format(
|
||||
pd=res.parser_data,
|
||||
ex=res.extra,
|
||||
starts=list(res.starts),
|
||||
trans_def=res.trans_def,
|
||||
ntmap=json.dumps(res.nonterminal_map),
|
||||
)
|
||||
)
|
||||
|
||||
if show_only:
|
||||
print(s.getvalue())
|
||||
sys.exit(0)
|
||||
|
||||
print("Dropping you in a Python shell...")
|
||||
print("Call parse(str) to parse something.")
|
||||
|
||||
import imp
|
||||
|
||||
mod = imp.new_module("mod")
|
||||
exec(s.getvalue(), mod.__dict__)
|
||||
|
||||
import code
|
||||
|
||||
code.InteractiveConsole(locals=mod.__dict__).interact()
|
287
src/agtest/gen.py
Normal file
287
src/agtest/gen.py
Normal file
|
@ -0,0 +1,287 @@
|
|||
from typing import *
|
||||
import textwrap
|
||||
import re
|
||||
import copy
|
||||
import json
|
||||
import sys
|
||||
import copy
|
||||
from collections import defaultdict
|
||||
|
||||
from agtest.ast import *
|
||||
|
||||
|
||||
def eprint(*args: Any, **kwargs: Any) -> None:
|
||||
print(*args, file=sys.stderr, **kwargs)
|
||||
|
||||
|
||||
global i
|
||||
i = 0
|
||||
|
||||
|
||||
def gensym(prefix: str = "", suffix: str = "") -> str:
|
||||
global i
|
||||
presan = re.sub("[^0-9a-zA-Z]+", "_", prefix)
|
||||
sufsan = re.sub("[^0-9a-zA-Z]+", "_", suffix)
|
||||
i += 1
|
||||
return f"{presan}{i}{sufsan}"
|
||||
|
||||
|
||||
class NodeDesc:
|
||||
def __init__(self, node: Node):
|
||||
self.node = node
|
||||
self.name = node.name
|
||||
self.nonterminal = gensym(node.name.lower())
|
||||
|
||||
|
||||
class ParseEquation:
|
||||
def __init__(self, name: str, syms: List[str], pyty: str, assigns: List[str], pycode: str):
|
||||
self.name = name
|
||||
self.syms = syms
|
||||
self.pyty = pyty
|
||||
self.assigns = assigns
|
||||
self.pycode = pycode
|
||||
|
||||
|
||||
class GenResult:
|
||||
def __init__(self, program: List[Decl]):
|
||||
self.program = program
|
||||
self.literals: Dict[str, str] = dict()
|
||||
self.parse_rules: defaultdict[str, List[ParseEquation]] = defaultdict(list)
|
||||
self.starts: Set[str] = set()
|
||||
self.extra: str = ""
|
||||
self.nonterminal_map: Dict[str, str] = dict()
|
||||
|
||||
self.ifaces: Dict[str, Iface] = dict()
|
||||
self.what_ifaces: Dict[str, Set[str]] = dict()
|
||||
self.what_fields: Dict[str, Dict[str, str]] = dict()
|
||||
self.node_map: Dict[str, NodeDesc] = dict()
|
||||
|
||||
self.builtins: Dict[str, str] = {
|
||||
"parseInt": "",
|
||||
}
|
||||
|
||||
@property
|
||||
def trans_def(self) -> str:
|
||||
s = []
|
||||
for name, rules in self.parse_rules.items():
|
||||
possible_returns = ", ".join(map(lambda e: e.pyty, rules))
|
||||
|
||||
n = name.lstrip("?")
|
||||
code = textwrap.dedent(
|
||||
f"""
|
||||
def {n}(self, items: List[Union[{possible_returns}]]) -> {n}:
|
||||
return items[0]
|
||||
"""
|
||||
)
|
||||
s.append(code)
|
||||
|
||||
for equation in rules:
|
||||
assigns = []
|
||||
for name, value in equation.assigns:
|
||||
assigns.append(f"res.{name} = {value}")
|
||||
assigns_str = "\n".join(assigns)
|
||||
assigns_str = textwrap.indent(assigns_str, " ")
|
||||
|
||||
pycode = textwrap.indent(equation.pycode, " ")
|
||||
code = textwrap.dedent(
|
||||
"""
|
||||
def {equation.name}(self, items: Any) -> {equation.pyty}:
|
||||
res = {equation.pyty}()
|
||||
{pycode}
|
||||
{assigns_str}
|
||||
return res
|
||||
"""
|
||||
)
|
||||
code = code.format(equation=equation, assigns_str=assigns_str, pycode=pycode)
|
||||
s.append(code)
|
||||
if not s:
|
||||
s = ["pass"]
|
||||
return textwrap.indent("\n".join(s), " ")
|
||||
|
||||
@property
|
||||
def parser_data(self) -> str:
|
||||
s = []
|
||||
for sym, pat in self.literals.items():
|
||||
s.append(f"{sym}: {pat}")
|
||||
for name, rules in self.parse_rules.items():
|
||||
names = []
|
||||
for rule in rules:
|
||||
names.append(rule.name)
|
||||
s.append(f"{rule.name}: {' '.join(rule.syms)}")
|
||||
s.append(f"{name}: {' | '.join(names)}")
|
||||
s.append("%import common.WS")
|
||||
s.append("%ignore WS")
|
||||
return "\n".join(s)
|
||||
|
||||
def _collect_ifaces(self) -> None:
|
||||
"""collect a list of name -> iface declarations"""
|
||||
self.ifaces = dict(
|
||||
map(
|
||||
lambda c: (c.name, cast(Iface, c)),
|
||||
filter(lambda c: isinstance(c, Iface), self.program),
|
||||
)
|
||||
)
|
||||
|
||||
def _create_iface_mappings(self) -> None:
|
||||
"""list of node -> iface mappings"""
|
||||
self.what_ifaces = dict()
|
||||
self.what_fields = dict()
|
||||
for node in filter(lambda c: isinstance(c, Node), self.program):
|
||||
node = cast(Node, node)
|
||||
# all_fields = dict()
|
||||
self.what_ifaces[node.name] = set(node.ifaces)
|
||||
this_fields = dict()
|
||||
for iface in node.ifaces:
|
||||
fields = self.ifaces[iface].fields
|
||||
for field in fields:
|
||||
if field.name in this_fields:
|
||||
raise Exception("duplicate field name")
|
||||
this_fields[field.name] = field.ty
|
||||
self.what_fields[node.name] = this_fields
|
||||
|
||||
def _collect_required_thunks(
|
||||
self, env: List[Tuple[str, NodeRef]], expr: Expr
|
||||
) -> Dict[str, str]:
|
||||
names = dict(env)
|
||||
if isinstance(expr, ExprDot):
|
||||
return self._collect_required_thunks(env, expr.left)
|
||||
elif isinstance(expr, ExprMul):
|
||||
a = self._collect_required_thunks(env, expr.left)
|
||||
b = self._collect_required_thunks(env, expr.right)
|
||||
a.update(b)
|
||||
return a
|
||||
elif isinstance(expr, ExprAdd):
|
||||
a = self._collect_required_thunks(env, expr.left)
|
||||
b = self._collect_required_thunks(env, expr.right)
|
||||
a.update(b)
|
||||
return a
|
||||
elif isinstance(expr, ExprCall):
|
||||
return self._collect_required_thunks(env, expr.func)
|
||||
elif isinstance(expr, ExprName):
|
||||
if expr.name not in names and expr.name not in self.builtins:
|
||||
raise Exception(f"unbound name '{expr.name}'")
|
||||
return dict()
|
||||
raise Exception(f"unhandled {expr.__class__}")
|
||||
|
||||
def _resolve_production(self, sym: Sym) -> Tuple[bool, str]:
|
||||
"""resolving a production just means checking to make sure it's a type that exists or it's a regex"""
|
||||
if isinstance(sym, SymRename):
|
||||
if isinstance(sym.ty, NodeRefByName):
|
||||
if sym.ty.name in self.node_map:
|
||||
return True, self.node_map[sym.ty.name].nonterminal
|
||||
else:
|
||||
raise Exception(f"unresolved name {sym.ty.name} in production")
|
||||
elif isinstance(sym.ty, NodeRegex):
|
||||
sym_name = gensym("sym")
|
||||
self.literals[sym_name] = f"/{sym.ty.pat.pattern}/"
|
||||
return True, sym_name
|
||||
elif isinstance(sym, SymLit):
|
||||
sym_name = gensym("lit")
|
||||
# hack to make repr have double quotes
|
||||
self.literals[sym_name] = json.dumps(sym.lit)
|
||||
return False, sym_name
|
||||
raise Exception(f"unhandled {sym.__class__}")
|
||||
|
||||
def _build_node_map(self) -> None:
|
||||
for _node in filter(lambda c: isinstance(c, Node), self.program):
|
||||
nd = NodeDesc(cast(Node, _node))
|
||||
self.node_map[_node.name] = nd
|
||||
self.nonterminal_map[nd.name] = nd.nonterminal
|
||||
|
||||
def build(self) -> None:
|
||||
def v(name: str) -> str:
|
||||
return f"__ag_{name}"
|
||||
|
||||
self._collect_ifaces()
|
||||
self._create_iface_mappings()
|
||||
self._build_node_map()
|
||||
|
||||
eprint("IFACE MAPS", self.what_fields, self.what_ifaces)
|
||||
|
||||
# a high-level dictionary of productions; this has sub-productions that
|
||||
# should be further expanded at a later step before converting into lark
|
||||
# code
|
||||
productions_hi: Dict[str, Union[str, List[str]]] = dict()
|
||||
|
||||
for node_desc in self.node_map.values():
|
||||
assert isinstance(node_desc, NodeDesc)
|
||||
self.starts.add(node_desc.nonterminal)
|
||||
|
||||
class_fields = []
|
||||
for field_name, field_ty in self.what_fields[node_desc.name].items():
|
||||
class_fields.append(f"{field_name}: Thunk[{field_ty}]")
|
||||
class_fields_str = textwrap.indent("\n".join(class_fields), " ")
|
||||
|
||||
class_decl = textwrap.dedent(
|
||||
"""
|
||||
class {nonterminal}:
|
||||
{g}
|
||||
pass
|
||||
"""
|
||||
).format(nonterminal=node_desc.nonterminal, g=class_fields_str)
|
||||
self.extra += class_decl
|
||||
|
||||
# print(node_desc.name, node_desc.node.ifaces)
|
||||
|
||||
for variant in node_desc.node.variants:
|
||||
v_class_name = gensym(f"{node_desc.nonterminal}_var")
|
||||
|
||||
prod_name = gensym(node_desc.nonterminal + "_")
|
||||
|
||||
# figure out which of the symbols are inputs (aka contain data)
|
||||
# vs. literals which only parse
|
||||
# TODO: probably should also just make literals return their values
|
||||
seq = []
|
||||
inputs = []
|
||||
renamed = dict()
|
||||
for i, sym in enumerate(variant.prod):
|
||||
isInput, n = self._resolve_production(sym)
|
||||
var_name = f"inp_{n}_idx{i}"
|
||||
|
||||
if isInput:
|
||||
inputs.append((i, n, var_name))
|
||||
seq.append(n)
|
||||
|
||||
if isinstance(sym, SymRename):
|
||||
renamed[sym.name] = i
|
||||
|
||||
for field in class_fields:
|
||||
eprint("FIELD", field)
|
||||
|
||||
# generate assignments for codegen
|
||||
input_fields = copy.deepcopy(class_fields)
|
||||
assigns = []
|
||||
for i, inp, var_name in inputs:
|
||||
name = gensym(f"{v_class_name}_inp")
|
||||
input_fields.append(f"{var_name}: Any = 0")
|
||||
assigns.append((var_name, f"items[{i}]"))
|
||||
class_fields_str = textwrap.indent("\n".join(input_fields), " ")
|
||||
|
||||
class_decl = textwrap.dedent(
|
||||
"""
|
||||
class {v_class_name}({nonterminal}):
|
||||
{g}
|
||||
pass
|
||||
"""
|
||||
).format(
|
||||
v_class_name=v_class_name, nonterminal=node_desc.nonterminal, g=class_fields_str
|
||||
)
|
||||
self.extra += class_decl
|
||||
|
||||
pycode = ""
|
||||
self.parse_rules[node_desc.nonterminal].append(
|
||||
ParseEquation(prod_name, seq, v_class_name, assigns, pycode)
|
||||
)
|
||||
|
||||
# create an environment for checking the equations based on the
|
||||
# production
|
||||
env: List[Tuple[str, NodeRef]] = list()
|
||||
for sym in variant.prod:
|
||||
if isinstance(sym, SymRename):
|
||||
env.append((sym.name, sym.ty))
|
||||
|
||||
# for each of the equations, find out what the equation is
|
||||
# trying to compute, and generate a thunk corresponding to that
|
||||
# value.
|
||||
for eq in variant.equations:
|
||||
self._collect_required_thunks(copy.deepcopy(env), eq.rhs)
|
64
src/agtest/grammar.lark
Normal file
64
src/agtest/grammar.lark
Normal file
|
@ -0,0 +1,64 @@
|
|||
program: decl*
|
||||
|
||||
?decl: iface
|
||||
| node
|
||||
| func
|
||||
|
||||
sep_trail{item, punc}: item (punc item)? punc?
|
||||
|
||||
func: "fn" ident "(" ")" ("->" ty) "{" "}"
|
||||
|
||||
iface: "iface" ident "{" sep_trail{iface_field, ","} "}"
|
||||
iface_field: ident ":" ident
|
||||
|
||||
iface_ref: ident
|
||||
iface_refs: iface_ref*
|
||||
node: "node" ident ":" iface_refs "{" variants "}"
|
||||
variants: variant*
|
||||
variant: prod "=>" "{" equations "}"
|
||||
prod: sym*
|
||||
?sym: sym_rename
|
||||
| sym_lit
|
||||
sym_lit: ESCAPED_STRING
|
||||
sym_rename: "<" ident ":" node_ref ">"
|
||||
?node_ref: node_ref_name
|
||||
| node_regex
|
||||
node_ref_name: ident
|
||||
node_regex: ESCAPED_STRING
|
||||
equations: equation_semi*
|
||||
equation_semi: equation ";"
|
||||
// TODO: the left side should really be a separate type
|
||||
// called lvalue, and should NOT include literals
|
||||
equation: expr "=" expr
|
||||
|
||||
// Expressions
|
||||
?expr: expr2
|
||||
| expr_add
|
||||
expr_add: expr "+" expr2
|
||||
|
||||
?expr2: expr3
|
||||
| expr_mul
|
||||
| expr_call
|
||||
expr_mul: expr2 "*" expr3
|
||||
expr_call: expr2 "(" args ")"
|
||||
|
||||
?expr3: "(" expr ")"
|
||||
| expr_dot
|
||||
| expr_name
|
||||
expr_dot: expr3 "." ident
|
||||
expr_name: ident
|
||||
|
||||
args: sep_trail{expr, ","}
|
||||
|
||||
ty: ident
|
||||
|
||||
ident: IDENT
|
||||
COMMENT: /\/\/[^\n]*/
|
||||
IDENT: /([a-zA-Z][a-zA-Z0-9_]*)|(_[a-zA-Z0-9_]+)/
|
||||
|
||||
%import python.STRING
|
||||
%import common.WS
|
||||
%import common.ESCAPED_STRING
|
||||
|
||||
%ignore WS
|
||||
%ignore COMMENT
|
53
src/agtest/runtime.tmpl.py
Normal file
53
src/agtest/runtime.tmpl.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
# This document is generated by agtest.
|
||||
# type: ignore
|
||||
|
||||
__all__ = ["parse"]
|
||||
import re
|
||||
from typing import Generic, TypeVar, Optional, Callable, Dict, Any, Union, List
|
||||
from lark import Lark, Transformer
|
||||
|
||||
T = TypeVar("T")
|
||||
builtins: Dict[str, Any] = {{"parseInt": lambda s: int(s)}}
|
||||
|
||||
|
||||
class Thunk(Generic[T]):
|
||||
"""A thunk represents a value that may be computed lazily."""
|
||||
|
||||
value: Optional[T]
|
||||
|
||||
def __init__(self, func: Callable[[], T]):
|
||||
self.func = func
|
||||
self.value = None
|
||||
|
||||
def get(self) -> T:
|
||||
if self.value is None:
|
||||
self.value = self.func()
|
||||
return self.value
|
||||
|
||||
|
||||
parser = Lark(
|
||||
"""
|
||||
{pd}
|
||||
""",
|
||||
parser="lalr",
|
||||
start={starts},
|
||||
debug=True,
|
||||
)
|
||||
|
||||
{ex}
|
||||
|
||||
|
||||
class Trans(Transformer[None]):
|
||||
{trans_def}
|
||||
|
||||
|
||||
__agNonterminals = {ntmap}
|
||||
|
||||
|
||||
def parse(input: str, start: Optional[str] = None) -> Any:
|
||||
if start is not None:
|
||||
start = __agNonterminals[start]
|
||||
tree = parser.parse(input, start)
|
||||
trans = Trans()
|
||||
res = trans.transform(tree)
|
||||
return res
|
9
test/agtest.ag
Normal file
9
test/agtest.ag
Normal file
|
@ -0,0 +1,9 @@
|
|||
iface Pycode {
|
||||
pycode: str,
|
||||
}
|
||||
|
||||
node Program : Pycode {
|
||||
}
|
||||
|
||||
node Decl : Pycode {
|
||||
}
|
16
test/arith.ag
Normal file
16
test/arith.ag
Normal file
|
@ -0,0 +1,16 @@
|
|||
iface HasValue {
|
||||
val: int,
|
||||
}
|
||||
|
||||
node Expr : HasValue {
|
||||
<l:Expr> "+" <r:Expr> => {
|
||||
self.val = l.val + r.val;
|
||||
}
|
||||
|
||||
<l:Expr> "*" <r:Expr> => {
|
||||
self.val = l.val * r.val;
|
||||
}
|
||||
|
||||
<n:"[0-9]+"> => { self.val = parseInt(n); }
|
||||
}
|
||||
|
25
test/let.ag
Normal file
25
test/let.ag
Normal file
|
@ -0,0 +1,25 @@
|
|||
iface HasEnv {
|
||||
env: Map<str, str>,
|
||||
}
|
||||
|
||||
iface HasVal {
|
||||
val: str,
|
||||
}
|
||||
|
||||
alias Ident = /([a-zA-Z][a-zA-Z0-9_]*)|(_[a-zA-Z0-9_]+)/
|
||||
|
||||
node Expr : HasEnv + HasVal {
|
||||
"let" <name:Ident> "=" <val:Expr> "in" <body:Expr> => {
|
||||
body.env = self.env.with(name, val);
|
||||
self.val = body.val;
|
||||
}
|
||||
<name:Ident> => {
|
||||
// TODO: does env need to be referenced here?
|
||||
// TODO: how to check for unbound names ahead of time
|
||||
// (for self-implementation)
|
||||
self.val = self.env.lookup(name);
|
||||
}
|
||||
<string:StringLit> => {
|
||||
self.val = string;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue