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 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
@@ -0,0 +1,7 @@
1
+ """`python -m forterp ...` -> the general CLI driver (forterp.cli:main)."""
2
+
3
+ import sys
4
+
5
+ from forterp.cli import main
6
+
7
+ sys.exit(main())
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)