Skip to content
Merged
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
18 changes: 16 additions & 2 deletions src/mccode_antlr/common/textwrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,20 @@ def hide(content: str) -> str:
raise NotImplementedError


def _own_lines(content: str) -> str:
"""Put block content between its `%{` and `%}` lines.

Reading a block keeps everything between the delimiters, so content that was read
keeps its newlines as they were, and writing must add only what is missing: adding
more grows every block each time it is written and read again.
"""
if not content.startswith('\n'):
content = '\n' + content
if not content.endswith('\n'):
content += '\n'
return content


class TextWrapper(BaseWrapper):

def wrap(self, text: str, pad: str = None) -> list[str]:
Expand All @@ -132,7 +146,7 @@ def end_block_comment() -> str:
return '*/'

def block(self, name: str, content: str) -> str:
return '\n'.join([name + '\n%{\n', content + '\n%}\n'])
return f'{name}\n%{{{_own_lines(content)}%}}\n'

def line(self, name: str, items: list[str], sep: str = ' ') -> str:
items = [item or 'None' for item in items]
Expand All @@ -147,7 +161,7 @@ def lines(self, items: list[str]) -> str:

@staticmethod
def metadata_group(name: str, mimetype: str, item: str, value: str) -> str:
return f'{name} "{mimetype}" {item} \n%{{\n{value}\n%}}\n'
return f'{name} "{mimetype}" {item}\n%{{{_own_lines(value)}%}}\n'

@staticmethod
def datatype(data_type: str) -> str:
Expand Down
74 changes: 35 additions & 39 deletions src/mccode_antlr/comp/comp.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,59 +165,55 @@ def to_file(self, output=None, wrapper=None):
def _join(raw_tuple):
return '\n'.join(str(rc) for rc in raw_tuple)

# Build the parameter type prefix for define/setting/output parameters
def _param_type(p):
from mccode_antlr.common.expression import DataType, ShapeType
def _param_str(p):
# The type and default exactly as a parameter list spells them, so that
# reading the text back gives the same parameter
from mccode_antlr.common.expression import DataType, ObjectType
from mccode_antlr.common.expression.sympy_classes import UNSET_SYMPY
v = p.value
if v.is_str:
return 'string '
if v.is_vector and getattr(v, 'data_type', None) == DataType.float and \
getattr(v, '_shape', None) is not None:
return 'vector '
if hasattr(v, 'data_type') and v.data_type == DataType.int:
return 'int '
return ''
# an archaic `symbol` is read as a string with an identifier default
default = str(v)
s = 'string ' if default in ('None', 'NULL') or default.startswith('"') else 'symbol '
elif v.is_vector:
s = 'int *' if v.data_type == DataType.int else 'vector '
elif v.data_type == DataType.int:
s = 'int '
else:
s = ''
s += p.name
if not (v.is_singular and v._exprs[0] is UNSET_SYMPY):
value = str(v)
if v.object_type == ObjectType.initializer_list:
value = '{' + value + '}'
s += '=' + value
return s

def _params_str(params):
parts = []
for p in params:
s = _param_type(p) + p.name
if p.value.has_value:
s += '=' + str(p.value)
parts.append(s)
return ', '.join(parts)

name_line = f'DEFINE COMPONENT {self.name}'
if self.dependency:
name_line += f'\nDEPENDENCY "{self.dependency}"'
print(name_line, file=output)
return ', '.join(_param_str(p) for p in params)

print(f'DEFINE COMPONENT {self.name}', file=output)
if self.define:
print(f'DEFINITION PARAMETERS ({_params_str(self.define)})', file=output)
if self.setting:
print(f'SETTING PARAMETERS ({_params_str(self.setting)})', file=output)
if self.output:
print(f'OUTPUT PARAMETERS ({_params_str(self.output)})', file=output)

# The grammar fixes the order: metadata, then dependency, then NOACC
for metadata in self.metadata:
metadata.to_file(output=output, wrapper=wrapper)

if self.share:
print(wrapper.block('SHARE', _join(self.share)), file=output)
if self.user:
print(wrapper.block('USERVARS', _join(self.user)), file=output)
if self.declare:
print(wrapper.block('DECLARE', _join(self.declare)), file=output)
if self.initialize:
print(wrapper.block('INITIALIZE', _join(self.initialize)), file=output)
if self.trace:
print(wrapper.block('TRACE', _join(self.trace)), file=output)
if self.save:
print(wrapper.block('SAVE', _join(self.save)), file=output)
if self.final:
print(wrapper.block('FINALLY', _join(self.final)), file=output)
if self.display:
print(wrapper.block('MCDISPLAY', _join(self.display)), file=output)
if self.dependency:
print(f'DEPENDENCY "{self.dependency}"', file=output)
if not self.acc:
print('NOACC', file=output)

