Skip to content

Commit

Permalink
Merge pull request #33 from wpbonelli/lark
Browse files Browse the repository at this point in the history
dfn parser working, still need to handle records and lists as nested contexts
  • Loading branch information
wpbonelli authored Sep 7, 2024
2 parents e66b09d + 2e0b811 commit eea01c8
Show file tree
Hide file tree
Showing 10 changed files with 302 additions and 58 deletions.
Empty file removed flopy4/converter.py
Empty file.
25 changes: 25 additions & 0 deletions flopy4/io/lark.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import numpy as np


def parse_word(self, w):
(w,) = w
return str(w)


def parse_string(self, s):
return " ".join(s)


def parse_int(self, i):
(i,) = i
return int(i)


def parse_float(self, f):
(f,) = f
return float(f)


def parse_array(self, a):
(a,) = a
return np.array(a)
File renamed without changes.
3 changes: 3 additions & 0 deletions flopy4/mf6/io/converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
def make_converter():
TODO
pass
34 changes: 18 additions & 16 deletions flopy4/mf6/io/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,28 @@

MF6_GRAMMAR = r"""
// component
component: _NL* (block _NL+)* _NL*
component: _NL* (block _NL+)+ _NL*
// block
block: _paramblock | _listblock
_paramblock: _BEGIN paramblock _NL params _END paramblock
block: _dictblock | _listblock
_dictblock: _BEGIN dictblock _NL dict _END dictblock
_listblock: _BEGIN listblock _NL list _END listblock
paramblock: PARAMBLOCK
dictblock: DICTBLOCK
listblock: LISTBLOCK [_blockindex]
_blockindex: INT
_BEGIN: "begin"i
_END: "end"i
// dict
dict.1: (param _NL)*
// list adapted from https://github.com/lark-parser/lark/blob/master/examples/composition/csv.lark
// negative priority for records because the pattern is so indiscriminate
list.-1: record*
record.-1: _record+ _NL
_record: scalar
// parameter
params.1: (param _NL)*
param: key | _pair
_pair: key value
key: PARAM
Expand Down Expand Up @@ -54,12 +62,6 @@
factor: "FACTOR" NUMBER
iprn: "IPRN" INT
// list adapted from https://github.com/lark-parser/lark/blob/master/examples/composition/csv.lark
// negative priority for records because the pattern is so indiscriminate
list.-1: record*
record.-1: _record+ _NL
_record: scalar
// newline
_NL: /(\r?\n[\t ]*)+/
Expand All @@ -80,7 +82,7 @@

def make_parser(
params: Iterable[str],
param_blocks: Iterable[str],
dict_blocks: Iterable[str],
list_blocks: Iterable[str],
):
"""
Expand All @@ -92,18 +94,18 @@ def make_parser(
We specify blocks containing parameters separately from blocks
that contain a list. These must be handled separately because
the pattern for list elements (records) casts a wider net than
the pattern for parameters, causing parameter blocks to parse
as lists otherwise.
the pattern for parameters, which can cause a dictionary block
of named parameters to parse as a block with a list of records.
"""
params = "|".join(['"' + n + '"i' for n in params])
param_blocks = "|".join(['"' + n + '"i' for n in param_blocks])
dict_blocks = "|".join(['"' + n + '"i' for n in dict_blocks])
list_blocks = "|".join(['"' + n + '"i' for n in list_blocks])
grammar = linesep.join(
[
MF6_GRAMMAR,
f"PARAM: ({params})",
f"PARAMBLOCK: ({param_blocks})",
f"DICTBLOCK: ({dict_blocks})",
f"LISTBLOCK: ({list_blocks})",
]
)
Expand Down
4 changes: 4 additions & 0 deletions flopy4/mf6/io/spec/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
__all__ = ["make_parser", "DFNTransformer"]

from flopy4.mf6.io.spec.parser import make_parser
from flopy4.mf6.io.spec.transformer import DFNTransformer
78 changes: 78 additions & 0 deletions flopy4/mf6/io/spec/parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from os import linesep

from lark import Lark

ATTRIBUTES = [
"block",
"name",
"type",
"reader",
"optional",
"true",
"mf6internal",
"longname",
"description",
"layered",
"shape",
"valid",
"tagged",
"in_record",
"preserve_case",
"default_value",
"numeric_index",
"deprecated",
]

DFN_GRAMMAR = r"""
// dfn
dfn: _NL* (block _NL*)+ _NL*
// block
block: _header parameter*
_header: _hash _dashes _headtext _dashes _NL+
_headtext: component subcompnt blockname
component: _word
subcompnt: _word
blockname: _word
// parameter
parameter.+1: _paramhead _NL (attribute _NL)*
_paramhead: paramblock _NL paramname
paramblock: "block" _word
paramname: "name" _word
// attribute
attribute.-1: key value
key: ATTRIBUTE
value: string
// string
_word: /[a-zA-z0-9.;\(\)\-\,\\\/]+/
string: _word+
// newline
_NL: /(\r?\n[\t ]*)+/
// comment format
_hash: /\#/
_dashes: /[\-]+/
%import common.SH_COMMENT -> COMMENT
%import common.WORD
%import common.WS_INLINE
%ignore WS_INLINE
"""
"""
EBNF description for the MODFLOW 6 definition language.
"""


def make_parser():
"""
Create a parser for the MODFLOW 6 definition language.
"""

attributes = "|".join(['"' + n + '"i' for n in ATTRIBUTES])
grammar = linesep.join([DFN_GRAMMAR, f"ATTRIBUTE: ({attributes})"])
return Lark(grammar, start="dfn")
63 changes: 63 additions & 0 deletions flopy4/mf6/io/spec/transformer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from lark import Transformer

from flopy4.io.lark import parse_string


class DFNTransformer(Transformer):
"""
Transforms a parse tree for the MODFLOW 6
specification language into a nested AST
suitable for generating an object model.
Notes
-----
Rather than a flat list of parameters for each component,
which a subsequent step is responsible for turning into a
an object hierarchy, we derive the hierarchical parameter
structure from the DFN file and return a dict of blocks,
each of which is a dict of parameters.
This can be fed to a Jinja template to generate component
modules.
"""

def key(self, k):
(k,) = k
return str(k).lower()

def value(self, v):
(v,) = v
return str(v)

def attribute(self, p):
return str(p[0]), str(p[1])

def parameter(self, p):
return dict(p[1:])

def paramname(self, n):
(n,) = n
return "name", str(n)

def paramblock(self, b):
(b,) = b
return "block", str(b)

def component(self, c):
(c,) = c
return "component", str(c)

def subcompnt(self, s):
(s,) = s
return "subcomponent", str(s)

def blockname(self, b):
(b,) = b
return "block", str(b)

def block(self, b):
params = {p["name"]: p for p in b[6:]}
return b[4][1], params

string = parse_string
dfn = dict
59 changes: 26 additions & 33 deletions flopy4/mf6/io/transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
import numpy as np
from lark import Transformer

from flopy4.io.lark import (
parse_array,
parse_float,
parse_int,
parse_string,
parse_word,
)


class MF6Transformer(Transformer):
"""
Expand All @@ -25,29 +33,6 @@ def key(self, k):
(k,) = k
return str(k).lower()

def word(self, w):
(w,) = w
return str(w)

def path(self, p):
_, p = p
return Path(p)

def string(self, s):
return " ".join(s)

def int(self, i):
(i,) = i
return int(i)

def float(self, f):
(f,) = f
return float(f)

def array(self, a):
(a,) = a
return a

def constantarray(self, a):
# TODO factor out `ConstantArray`
# array-like class from `MFArray`
Expand All @@ -65,27 +50,35 @@ def externalarray(self, a):
# TODO
pass

record = tuple
list = list
def path(self, p):
_, p = p
return Path(p)

def param(self, p):
k = p[0]
v = True if len(p) == 1 else p[1]
return k, v

params = dict

def block(self, b):
return tuple(b[:2])

def paramblock(self, bn):
return str(bn[0]).lower()
def dictblock(self, b):
return str(b[0]).lower()

def listblock(self, bn):
name = str(bn[0])
if len(bn) == 2:
index = int(bn[1])
def listblock(self, b):
name = str(b[0])
if len(b) == 2:
index = int(b[1])
name = f"{name} {index}"
return name.lower()

word = parse_word
string = parse_string
int = parse_int
float = parse_float
array = parse_array
record = tuple
list = list
dict = dict
params = dict
component = dict
Loading

0 comments on commit eea01c8

Please sign in to comment.