Skip to content
Open
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
49 changes: 37 additions & 12 deletions corpus/baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -913,32 +913,29 @@
"stopped_by": "differential.bitexact",
"verdicts": {
"differential.bitexact": {
"detail": "2 subprogram(s) could not be compared: dogleg: 10 of 10 trial(s) were compared only after the free extent(s) lr, n were moved off the configured values; the 45 ",
"detail": "3 subprogram(s) could not be compared: chkder: 19 draw(s) were declined (19 error stop) to compare 10 trial(s); the trials compared are a minority of what the c",
"metrics": {
"bit_exact": 17270,
"bit_exact": 17110,
"declined": {
"NaN on both sides": 7
},
"integer_mismatch": 0,
"integer_points": 760,
"max_rel": 0.0,
"max_ulp": 0,
"nan_mismatch": 0,
"points": 17270,
"points": 17110,
"redrawn": 7,
"subprograms": {
"chkder": {
"bit_exact": 160,
"integer_mismatch": 0,
"integer_points": 0,
"max_rel": 0.0,
"max_ulp": 0,
"nan_mismatch": 0,
"points": 160,
"redrawn": 19,
"reshaped": 0
"error": "19 draw(s) were declined (19 error stop) to compare 10 trial(s); the trials compared are a minority of what the configured draw produces and are not evidence about the rest. Narrow the draw with `ranges`, or pin `dims`, to values the subprogram takes"
},
"dogleg": {
"error": "10 of 10 trial(s) were compared only after the free extent(s) lr, n were moved off the configured values; the 45 point(s) that fit are not evidence at those extents. Pin `dims` to extents the subprogram takes"
},
"enorm": {
"bit_exact": 10,
"declined": {},
"integer_mismatch": 0,
"integer_points": 0,
"max_rel": 0.0,
Expand All @@ -950,6 +947,7 @@
},
"fdjac1": {
"bit_exact": 890,
"declined": {},
"integer_mismatch": 0,
"integer_points": 10,
"max_rel": 0.0,
Expand All @@ -961,6 +959,7 @@
},
"fdjac2": {
"bit_exact": 810,
"declined": {},
"integer_mismatch": 0,
"integer_points": 10,
"max_rel": 0.0,
Expand All @@ -972,6 +971,7 @@
},
"hybrd": {
"bit_exact": 1380,
"declined": {},
"integer_mismatch": 0,
"integer_points": 20,
"max_rel": 0.0,
Expand All @@ -983,6 +983,7 @@
},
"hybrd1": {
"bit_exact": 250,
"declined": {},
"integer_mismatch": 0,
"integer_points": 10,
"max_rel": 0.0,
Expand All @@ -994,6 +995,7 @@
},
"hybrj": {
"bit_exact": 1390,
"declined": {},
"integer_mismatch": 0,
"integer_points": 30,
"max_rel": 0.0,
Expand All @@ -1005,6 +1007,7 @@
},
"hybrj1": {
"bit_exact": 890,
"declined": {},
"integer_mismatch": 0,
"integer_points": 10,
"max_rel": 0.0,
Expand All @@ -1016,6 +1019,7 @@
},
"lmder": {
"bit_exact": 1390,
"declined": {},
"integer_mismatch": 0,
"integer_points": 110,
"max_rel": 0.0,
Expand All @@ -1027,6 +1031,7 @@
},
"lmder1": {
"bit_exact": 970,
"declined": {},
"integer_mismatch": 0,
"integer_points": 90,
"max_rel": 0.0,
Expand All @@ -1038,6 +1043,7 @@
},
"lmdif": {
"bit_exact": 1380,
"declined": {},
"integer_mismatch": 0,
"integer_points": 100,
"max_rel": 0.0,
Expand All @@ -1049,6 +1055,7 @@
},
"lmdif1": {
"bit_exact": 330,
"declined": {},
"integer_mismatch": 0,
"integer_points": 90,
"max_rel": 0.0,
Expand All @@ -1060,6 +1067,9 @@
},
"lmpar": {
"bit_exact": 970,
"declined": {
"NaN on both sides": 7
},
"integer_mismatch": 0,
"integer_points": 0,
"max_rel": 0.0,
Expand All @@ -1071,6 +1081,7 @@
},
"lmstr": {
"bit_exact": 1390,
"declined": {},
"integer_mismatch": 0,
"integer_points": 110,
"max_rel": 0.0,
Expand All @@ -1082,6 +1093,7 @@
},
"lmstr1": {
"bit_exact": 970,
"declined": {},
"integer_mismatch": 0,
"integer_points": 90,
"max_rel": 0.0,
Expand All @@ -1093,6 +1105,7 @@
},
"qform": {
"bit_exact": 720,
"declined": {},
"integer_mismatch": 0,
"integer_points": 0,
"max_rel": 0.0,
Expand All @@ -1104,6 +1117,7 @@
},
"qrfac": {
"bit_exact": 960,
"declined": {},
"integer_mismatch": 0,
"integer_points": 80,
"max_rel": 0.0,
Expand All @@ -1115,6 +1129,7 @@
},
"qrsolv": {
"bit_exact": 880,
"declined": {},
"integer_mismatch": 0,
"integer_points": 0,
"max_rel": 0.0,
Expand All @@ -1126,6 +1141,7 @@
},
"r1mpyq": {
"bit_exact": 640,
"declined": {},
"integer_mismatch": 0,
"integer_points": 0,
"max_rel": 0.0,
Expand All @@ -1140,6 +1156,7 @@
},
"rwupdt": {
"bit_exact": 890,
"declined": {},
"integer_mismatch": 0,
"integer_points": 0,
"max_rel": 0.0,
Expand Down Expand Up @@ -1318,15 +1335,18 @@
"detail": "1 translated subprogram(s) were never compared: print_msg -- defer them or drop them from the unit; silence is not a pass",
"metrics": {
"bit_exact": 10,
"declined": {},
"integer_mismatch": 0,
"integer_points": 10,
"max_rel": 0.0,
"max_ulp": 0,
"nan_mismatch": 0,
"points": 10,
"redrawn": 0,
"subprograms": {
"is_inf": {
"bit_exact": 10,
"declined": {},
"integer_mismatch": 0,
"integer_points": 10,
"max_rel": 0.0,
Expand Down Expand Up @@ -1914,15 +1934,18 @@
"detail": "80 points across 1 subprogram(s), all bit-exact",
"metrics": {
"bit_exact": 80,
"declined": {},
"integer_mismatch": 0,
"integer_points": 0,
"max_rel": 0.0,
"max_ulp": 0,
"nan_mismatch": 0,
"points": 80,
"redrawn": 0,
"subprograms": {
"sort": {
"bit_exact": 80,
"declined": {},
"integer_mismatch": 0,
"integer_points": 0,
"max_rel": 0.0,
Expand Down Expand Up @@ -2061,12 +2084,14 @@
"detail": "11 subprogram(s) could not be compared: dchfdv: oracle raised: ValueError: failed to create intent(cache|hide)|optional array-- must have defined dimensions but",
"metrics": {
"bit_exact": 0,
"declined": {},
"integer_mismatch": 0,
"integer_points": 0,
"max_rel": 0.0,
"max_ulp": 0,
"nan_mismatch": 0,
"points": 0,
"redrawn": 0,
"subprograms": {
"dchfdv": {
"error": "oracle raised: ValueError: failed to create intent(cache|hide)|optional array-- must have defined dimensions but got (-1,)"
Expand Down
96 changes: 84 additions & 12 deletions src/recast/fortran/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -1226,7 +1226,7 @@ def is_public(name: str) -> bool:
for s in subs
]
_infer_write_only_intents(subs, subprograms)
_infer_read_only_intents(subs, subprograms, sub_names)
_infer_read_only_intents(subs, subprograms, sub_names, set(state_names))
# After the inference, not before: an intent this pass just gave a
# dummy is one this rule has to see.
_mark_buffer_out_arrays(subprograms, every=buffer_out_arrays == "all")
Expand Down Expand Up @@ -1449,15 +1449,22 @@ def _mark_buffer_out_arrays(records: list[dict[str, Any]], every: bool = False)
argument["buffer"] = True


def _written_or_escaping(exec_part: Any, sub_names: set[str]) -> set[str]:
def _written_or_escaping(
exec_part: Any, sub_names: set[str], variables: set[str] | None = None
) -> set[str]:
"""Names this execution part could change, read conservatively.

A name is here if it is assigned to, if it controls a DO, if a READ fills
it, if an ALLOCATE/DEALLOCATE/NULLIFY names it -- or if it is handed to
something that might write it. An intrinsic never writes its argument and
a subscript is not a call, so neither of those escapes; a reference to one
of this file's own subprograms, and anything fparser could not resolve to
either, does.
it or a WRITE takes it as the internal unit, if an ALLOCATE, DEALLOCATE,
NULLIFY or INQUIRE names it, if an ASSOCIATE takes it as a selector -- or
if it is handed to something that might write it: a CALL, a function
reference, or a parenthesised reference whose base is not a variable this
scope declares. An intrinsic never writes its argument and a subscript of
a declared array is not a call, so neither of those escapes. A reference
to a use-associated or external procedure parses exactly like a subscript,
so a base that ``variables`` does not name is taken for a call and its
variable actuals escape; with no ``variables`` given, only this file's
own subprograms are calls, as before.
"""
escaping: set[str] = set()

Expand All @@ -1469,6 +1476,25 @@ def leftmost(node: Any) -> str | None:
node = children[0]
return str(node).lower() if isinstance(node, f03.Name) else None

def actuals(arguments: Any) -> list[Any]:
if arguments is None:
return []
if type(arguments).__name__.endswith("_List"):
return list(arguments.children)
return [arguments]

def variable_actual(item: Any) -> str | None:
"""The variable an actual argument names, if the callee could write
it: a bare name, a keyword form of one, an element or component of
one. An expression is a temporary and nobody's to write."""
if isinstance(item, f03.Actual_Arg_Spec):
return variable_actual(item.children[1])
if isinstance(item, f03.Name):
return str(item).lower()
if isinstance(item, (f03.Part_Ref, f03.Data_Ref)):
return leftmost(item)
return None

for assignment in walk(exec_part, (f03.Assignment_Stmt, f03.Pointer_Assignment_Stmt)):
name = leftmost(assignment.children[0])
if name:
Expand All @@ -1479,27 +1505,62 @@ def leftmost(node: Any) -> str | None:
break
for statement in walk(
exec_part,
(f03.Read_Stmt, f03.Allocate_Stmt, f03.Deallocate_Stmt, f03.Nullify_Stmt),
(
f03.Read_Stmt,
f03.Allocate_Stmt,
f03.Deallocate_Stmt,
f03.Nullify_Stmt,
f03.Inquire_Stmt,
),
):
for name in walk(statement, f03.Name):
escaping.add(str(name).lower())
for statement in walk(exec_part, f03.Write_Stmt):
# ``write(buf, fmt) ...``: a character variable as the unit is the
# thing written.
for spec in walk(statement.children[0], f03.Io_Control_Spec):
key, value = spec.children
if key in (None, "UNIT") and isinstance(value, f03.Name):
escaping.add(str(value).lower())
for association in walk(exec_part, f03.Association):
name = leftmost(association.children[2])
if name:
escaping.add(name)
for call in walk(exec_part, f03.Call_Stmt):
arguments = call.children[1]
for name in walk(arguments, f03.Name) if arguments is not None else []:
escaping.add(str(name).lower())
for function in walk(exec_part, f03.Function_Reference):
for item in actuals(function.children[1]):
name = variable_actual(item)
if name:
escaping.add(name)
for reference in walk(exec_part, (f03.Part_Ref, f03.Structure_Constructor)):
base = reference.children[0]
if not isinstance(base, f03.Name):
continue
if isinstance(reference, f03.Part_Ref) and str(base).lower() not in sub_names:
continue # a subscript, or a reference to something with no body here
base_name = str(base).lower()
if isinstance(reference, f03.Part_Ref) and base_name not in sub_names:
if variables is None or base_name in variables:
continue # a subscript
# Not a variable this scope declares and not a subprogram of this
# file: a use-associated or external procedure, parsed as a
# subscript. Its variable actuals are the callee's to write.
for item in actuals(reference.children[1]):
name = variable_actual(item)
if name:
escaping.add(name)
continue
for name in walk(reference.children[1], f03.Name):
escaping.add(str(name).lower())
return escaping


def _infer_read_only_intents(
subs: list[Any], records: list[dict[str, Any]], sub_names: set[str]
subs: list[Any],
records: list[dict[str, Any]],
sub_names: set[str],
state_names: set[str] | None = None,
) -> None:
"""Give a dummy the body never changes the intent its use says it has.

Expand All @@ -1509,6 +1570,11 @@ def _infer_read_only_intents(
for anything downstream that has to know whether the value after the call
is an output -- the differential gate refuses the whole subprogram over
it, so one undeclared argument costs the routine its evidence.

Read-only has to be proved, not assumed: a dummy handed to a function, to
a procedure this file does not define, to an internal WRITE or an
ASSOCIATE stays UNKNOWN, and the gate keeps refusing the routine by name
rather than comparing it with an output missing on both sides.
"""
by_name = {sub_name_of(s): s for s in subs}
for record in records:
Expand All @@ -1525,7 +1591,13 @@ def _infer_read_only_intents(
exec_part = next((c for c in node.children if isinstance(c, f03.Execution_Part)), None)
if exec_part is None:
continue
escaping = _written_or_escaping(exec_part, sub_names)
variables = (
{a["name"] for a in record["args"]}
| {local["name"] for local in record.get("locals") or []}
| {p["name"] for p in record.get("local_parameters") or []}
| set(state_names or ())
)
escaping = _written_or_escaping(exec_part, sub_names, variables)
for argument in candidates:
if argument["name"] not in escaping:
argument["intent"] = "IN"
Expand Down
Loading