for name, raw in (('SHARE', self.share), ('USERVARS', self.user),
('DECLARE', self.declare), ('INITIALIZE', self.initialize),
('TRACE', self.trace), ('SAVE', self.save),
('FINALLY', self.final), ('MCDISPLAY', self.display)):
if raw:
print(wrapper.block(name, _join(raw)), file=output)

print('END', file=output)

Expand Down
75 changes: 75 additions & 0 deletions tests/test_comp_to_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Comp.to_string writes a definition that reads back as the same component (#391)."""
from mccode_antlr.comp import Comp
from mccode_antlr.common import DataType

SOURCE = '''DEFINE COMPONENT Everything
DEFINITION PARAMETERS (int n=3)
SETTING PARAMETERS (a, b=1e-4, int c=2, string s="x", string none=0, char *cs=NULL,
vector v=NULL, vector w={1, 2, 3}, vector u=nm, double *d, int *i={1, 2}, int *j,
symbol big=FLT_MAX)
OUTPUT PARAMETERS (o)
METADATA "text/plain" note %{
a note
%}
DEPENDENCY "-lm"
NOACC
SHARE %{
double helper(double x) { return x; }
%}
USERVARS %{ int seen; %}
DECLARE %{
double total;
%}
INITIALIZE %{
total = 0;
%}
TRACE %{
total += helper(a);
%}
SAVE %{
%}
FINALLY %{
total = 0;
%}
MCDISPLAY %{
line(0,0,0, 1,0,0);
%}
END
'''


def _read(source):
import warnings
with warnings.catch_warnings():
warnings.simplefilter('ignore') # `symbol` is archaic, and says so
return Comp.from_source(None, None, source)


def _parameters(comp):
return [(p.name, str(p.value), p.value.data_type, p.value.shape_type, p.value.object_type)
for p in comp.define + comp.setting + comp.output]


def test_a_written_component_reads_back_the_same():
original = _read(SOURCE)
written = original.to_string()
back = _read(written)
assert _parameters(back) == _parameters(original)
assert back.dependency == original.dependency == '-lm'
assert back.acc is original.acc is False
assert [(m.name, m.mimetype, m.value) for m in back.metadata] == \
[(m.name, m.mimetype, m.value) for m in original.metadata]
for block in ('share', 'user', 'declare', 'initialize', 'trace', 'save', 'final', 'display'):
# an inline `%{ ... %}` gains its own lines once, and keeps them thereafter
assert [str(r).strip() for r in getattr(back, block)] == \
[str(r).strip() for r in getattr(original, block)]
assert back.to_string() == written


def test_vector_and_array_parameters_keep_their_type_and_default():
written = _read(SOURCE).to_string()
for text in ('vector v=NULL', 'vector w={1,2,3}', 'vector u=nm', 'vector d,',
'int *i={1,2}', 'int *j,', 'symbol big=FLT_MAX'):
assert text in written
i = next(p for p in _read(written).setting if p.name == 'i')
assert i.value.data_type == DataType.int and i.value.is_vector
23 changes: 23 additions & 0 deletions tests/test_instr_to_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,26 @@ def test_to_file_with_a_file_name_or_path(tmp_path):
instr.to_file(tmp_path / 'b.instr')
assert (tmp_path / 'a.instr').read_text(encoding='utf-8') == expected
assert (tmp_path / 'b.instr').read_text(encoding='utf-8') == expected


def test_written_blocks_do_not_grow_when_read_back():
"""Writing added a blank line inside every block, so each read and write grew it."""
instr = parse_mcstas_instr(dedent("""DEFINE INSTRUMENT blocks(a=1)
METADATA "text/plain" note %{
instrument note
%}
DECLARE %{
double x;
%}
TRACE
COMPONENT origin = Arm() AT (0, 0, 0) ABSOLUTE
EXTEND %{
x += a;
%}
METADATA "text/plain" inote %{
instance note
%}
END
"""))
once = instr.to_string(None)
assert parse_mcstas_instr(once).to_string(None) == once
Loading