forterp 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- forterp/__init__.py +142 -0
- forterp/__main__.py +7 -0
- forterp/ast.py +7 -0
- forterp/ast_nodes.py +259 -0
- forterp/cli.py +219 -0
- forterp/command.py +354 -0
- forterp/debug.py +313 -0
- forterp/diagnostics.py +53 -0
- forterp/dialect.py +74 -0
- forterp/engine.py +2377 -0
- forterp/fmt.py +562 -0
- forterp/forbin.py +259 -0
- forterp/forlib.py +191 -0
- forterp/format.py +9 -0
- forterp/frontend.py +24 -0
- forterp/hostlib.py +529 -0
- forterp/interpreter.py +162 -0
- forterp/lexer.py +234 -0
- forterp/parser.py +1480 -0
- forterp/repl.py +262 -0
- forterp/runtime.py +117 -0
- forterp/source.py +385 -0
- forterp/target.py +153 -0
- forterp/uuolib.py +107 -0
- forterp-0.1.0.dist-info/METADATA +255 -0
- forterp-0.1.0.dist-info/RECORD +30 -0
- forterp-0.1.0.dist-info/WHEEL +5 -0
- forterp-0.1.0.dist-info/entry_points.txt +4 -0
- forterp-0.1.0.dist-info/licenses/LICENSE +21 -0
- forterp-0.1.0.dist-info/top_level.txt +1 -0
forterp/__init__.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""forterp -- a configurable FORTRAN-66 / DEC FORTRAN-10 interpreter in Python.
|
|
2
|
+
|
|
3
|
+
A configurable FORTRAN-66 interpreter: the machine value model (`Target`) and the
|
|
4
|
+
front-end dialect (`Dialect`) are both pluggable. The default target is NATIVE (a
|
|
5
|
+
portable 64-bit host); PDP10 (36-bit, packed ASCII, .TRUE.=-1) is the faithful DEC
|
|
6
|
+
FORTRAN-10 target, selected with `Engine(..., target=PDP10)`.
|
|
7
|
+
|
|
8
|
+
Quick start::
|
|
9
|
+
|
|
10
|
+
import forterp
|
|
11
|
+
eng = forterp.run_source(''' PROGRAM HI
|
|
12
|
+
WRITE(6,10)
|
|
13
|
+
10 FORMAT(' HELLO, WORLD')
|
|
14
|
+
END
|
|
15
|
+
''', printer=print)
|
|
16
|
+
|
|
17
|
+
Public API (see ``__all__``):
|
|
18
|
+
run_source(text, ...) -- parse + run a source string; returns the Engine
|
|
19
|
+
parse_source(text, ...) -- parse source -> {name: ProgramUnit} (raises ParseError)
|
|
20
|
+
f66, fortran10 -- prebuilt, ready-to-run interpreters (the easy path:
|
|
21
|
+
forterp.fortran10.run_source(src) / .parse_dir(dir) /
|
|
22
|
+
.build_engine(units)); Interpreter to roll your own
|
|
23
|
+
F66, FORTRAN10, Dialect -- the front-end dialect (F66 is the default; FORTRAN10 the
|
|
24
|
+
DEC superset: octal / tab-format / '!' / free-form input)
|
|
25
|
+
NATIVE, PDP10, VAX, Target -- the machine value model (NATIVE 64-bit is the default;
|
|
26
|
+
PDP10 the 36-bit DEC target; VAX provisional)
|
|
27
|
+
ParseError, SourceOptions -- the parse error, and source-recovery options (orthogonal
|
|
28
|
+
to the dialect; default: no recovery)
|
|
29
|
+
|
|
30
|
+
Expert surfaces live behind explicit namespaces (the package root exposes only the names in
|
|
31
|
+
``__all__`` above -- there are no back-compat aliases):
|
|
32
|
+
forterp.frontend -- lexer + parser stages (scan_file, parse_units, tokenize, ...)
|
|
33
|
+
forterp.format -- the FORMAT engine (parse_format, render, read_values, ...)
|
|
34
|
+
forterp.runtime -- the Engine and engine builders (Engine, Frame, make_engine, ...)
|
|
35
|
+
forterp.hostlib -- declarative marshalling for host-defined builtins
|
|
36
|
+
forterp.ast -- the AST node classes the parser produces
|
|
37
|
+
forterp.debug -- OOB-access census + the interactive tracer / profiler
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
# The package root exposes ONLY the focused public surface (see __all__). Everything else --
|
|
41
|
+
# the Engine and builders, the lexer/parser stages, the FORMAT engine, the AST nodes -- lives
|
|
42
|
+
# in the expert namespaces (forterp.frontend / .format / .runtime / .ast / .hostlib), imported
|
|
43
|
+
# at the bottom of this module. There are no back-compat root aliases.
|
|
44
|
+
from forterp.dialect import F66, FORTRAN10, Dialect
|
|
45
|
+
from forterp.interpreter import Interpreter, f66, fortran10
|
|
46
|
+
from forterp.parser import ParseError
|
|
47
|
+
from forterp.source import SourceOptions
|
|
48
|
+
from forterp.target import NATIVE, PDP10, VAX, Target
|
|
49
|
+
from forterp.uuolib import UnmodeledMonitorTable
|
|
50
|
+
|
|
51
|
+
# The one place the version is written. pyproject.toml reads it via
|
|
52
|
+
# [tool.setuptools.dynamic] (attr = "forterp.__version__"), so the package metadata and
|
|
53
|
+
# this attribute can never drift apart.
|
|
54
|
+
__version__ = "0.1.0"
|
|
55
|
+
|
|
56
|
+
# The complete public surface: the package root exposes exactly these names. Everything else
|
|
57
|
+
# lives in the expert namespaces (forterp.frontend / .format / .runtime / .ast / .hostlib).
|
|
58
|
+
__all__ = [
|
|
59
|
+
# parse + run
|
|
60
|
+
"run_source",
|
|
61
|
+
"parse_source",
|
|
62
|
+
# prebuilt interpreters and the class behind them
|
|
63
|
+
"f66",
|
|
64
|
+
"fortran10",
|
|
65
|
+
"Interpreter",
|
|
66
|
+
# dialects
|
|
67
|
+
"F66",
|
|
68
|
+
"FORTRAN10",
|
|
69
|
+
"Dialect",
|
|
70
|
+
# machine value models
|
|
71
|
+
"NATIVE",
|
|
72
|
+
"PDP10",
|
|
73
|
+
"VAX",
|
|
74
|
+
"Target",
|
|
75
|
+
# commonly-needed types
|
|
76
|
+
"ParseError",
|
|
77
|
+
"SourceOptions",
|
|
78
|
+
"UnmodeledMonitorTable",
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def parse_source(text, dialect=F66, on_error=None, options=None, include_dir="."):
|
|
83
|
+
"""Parse FORTRAN source text into a {name: ProgramUnit} dict.
|
|
84
|
+
|
|
85
|
+
`dialect` selects the language (F66 default / FORTRAN10 superset). `options` is a
|
|
86
|
+
`SourceOptions` for source-recovery handling (orthogonal to the dialect; default is
|
|
87
|
+
no recovery). `include_dir` is the base directory INCLUDE targets resolve
|
|
88
|
+
against (default the current directory; the CLI passes the source file's directory).
|
|
89
|
+
|
|
90
|
+
Raises ``ParseError`` on malformed source, with every diagnostic in the message --
|
|
91
|
+
invalid statements are NOT silently dropped. Pass ``on_error(statement, message)``
|
|
92
|
+
to instead receive each diagnostic yourself and keep the (partial) result.
|
|
93
|
+
"""
|
|
94
|
+
from forterp.parser import parse_units
|
|
95
|
+
from forterp.source import DEFAULT_OPTIONS, expand_includes, scan_text
|
|
96
|
+
|
|
97
|
+
errs = []
|
|
98
|
+
cb = on_error if on_error is not None else (lambda st, m: errs.append((st.line, m)))
|
|
99
|
+
opts = options if options is not None else DEFAULT_OPTIONS
|
|
100
|
+
stmts = expand_includes(
|
|
101
|
+
scan_text(text, dialect=dialect, options=opts).statements, include_dir, dialect=dialect
|
|
102
|
+
)
|
|
103
|
+
units = {u.name: u for u in parse_units(stmts, dialect=dialect, on_error=cb)}
|
|
104
|
+
if on_error is None and errs:
|
|
105
|
+
raise ParseError("parse error(s):\n" + "\n".join(f" line {ln}: {m}" for ln, m in errs))
|
|
106
|
+
return units
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def run_source(
|
|
110
|
+
text, program=None, dialect=F66, options=None, include_dir=".", setup=None, **kwargs
|
|
111
|
+
):
|
|
112
|
+
"""Parse + run a FORTRAN source string; return the Engine to inspect its state.
|
|
113
|
+
`program` selects the main PROGRAM (defaults to the first program unit). `options`
|
|
114
|
+
is an optional `SourceOptions` for source-recovery handling. `include_dir` is the
|
|
115
|
+
base directory for INCLUDE resolution (default the current directory). `setup` is an
|
|
116
|
+
optional `fn(eng)` called after the engine is built and the runtime/builtins installed,
|
|
117
|
+
but before the program runs -- the place to register OPEN devices, prime COMMON, or do
|
|
118
|
+
any host-side engine setup the program needs."""
|
|
119
|
+
from forterp.runtime import default_terminal_echo, make_engine
|
|
120
|
+
|
|
121
|
+
units = parse_source(text, dialect=dialect, options=options, include_dir=include_dir)
|
|
122
|
+
eng = make_engine(units, dialect=dialect, **kwargs)
|
|
123
|
+
# Default terminal-echo control: unless the caller wired its own `set_echo`, honor the
|
|
124
|
+
# program's ECHOON/ECHOFF on an interactive tty (no-op off a terminal), restored after.
|
|
125
|
+
restore = None
|
|
126
|
+
if "set_echo" not in kwargs:
|
|
127
|
+
set_echo, restore = default_terminal_echo()
|
|
128
|
+
if set_echo is not None:
|
|
129
|
+
eng._set_echo = set_echo
|
|
130
|
+
if setup is not None:
|
|
131
|
+
setup(eng)
|
|
132
|
+
try:
|
|
133
|
+
return eng.run_program(program)
|
|
134
|
+
finally:
|
|
135
|
+
if restore is not None:
|
|
136
|
+
restore()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# Expert namespaces -- the organized API beyond the focused public names above:
|
|
140
|
+
# forterp.frontend (lexer/parser), .format (FORMAT engine), .runtime (Engine + builders),
|
|
141
|
+
# .ast (AST nodes), .hostlib (host-builtin marshalling), .debug (OOB census + tracer).
|
|
142
|
+
from forterp import ast, debug, format, frontend, hostlib, runtime # noqa: E402,F401
|
forterp/__main__.py
ADDED
forterp/ast.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""The AST node types produced by the parser (expert API).
|
|
2
|
+
|
|
3
|
+
A namespace alias for `forterp.ast_nodes`: the node classes the front end builds and the
|
|
4
|
+
engine walks. Useful for tooling that inspects or transforms parsed programs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from forterp.ast_nodes import * # noqa: F403 (re-export the node classes)
|
forterp/ast_nodes.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""AST node definitions for FORTRAN-66 / DEC FORTRAN-10.
|
|
2
|
+
|
|
3
|
+
Expressions and executable statements are dataclasses. Program-unit-level
|
|
4
|
+
declaration information (types, arrays, commons, parameters, data, formats)
|
|
5
|
+
lives on ProgramUnit rather than as statement nodes, because the executor runs
|
|
6
|
+
a flat statement list with a label table + DO-stack (FORTRAN's arbitrary GOTOs
|
|
7
|
+
into/out of "blocks" make a nested-block AST counterproductive).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Optional, Union
|
|
14
|
+
|
|
15
|
+
# --- type aliases: the contracts the parser produces and the engine consumes ------
|
|
16
|
+
# `from __future__ import annotations` keeps field annotations lazy (strings), so these
|
|
17
|
+
# document intent for readers/tooling at no import-time cost. There is no static checker
|
|
18
|
+
# in the build; they are precise documentation, not enforced types.
|
|
19
|
+
Expr = Union[
|
|
20
|
+
"IntLit",
|
|
21
|
+
"RealLit",
|
|
22
|
+
"OctalLit",
|
|
23
|
+
"Complex",
|
|
24
|
+
"StrLit",
|
|
25
|
+
"LogicalLit",
|
|
26
|
+
"Var",
|
|
27
|
+
"Ref",
|
|
28
|
+
"Unary",
|
|
29
|
+
"Binary",
|
|
30
|
+
]
|
|
31
|
+
IoItem = Union[Expr, "ImpliedDo"] # an I/O list element: an expression or an implied-DO
|
|
32
|
+
FormatRef = Union[int, str, None] # FORMAT label, '*' (list-directed), or None (unformatted)
|
|
33
|
+
Dims = list[tuple[int, int]] # array bounds: [(lo, hi), ...]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------- expressions
|
|
37
|
+
@dataclass
|
|
38
|
+
class IntLit:
|
|
39
|
+
value: int
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class RealLit:
|
|
44
|
+
value: float
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class OctalLit:
|
|
49
|
+
value: int
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class Complex: # complex constant (re, im) -- V5 Ch4
|
|
54
|
+
re: Expr
|
|
55
|
+
im: Expr
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class StrLit:
|
|
60
|
+
value: str # raw characters; packed to a word at eval time
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class LogicalLit:
|
|
65
|
+
value: bool
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class Var:
|
|
70
|
+
name: str # scalar variable / bare function name
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class Ref:
|
|
75
|
+
name: str # NAME(args): array element OR function call
|
|
76
|
+
args: list[Expr]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class Unary:
|
|
81
|
+
op: str # 'NOT' | '-' | '+'
|
|
82
|
+
operand: Expr
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class Binary:
|
|
87
|
+
op: str # OR AND EQ NE LT LE GT GE + - * / ^
|
|
88
|
+
left: Expr
|
|
89
|
+
right: Expr
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ----------------------------------------------------------------- statements
|
|
93
|
+
@dataclass
|
|
94
|
+
class Stmt:
|
|
95
|
+
label: Optional[int] = None
|
|
96
|
+
file: str = ""
|
|
97
|
+
line: int = 0
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass
|
|
101
|
+
class Assign(Stmt):
|
|
102
|
+
target: Optional[Expr] = None # Var or Ref
|
|
103
|
+
expr: Optional[Expr] = None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class Goto(Stmt):
|
|
108
|
+
target: int = 0
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class CompGoto(Stmt):
|
|
113
|
+
labels: list[int] = field(default_factory=list)
|
|
114
|
+
index: Optional[Expr] = None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass
|
|
118
|
+
class AssignLabel(Stmt): # ASSIGN <label> TO <var> (tgt avoids Stmt.label)
|
|
119
|
+
tgt: int = 0
|
|
120
|
+
var: str = ""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass
|
|
124
|
+
class AssignedGoto(Stmt): # GO TO <var> [,(label-list)]
|
|
125
|
+
var: str = ""
|
|
126
|
+
labels: list[int] = field(default_factory=list)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass
|
|
130
|
+
class IfLogical(Stmt):
|
|
131
|
+
cond: Optional[Expr] = None
|
|
132
|
+
stmt: Optional[Stmt] = None # embedded statement
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass
|
|
136
|
+
class IfBranch(Stmt):
|
|
137
|
+
"""Arithmetic IF (3 labels) or logical two-way IF (2 labels)."""
|
|
138
|
+
|
|
139
|
+
cond: Optional[Expr] = None
|
|
140
|
+
labels: list[int] = field(default_factory=list)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass
|
|
144
|
+
class Do(Stmt):
|
|
145
|
+
var: str = ""
|
|
146
|
+
start: Optional[Expr] = None
|
|
147
|
+
stop: Optional[Expr] = None
|
|
148
|
+
step: Optional[Expr] = None # may be None -> 1
|
|
149
|
+
term_label: int = 0
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass
|
|
153
|
+
class Continue(Stmt):
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass
|
|
158
|
+
class EntryStmt(Stmt): # ENTRY name(args) -- alternate subprogram entry point
|
|
159
|
+
name: str = ""
|
|
160
|
+
params: list[str] = field(default_factory=list)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass
|
|
164
|
+
class EncDec(Stmt): # ENCODE/DECODE(count, fmt, buf) iolist -- internal fmt I/O
|
|
165
|
+
decode: bool = False
|
|
166
|
+
count: Optional[Expr] = None
|
|
167
|
+
fmt: FormatRef = None
|
|
168
|
+
buf: Optional[Expr] = None
|
|
169
|
+
items: list[IoItem] = field(default_factory=list)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass
|
|
173
|
+
class Call(Stmt):
|
|
174
|
+
name: str = ""
|
|
175
|
+
args: list[Expr] = field(default_factory=list)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass
|
|
179
|
+
class Return(Stmt):
|
|
180
|
+
expr: Optional[Expr] = None # RETURN e -> alternate (multiple) return
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclass
|
|
184
|
+
class LabelArg: # $nnn / *nnn actual arg = alternate-return target
|
|
185
|
+
label: int = 0
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@dataclass
|
|
189
|
+
class StopStmt(Stmt):
|
|
190
|
+
code: Optional[Expr] = None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@dataclass
|
|
194
|
+
class PauseStmt(Stmt): # PAUSE [n] -- print and continue (batch behavior)
|
|
195
|
+
code: Optional[Expr] = None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@dataclass
|
|
199
|
+
class TypeStmt(Stmt): # TYPE fmt, iolist (terminal output)
|
|
200
|
+
fmt: FormatRef = None # label int, or '*'
|
|
201
|
+
items: list[IoItem] = field(default_factory=list)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@dataclass
|
|
205
|
+
class AcceptStmt(Stmt): # ACCEPT fmt, iolist (terminal input)
|
|
206
|
+
fmt: FormatRef = None
|
|
207
|
+
items: list[IoItem] = field(default_factory=list)
|
|
208
|
+
reread: bool = False # REREAD: re-parse the last input record
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@dataclass
|
|
212
|
+
class IoStmt(Stmt): # READ/WRITE (unit[,fmt][,specs]) iolist
|
|
213
|
+
mode: str = "" # 'READ' | 'WRITE'
|
|
214
|
+
unit: Optional[Expr] = None
|
|
215
|
+
fmt: FormatRef = None # label int, '*', or None (unformatted)
|
|
216
|
+
specs: dict[str, object] = field(default_factory=dict) # e.g. {'END': 450}
|
|
217
|
+
items: list[IoItem] = field(default_factory=list)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@dataclass
|
|
221
|
+
class FileCtl(Stmt): # OPEN/CLOSE/REWIND (specs...)
|
|
222
|
+
verb: str = ""
|
|
223
|
+
specs: dict[str, object] = field(default_factory=dict)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@dataclass
|
|
227
|
+
class DefineFile(Stmt): # DEFINE FILE u(m,n,U,v) [,...] (V5 10.3.5)
|
|
228
|
+
defs: list[dict] = field(default_factory=list) # [{unit,maxrec,recsize,assoc}, ...]
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@dataclass
|
|
232
|
+
class ImpliedDo: # io-list element: ( items, var=e1,e2[,e3] )
|
|
233
|
+
items: list[IoItem]
|
|
234
|
+
var: str
|
|
235
|
+
start: Expr
|
|
236
|
+
stop: Expr
|
|
237
|
+
step: Optional[Expr]
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# --------------------------------------------------------------- program unit
|
|
241
|
+
@dataclass
|
|
242
|
+
class ProgramUnit:
|
|
243
|
+
kind: str # 'program' | 'subroutine' | 'function'
|
|
244
|
+
name: str
|
|
245
|
+
params: list[str] = field(default_factory=list)
|
|
246
|
+
ret_type: Optional[str] = None # for typed functions
|
|
247
|
+
implicit: dict[str, str] = field(default_factory=dict) # letter -> type
|
|
248
|
+
types: dict[str, str] = field(default_factory=dict) # name -> type
|
|
249
|
+
arrays: dict[str, Dims] = field(default_factory=dict) # name -> [(lo,hi), ...]
|
|
250
|
+
consts: dict[str, object] = field(default_factory=dict) # PARAMETER name -> value
|
|
251
|
+
commons: list[tuple[str, list]] = field(default_factory=list) # (block, [(name, dims)])
|
|
252
|
+
data: list[tuple] = field(default_factory=list) # (targets, values)
|
|
253
|
+
externals: set[str] = field(default_factory=set)
|
|
254
|
+
formats: dict[int, str] = field(default_factory=dict) # label -> raw format text
|
|
255
|
+
code: list[Stmt] = field(default_factory=list) # executable Stmt list
|
|
256
|
+
labels: dict[int, int] = field(default_factory=dict) # label -> index into code
|
|
257
|
+
stmt_funcs: dict[str, tuple] = field(default_factory=dict) # name -> ([param, ...], expr)
|
|
258
|
+
namelists: dict[str, list] = field(default_factory=dict) # group -> [item nodes] (V5 Ch11)
|
|
259
|
+
equivs: list[list] = field(default_factory=list) # EQUIVALENCE groups [[(name,[subs]),...],...]
|
forterp/cli.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Command-line front-ends over the forterp engine.
|
|
2
|
+
|
|
3
|
+
Three console entry points (declared in pyproject [project.scripts]):
|
|
4
|
+
pyf66 run a source file as strict ANSI FORTRAN-66 (dialect F66)
|
|
5
|
+
pyfortran10 run it as DEC FORTRAN-10 (dialect FORTRAN10 -- the DEC superset)
|
|
6
|
+
forterp general driver; --std selects the dialect (default f66)
|
|
7
|
+
|
|
8
|
+
Each reads a .FOR file, runs its main program, and wires the program's terminal and
|
|
9
|
+
line-printer output to stdout and READ/ACCEPT to stdin. The dialect-named commands are
|
|
10
|
+
thin presets over `forterp` itself -- like g77/gfortran over gcc.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import importlib.util
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
import forterp
|
|
21
|
+
from forterp.forbin import Dec10FloatError
|
|
22
|
+
|
|
23
|
+
_TARGETS = forterp.target.TARGETS
|
|
24
|
+
_DIALECTS = forterp.dialect.DIALECTS
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _load_builtins(paths):
|
|
28
|
+
"""Import each ``.py`` path as a module and collect what it provides: the host routines it
|
|
29
|
+
declares (``{name: fn}``, via `forterp.hostlib.builtins_in`) and any module-level
|
|
30
|
+
``register(eng)`` hook. The file's directory is put on ``sys.path`` so sibling modules can
|
|
31
|
+
import each other; the module executes on import (it is host code, like any plugin loader).
|
|
32
|
+
|
|
33
|
+
Returns ``(table, hooks)``. ``register(eng)`` lets a dropped-in module do engine setup the
|
|
34
|
+
auto-discovered builtins can't express -- register an OPEN device (``eng.register_device``),
|
|
35
|
+
prime COMMON, inject a monitor facade -- and is called after the engine is built.
|
|
36
|
+
|
|
37
|
+
The directory is on ``sys.path`` and the module is in ``sys.modules`` only for the duration
|
|
38
|
+
of loading (so sibling imports resolve by name); both are restored afterward, so loading a
|
|
39
|
+
file named like a stdlib module (``time.py``) doesn't leave it shadowing later imports in an
|
|
40
|
+
in-process caller. The loaded routines keep working -- they hold the module's namespace
|
|
41
|
+
directly, independent of ``sys.modules``."""
|
|
42
|
+
table, hooks = {}, []
|
|
43
|
+
inserted = [] # sys.path dirs we added (remove exactly these afterward)
|
|
44
|
+
saved = {} # modname -> (was_present, prior) so sys.modules can be put back
|
|
45
|
+
try:
|
|
46
|
+
for path in paths:
|
|
47
|
+
directory = os.path.dirname(os.path.abspath(path)) or "."
|
|
48
|
+
if directory not in sys.path:
|
|
49
|
+
sys.path.insert(0, directory)
|
|
50
|
+
inserted.append(directory)
|
|
51
|
+
modname = os.path.splitext(os.path.basename(path))[0]
|
|
52
|
+
if modname not in saved: # record the pre-existing entry once
|
|
53
|
+
saved[modname] = (modname in sys.modules, sys.modules.get(modname))
|
|
54
|
+
spec = importlib.util.spec_from_file_location(modname, path)
|
|
55
|
+
module = importlib.util.module_from_spec(spec)
|
|
56
|
+
sys.modules[modname] = module # so dataclasses / sibling imports resolve by name
|
|
57
|
+
spec.loader.exec_module(module)
|
|
58
|
+
table.update(forterp.hostlib.builtins_in(module))
|
|
59
|
+
hook = getattr(module, "register", None)
|
|
60
|
+
if callable(hook):
|
|
61
|
+
hooks.append(hook)
|
|
62
|
+
return table, hooks
|
|
63
|
+
finally: # un-pollute the global import state (do not leak into an embedding process)
|
|
64
|
+
for directory in inserted:
|
|
65
|
+
try:
|
|
66
|
+
sys.path.remove(directory)
|
|
67
|
+
except ValueError:
|
|
68
|
+
pass
|
|
69
|
+
for modname, (was_present, prior) in saved.items():
|
|
70
|
+
if was_present:
|
|
71
|
+
sys.modules[modname] = prior
|
|
72
|
+
else:
|
|
73
|
+
sys.modules.pop(modname, None)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _run(argv, dialect, prog, *, allow_std, default_target="native"):
|
|
77
|
+
ap = argparse.ArgumentParser(prog=prog, description=__doc__.strip().splitlines()[0])
|
|
78
|
+
ap.add_argument("--version", action="version", version=f"%(prog)s {forterp.__version__}")
|
|
79
|
+
ap.add_argument(
|
|
80
|
+
"file",
|
|
81
|
+
nargs="*",
|
|
82
|
+
help="FORTRAN source file(s) to run (several are linked together by unit name into one "
|
|
83
|
+
"program); any *.py argument is imported and its @fcall/@uuo host routines "
|
|
84
|
+
"registered, and its optional register(eng) hook called for engine setup "
|
|
85
|
+
"(OPEN devices, COMMON priming); omit all for interactive mode",
|
|
86
|
+
)
|
|
87
|
+
ap.add_argument(
|
|
88
|
+
"--target",
|
|
89
|
+
choices=_TARGETS,
|
|
90
|
+
default=default_target,
|
|
91
|
+
help=f"machine value model (default: {default_target} for {prog})",
|
|
92
|
+
)
|
|
93
|
+
ap.add_argument(
|
|
94
|
+
"--program", metavar="NAME", help="main PROGRAM unit to run (default: the first)"
|
|
95
|
+
)
|
|
96
|
+
ap.add_argument(
|
|
97
|
+
"--check",
|
|
98
|
+
action="store_true",
|
|
99
|
+
help="parse and report all diagnostics; do not run (compile-check)",
|
|
100
|
+
)
|
|
101
|
+
ap.add_argument(
|
|
102
|
+
"--recover-shifted-cols",
|
|
103
|
+
action="store_true",
|
|
104
|
+
help="recover statement text reindented past column 72 (off by default -- a faithful "
|
|
105
|
+
"FORTRAN-10 compiler drops cols 73+); for a deck nudged a column or two to the right",
|
|
106
|
+
)
|
|
107
|
+
ap.add_argument(
|
|
108
|
+
"--no-wrap",
|
|
109
|
+
action="store_true",
|
|
110
|
+
help="disable the FORTRAN-10 terminal free-CR-LF wrap at column 80 (TOPS-10 .TONFC); "
|
|
111
|
+
"no effect under strict F66, which never wraps",
|
|
112
|
+
)
|
|
113
|
+
if allow_std:
|
|
114
|
+
ap.add_argument(
|
|
115
|
+
"--std",
|
|
116
|
+
choices=_DIALECTS,
|
|
117
|
+
default="f66",
|
|
118
|
+
help="language dialect (default: f66 = strict ANSI; fortran10 = DEC superset)",
|
|
119
|
+
)
|
|
120
|
+
args = ap.parse_args(argv)
|
|
121
|
+
std = args.std if allow_std else ("fortran10" if dialect is forterp.FORTRAN10 else "f66")
|
|
122
|
+
dialect = _DIALECTS[std]
|
|
123
|
+
options = forterp.SourceOptions(recover_shifted_cols=args.recover_shifted_cols)
|
|
124
|
+
|
|
125
|
+
# *.py args are Python host-routine modules; everything else is FORTRAN source.
|
|
126
|
+
py_files = [p for p in args.file if p.endswith(".py")]
|
|
127
|
+
src_files = [p for p in args.file if not p.endswith(".py")]
|
|
128
|
+
|
|
129
|
+
if not src_files: # no FORTRAN to run
|
|
130
|
+
if py_files:
|
|
131
|
+
ap.error("Python builtin module(s) given but no FORTRAN source to run")
|
|
132
|
+
if args.check:
|
|
133
|
+
ap.error("--check requires a file")
|
|
134
|
+
from forterp.command import CommandProcessor
|
|
135
|
+
|
|
136
|
+
return CommandProcessor(std=std, target=args.target, program=args.program).run()
|
|
137
|
+
|
|
138
|
+
try: # several FORTRAN files are concatenated, then linked together by unit name
|
|
139
|
+
text = "\n".join(open(p, "r", errors="replace").read() for p in src_files)
|
|
140
|
+
except OSError as e:
|
|
141
|
+
ap.error(str(e))
|
|
142
|
+
name = " + ".join(os.path.basename(p) for p in src_files)
|
|
143
|
+
# INCLUDE targets resolve against the (first) source file's directory, not the cwd.
|
|
144
|
+
include_dir = os.path.dirname(src_files[0]) or "."
|
|
145
|
+
|
|
146
|
+
if args.check: # compile-check: list every %FTN diagnostic, don't run
|
|
147
|
+
diags = []
|
|
148
|
+
units = forterp.parse_source(
|
|
149
|
+
text,
|
|
150
|
+
dialect=dialect,
|
|
151
|
+
include_dir=include_dir,
|
|
152
|
+
options=options,
|
|
153
|
+
on_error=lambda st, m: diags.append(m),
|
|
154
|
+
)
|
|
155
|
+
if diags:
|
|
156
|
+
print(f"?{name}: {len(diags)} error(s)", file=sys.stderr)
|
|
157
|
+
for d in diags:
|
|
158
|
+
print(f" {d}", file=sys.stderr)
|
|
159
|
+
return 1
|
|
160
|
+
print(f"[{name}: {len(units)} unit(s) OK]")
|
|
161
|
+
return 0
|
|
162
|
+
|
|
163
|
+
try: # a bad builtins module is a clean ?-diagnostic, not a traceback
|
|
164
|
+
builtins, hooks = _load_builtins(py_files) if py_files else ({}, [])
|
|
165
|
+
except Exception as e:
|
|
166
|
+
print(f"?loading {', '.join(py_files)}: {e}", file=sys.stderr)
|
|
167
|
+
return 1
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
forterp.run_source(
|
|
171
|
+
text,
|
|
172
|
+
program=args.program,
|
|
173
|
+
dialect=dialect,
|
|
174
|
+
options=options,
|
|
175
|
+
include_dir=include_dir,
|
|
176
|
+
target=_TARGETS[args.target],
|
|
177
|
+
builtins=builtins or None, # host routines from the *.py args (after STDLIB)
|
|
178
|
+
setup=(lambda eng: [h(eng) for h in hooks]) if hooks else None, # register(eng) hooks
|
|
179
|
+
emit=sys.stdout.write, # TYPE / terminal output -> stdout
|
|
180
|
+
printer=sys.stdout.write, # line-printer (units 3/6) -> stdout
|
|
181
|
+
readline=sys.stdin.readline, # READ / ACCEPT <- stdin
|
|
182
|
+
tty_autowrap=not args.no_wrap, # FORTRAN-10 free-CR-LF wrap at col 80 unless --no-wrap
|
|
183
|
+
# echo control (ECHOON/ECHOFF) -> run_source's default_terminal_echo on a real tty
|
|
184
|
+
)
|
|
185
|
+
except forterp.ParseError as e:
|
|
186
|
+
print(e, file=sys.stderr)
|
|
187
|
+
return 1
|
|
188
|
+
except (forterp.fmt.InputConversionError, Dec10FloatError) as e:
|
|
189
|
+
# bad numeric field / unrepresentable float in binary I/O, no ERR= -> clean halt
|
|
190
|
+
print(f"?{e}", file=sys.stderr)
|
|
191
|
+
return 1
|
|
192
|
+
except forterp.engine.StopExecution:
|
|
193
|
+
return 0 # explicit STOP: normal termination (run_program also swallows it)
|
|
194
|
+
except (RuntimeError, ValueError, ArithmeticError, RecursionError, OSError, ImportError) as e:
|
|
195
|
+
# any other runtime fault (undefined unit/label/routine, step budget, bad dimension,
|
|
196
|
+
# deep recursion, a file error, or a host .py module whose basename shadows a stdlib
|
|
197
|
+
# import) -> a clean ?-diagnostic, never a raw traceback
|
|
198
|
+
print(f"?{e}", file=sys.stderr)
|
|
199
|
+
return 1
|
|
200
|
+
return 0
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def f66_main(argv=None):
|
|
204
|
+
"""`pyf66`: run a source file as strict ANSI FORTRAN-66."""
|
|
205
|
+
return _run(argv, forterp.F66, "pyf66", allow_std=False)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def f10_main(argv=None):
|
|
209
|
+
"""`pyfortran10`: run a source file as DEC FORTRAN-10 (the DEC superset) on the PDP-10 machine.
|
|
210
|
+
|
|
211
|
+
Defaults to the PDP10 target -- matching the prebuilt `forterp.fortran10` interpreter -- since
|
|
212
|
+
DEC FORTRAN-10 *is* the DECsystem-10 (36-bit words, packed ASCII, .TRUE.=-1); pass
|
|
213
|
+
`--target native` for the DEC language on the portable 64-bit machine instead."""
|
|
214
|
+
return _run(argv, forterp.FORTRAN10, "pyfortran10", allow_std=False, default_target="pdp10")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def main(argv=None):
|
|
218
|
+
"""`forterp`: general driver; `--std f66|fortran10` selects the dialect (default f66)."""
|
|
219
|
+
return _run(argv, forterp.F66, "forterp", allow_std=True)
|