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
11 changes: 7 additions & 4 deletions docs/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,8 @@ mccode-antlr SUBCOMMAND [OPTIONS]
| `register` | Mint a pooch registry file (path + sha256 hash per line) from a local directory tree |
| `ir-list` | List component IR cache files (`*.comp.<build>.json`) (`-l` for paths, sizes, stale status) |
| `ir-clean` | Delete component IR cache files (`--stale` for stale-only; `-f` to skip confirmation) |
| `ir-build` | Pre-build component IR cache files for all known registries |
| `build` | Pre-build the C typedef cache, and component IR cache files when enabled, for all known registries |
| `ir-build` | Deprecated spelling of `build` |

**`cache populate` options:**

Expand All @@ -289,14 +290,16 @@ mccode-antlr SUBCOMMAND [OPTIONS]
| `--ext SUFFIX` | Only include files whose name ends with this suffix (repeatable); default includes every file |
| `--recursive` / `--no-recursive` | Recurse into subdirectories of each `DIR` (default) |

**`cache ir-build` options:**
**`cache build` options:**

`cache build` parses only files already on disk; run `cache populate` first. It always warms the C typedef cache. It writes component IR sidecars (`*.comp.<build>.json`) only when the `cache.component_ir` configuration value is true, e.g. `MCCODEANTLR_CACHE__COMPONENT_IR=true`. They are off by default; turn them on for a prebuilt, shared or read-only cache.

| Flag | Description |
|---|---|
| `--flavor {mcstas,mcxtrace,both}` | Which flavor's registries to build IR for (default: `both`) |
| `--tag TAG` | McCode version whose cached components to build (default: the configured version) |
| `--flavor {mcstas,mcxtrace,both,none}` | Which flavor's registries to build (default: `both`); `none` builds only `--registry` ones |
| `-j N`, `--jobs N` | Number of parallel workers (default: `os.cpu_count()`) |
| `--force` | Rebuild even if the IR sidecar is already up-to-date |
| `--download` | Bulk-populate the pooch cache via git clone before building (faster than individual fetches) |
| `-R SPEC`, `--registry SPEC` | Extra registry to include (repeatable); same `SPEC` formats as `registry_from_specification` |

---
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ The `mccode-antlr` management command controls caches and configuration:

```bash
mccode-antlr cache list # list cached component registries
mccode-antlr cache ir-build # pre-build component intermediate representations
mccode-antlr cache build # pre-build the translation caches
mccode-antlr config list # display active configuration
```

Expand Down
2 changes: 1 addition & 1 deletion docs/how-to/registry-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,6 @@ automatically so the component definitions are available.
```bash
mccode-antlr cache list # show all cached registries
mccode-antlr cache populate # download default McStas + McXtrace registries
mccode-antlr cache ir-build # pre-build component IR (speeds up first parse)
mccode-antlr cache build # pre-build translation caches (speeds up first translation)
mccode-antlr cache ir-clean # remove stale IR cache entries
```
243 changes: 146 additions & 97 deletions src/mccode_antlr/cli/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@
- ``register`` – mint a pooch registry file (path + sha256 hash per line)
from a local directory tree.

Component intermediate-representation (IR) cache
-------------------------------------------------
The reader writes a ``{name}.comp.<salt>.json`` file alongside every ``.comp``
file it parses (the ``<salt>`` binds the sidecar to the mccode-antlr build that
wrote it). These files persist across process restarts (acting as a fast disk
cache), but they accumulate and users may wish to inspect or clean them up.
Translation caches
------------------
- ``build`` – pre-build the C typedef cache and, when enabled, the component
IR sidecars (``ir-build`` is its deprecated spelling).

When the ``cache.component_ir`` configuration value is true, the reader writes a
``{name}.comp.<salt>.json`` file alongside every ``.comp`` file it parses (the
``<salt>`` binds the sidecar to the mccode-antlr build that wrote it). These
files persist across process restarts (acting as a fast disk cache), but they
accumulate and users may wish to inspect or clean them up.

