Compare commits

...

No commits in common. "v2" and "master" have entirely different histories.
v2 ... master

26 changed files with 1095 additions and 0 deletions

10
Justfile Normal file
View 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
View file

@ -0,0 +1,2 @@
include src/agtest/*.lark
include src/agtest/runtime.tmpl.py

20
default.nix Normal file
View 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
View file

@ -0,0 +1 @@
_build

20
docs/Makefile Normal file
View 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
View file

29
docs/agtest.rst Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View file

2
mypy.ini Normal file
View file

@ -0,0 +1,2 @@
[mypy]
strict = True

15
setup.cfg Normal file
View 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
View 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
View file

277
src/agtest/ast.py Normal file
View 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
View 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
View 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
View 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

View 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
View file

@ -0,0 +1,9 @@
iface Pycode {
pycode: str,
}
node Program : Pycode {
}
node Decl : Pycode {
}

16
test/arith.ag Normal file
View 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
View 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;
}
}