- ``ir-list`` – find and list all component IR sidecars under the cache root.
- ``ir-clean`` – delete all (or only stale) component IR sidecars; a sidecar
Expand Down Expand Up @@ -348,7 +352,7 @@ def cache_populate(

Registries named with ``--registry`` are populated too, file by file through
pooch — they have no git-clone shortcut. This is the only command that
downloads; ``cache ir-build`` parses whatever is already on disk.
downloads; ``cache build`` parses whatever is already on disk.

``flavor='none'`` skips the McCode side entirely — no tag resolution, no
clone, no checkout — so populating only ``--registry`` extras costs nothing
Expand Down Expand Up @@ -472,16 +476,10 @@ def _iter_ir_files(root: Path):


def _is_stale(json_path: Path) -> bool:
"""True if *json_path* is unusable: sibling ``.comp`` newer, or it was
written by a different mccode-antlr build."""
from mccode_antlr.reader.reader import component_ir_comp_path, component_ir_is_current
if not component_ir_is_current(json_path):
return True
comp_path = component_ir_comp_path(json_path)
try:
return comp_path.stat().st_mtime_ns > json_path.stat().st_mtime_ns
except OSError:
return False
"""True if *json_path* is unusable: its ``.comp`` (or an INHERIT/COPY parent)
changed, or it was written by a different mccode-antlr build."""
from mccode_antlr.reader.reader import component_ir_is_fresh
return not component_ir_is_fresh(json_path)


def cache_ir_list(long: bool):
Expand Down Expand Up @@ -561,42 +559,69 @@ def _ir_worker_init(registries, flavor, tag=None) -> None:
_IR_READER = Reader(registries=list(registries), flavor=flavor)


def _build_one_ir(comp_path_str: str, force: bool) -> tuple[str, str]:
"""Parse one ``.comp`` file and write its ``.comp.json``.
def _warm_typedefs(comp) -> str | None:
"""Fill the C typedef cache for *comp*; return an error message on failure.

Constructing the C translator for a one-instance instrument parses exactly
what a real translation would for this component -- its SHARE, DECLARE and
USERVARS blocks and every library they ``%include`` -- and the cache is keyed
by the hash of that text, so any instrument using the component then hits.
Only an instrument's own USERVARS are left to parse at translation time.
"""
if _IR_READER is None:
return None
from mccode_antlr import Flavor
from mccode_antlr.assembler import Assembler
from mccode_antlr.translators.c import CTargetVisitor
flavor = _IR_READER.flavor or Flavor.MCSTAS
try:
assembler = Assembler('typedefs', registries=list(_IR_READER.registries), flavor=flavor)
# this exact Comp, not whichever same-named one the registries rank first
assembler.reader.components[comp.name] = comp
assembler.component('c', comp.name)
CTargetVisitor(assembler.instrument, flavor=flavor)
except Exception as exc:
return f'{type(exc).__name__}: {exc}'.split('\n')[0]
return None


def _build_one_ir(comp_path_str: str, force: bool) -> tuple[str, str, str | None]:
"""Parse one ``.comp`` file, write its ``.comp.json``, and warm the C
typedef cache for it (see :func:`_warm_typedefs`).

Module-level so it is picklable by ``ProcessPoolExecutor``.

Returns ``(comp_path_str, status)`` where *status* is one of
``'hit'``, ``'built'``, or ``'error: <message>'``.
Returns ``(comp_path_str, status, typedef_error)`` where *status* is one of
``'hit'``, ``'built'``, or ``'error: <message>'``, and *typedef_error* is
None unless warming the typedef cache failed.
"""
from pathlib import Path
from mccode_antlr.reader.reader import component_cache, make_reader_error_listener
from mccode_antlr.reader.reader import (
component_cache, component_ir_enabled, component_ir_is_fresh, component_ir_path,
make_reader_error_listener, recording_dependencies,
)
from mccode_antlr.comp import Comp
from mccode_antlr.grammar import McComp_ErrorListener

from mccode_antlr.reader.reader import component_ir_path

comp_path = Path(comp_path_str)
json_path = component_ir_path(comp_path)

if not force:
try:
if json_path.exists() and json_path.stat().st_mtime_ns >= comp_path.stat().st_mtime_ns:
return (comp_path_str, 'hit')
except OSError:
pass
if not force and component_ir_enabled() and component_ir_is_fresh(component_ir_path(comp_path)):
comp = component_cache.get(comp_path)
if comp is not None:
return (comp_path_str, 'hit', _warm_typedefs(comp))

try:
from mccode_antlr.common.encoding import read_source_text
source = read_source_text(comp_path)
error_listener = make_reader_error_listener(
McComp_ErrorListener, 'Component', comp_path.stem, source
)
comp = Comp.from_source(_IR_READER, error_listener, source, str(comp_path), str(comp_path))
component_cache.put(comp_path, comp)
return (comp_path_str, 'built')
with recording_dependencies() as depends:
comp = Comp.from_source(_IR_READER, error_listener, source, str(comp_path), str(comp_path))
component_cache.put(comp_path, comp, depends)
except Exception as exc:
return (comp_path_str, f'error: {exc}')
return (comp_path_str, f'error: {exc}', None)
return (comp_path_str, 'built', _warm_typedefs(comp))


_REGISTRY_SPEC_FORMS = (
Expand Down Expand Up @@ -687,10 +712,19 @@ def _collect_comp_paths(registries) -> tuple[list[Path], int]:
return paths, missing


def cache_ir_build(**kwargs):
"""Deprecated spelling of :func:`cache_build`."""
import warnings
from mccode_antlr.utils import McCodeAntlrDeprecationWarning
warnings.warn('`mccode-antlr cache ir-build` is deprecated since 0.36.0; '
'use `mccode-antlr cache build`', McCodeAntlrDeprecationWarning, stacklevel=2)
cache_build(**kwargs)


def _registries_for_flavors(flavor: str, registry: list[str] | None) -> tuple[list, list]:
"""Default registries for *flavor* plus any ``--registry`` extras.

Returns ``(flavors, registries)``; both ``populate`` and ``ir-build`` need the
Returns ``(flavors, registries)``; both ``populate`` and ``build`` need the
same set, so they resolve it the same way.
"""
from mccode_antlr import Flavor
Expand All @@ -715,9 +749,10 @@ def _registries_for_flavors(flavor: str, registry: list[str] | None) -> tuple[li
return flavors, registries


def cache_ir_build(flavor: str, jobs: int, force: bool, registry: list[str] | None,
tag: str | None = None):
"""Pre-build component IR (``.comp.json``) files for all known registries.
def cache_build(flavor: str, jobs: int, force: bool, registry: list[str] | None,
tag: str | None = None):
"""Pre-build component IR (``.comp.json``) files for all known registries,
and warm the C typedef cache with each component's C blocks and libraries.

Builds IR from files already on disk and downloads nothing: ``cache populate``
is the command that fills the caches, including any ``--registry`` extras.
Expand Down Expand Up @@ -755,12 +790,30 @@ def cache_ir_build(flavor: str, jobs: int, force: bool, registry: list[str] | No

n = len(comp_paths)
print(f'Found {n} .comp file(s). Building IR cache …', flush=True)
from mccode_antlr.reader.reader import component_ir_enabled
if not component_ir_enabled():
print(' Component IR sidecars are disabled (set cache.component_ir, e.g. '
'MCCODEANTLR_CACHE__COMPONENT_IR=true, to write them); warming the C '
'typedef cache only.', flush=True)

if jobs is None:
jobs = os.cpu_count() or 1

worker = partial(_build_one_ir, force=force)
built = hits = errors = 0
typedef_errors = []

def tally(path, status, typedef_error):
nonlocal built, hits, errors
if status == 'built':
built += 1
elif status == 'hit':
hits += 1
else:
errors += 1
print(f' {path.name}: {status}', flush=True)
if typedef_error is not None:
typedef_errors.append(f' {path.name}: {typedef_error}')

# The Reader that resolves INHERIT/COPY parents is built per process: once
# here for the serial path, once per worker via the pool's initializer.
Expand All @@ -785,33 +838,60 @@ def cache_ir_build(flavor: str, jobs: int, force: bool, registry: list[str] | No
if jobs == 1:
_ir_worker_init(*reader_args)
for path in comp_paths:
_, status = worker(str(path))
if status == 'built':
built += 1
elif status == 'hit':
hits += 1
else:
errors += 1
print(f' {path.name}: {status}', flush=True)
tally(path, *worker(str(path))[1:])
else:
with ProcessPoolExecutor(
max_workers=jobs, initializer=_ir_worker_init, initargs=reader_args,
) as pool:
futures = {pool.submit(_build_one_ir, str(p), force): p for p in comp_paths}
for future in as_completed(futures):
_, status = future.result()
if status == 'built':
built += 1
elif status == 'hit':
hits += 1
else:
errors += 1
print(f' {futures[future].name}: {status}', flush=True)
tally(futures[future], *future.result()[1:])

print(
f'\nDone. {built} built, {hits} already up-to-date, {errors} error(s).',
flush=True,
)
if typedef_errors:
print(f'{len(typedef_errors)} component(s) could not warm the C typedef cache '
'(their instruments will parse those blocks at translation time):', flush=True)
print('\n'.join(typedef_errors), flush=True)


def _add_build_parser(actions, name, action, help_text):
ib = actions.add_parser(name=name, help=help_text)
ib.add_argument(
'--tag', default=None,
help=(
'McCode version tag whose cached components to build IR for '
'(e.g. v3.5.31, latest); defaults to the currently-configured version. '
'The pooch caches are per version, so this picks between them when '
'several have been populated.'
),
)
ib.add_argument(
'--flavor', default='both', choices=['mcstas', 'mcxtrace', 'both', 'none'],
help=(
"Which flavor's registries to build IR for (default: both). Use 'none' "
"to build only the registries given with --registry."
),
)
ib.add_argument(
'-j', '--jobs', type=int, nargs='?', default=None, const=None, metavar='N',
help='Number of parallel workers; bare -j or omitted means os.cpu_count()',
)
ib.add_argument(
'--force', action='store_true',
help='Rebuild even if the IR sidecar is already up-to-date',
)
ib.add_argument(
'-R', '--registry', action='append', default=None, metavar='SPEC',
help=(
'Extra registry to build IR for (repeatable). Only files already on disk '
'are built: pass the same specification to `cache populate` first to '
'download them. ' + _REGISTRY_SPEC_FORMS.replace('\n', ' ')
),
)
ib.set_defaults(action=action)


def add_cache_management_parser(modes):
Expand Down Expand Up @@ -907,53 +987,22 @@ def add_cache_management_parser(modes):
)
ic.add_argument(
'--stale', action='store_true',
help='Only remove sidecars whose sibling .comp is newer, or that a different build wrote',
help='Only remove sidecars whose .comp (or an INHERIT/COPY parent) changed, or that a different build wrote',
)
ic.add_argument('-f', '--force', action='store_true',
help='Skip confirmation prompt')
ic.set_defaults(action=cache_ir_clean)

# -- ir-build --
ib = actions.add_parser(
name='ir-build',
help=(
'Pre-build component IR cache sidecars from the .comp files already on '
'disk (downloads nothing — see `cache populate`)'
),
)
ib.add_argument(
'--tag', default=None,
help=(
'McCode version tag whose cached components to build IR for '
'(e.g. v3.5.31, latest); defaults to the currently-configured version. '
'The pooch caches are per version, so this picks between them when '
'several have been populated.'
),
)
ib.add_argument(
'--flavor', default='both', choices=['mcstas', 'mcxtrace', 'both', 'none'],
help=(
"Which flavor's registries to build IR for (default: both). Use 'none' "
"to build only the registries given with --registry."
),
)
ib.add_argument(
'-j', '--jobs', type=int, nargs='?', default=None, const=None, metavar='N',
help='Number of parallel workers; bare -j or omitted means os.cpu_count()',
)
ib.add_argument(
'--force', action='store_true',
help='Rebuild even if the IR sidecar is already up-to-date',
)
ib.add_argument(
'-R', '--registry', action='append', default=None, metavar='SPEC',
help=(
'Extra registry to build IR for (repeatable). Only files already on disk '
'are built: pass the same specification to `cache populate` first to '
'download them. ' + _REGISTRY_SPEC_FORMS.replace('\n', ' ')
),
)
ib.set_defaults(action=cache_ir_build)
# -- build (and its deprecated spelling, ir-build) --
for name, action, help_text in (
('build', cache_build,
'Pre-build the translation caches -- the C typedef cache, and component IR '
'sidecars when cache.component_ir is enabled -- from the .comp files already '
'on disk (downloads nothing — see `cache populate`)'),
('ir-build', cache_ir_build, 'Deprecated spelling of `cache build`'),
):
_add_build_parser(actions, name, action, help_text)


# -- register --
reg = actions.add_parser(
Expand Down
Loading
Loading