Skip to content

Load a file written against an earlier format version - #43

Merged
fedonman merged 4 commits into
format-version-from-libraryfrom
format-migrations
Sep 8, 2026
Merged

Load a file written against an earlier format version#43
fedonman merged 4 commits into
format-version-from-libraryfrom
format-migrations

Conversation

@fedonman

@fedonman fedonman commented Sep 7, 2026

Copy link
Copy Markdown
Member

A file written against an earlier version now loads instead of being refused. A release that changes a syntax registers one rewrite under its own version with qp.register_migration, and reading applies every rewrite between the version in the file's header and the running one, oldest first, to the lines in memory; the file on disk is untouched. A release that breaks nothing registers nothing, so an older file with no rewrite behind it loads as it is. A rewrite works line for line and may not add or drop one, which is what keeps a ParseError's line number and every source_map entry naming a line of the file its author opened. .wfl reads the same way, from its own table, since a program body and a library entry are not the same text. The require <vendor> line follows the rule too, against the extension's version instead of the library's, so an extension that renames an operation registers a rewrite with qp.register_vendor_migration and the files its users already have keep loading.

Both readers now take a version to be exactly major.minor and refuse anything above the running one. A newer file used to load when its major matched; nothing runs backwards, so it is refused. A patch or a bare major in a header is refused as well, since a patch release changes code and never the format — the grammar's header terminal already said so, and the hand-written parser now agrees with it.

Sits on top of #42.

A release that changes a syntax registers one migration under its own
version, and reading applies every migration between the version in the
header and the running one, oldest first, to the lines in memory. The
file on disk is untouched, and writing it back out writes today's
version. A release that breaks nothing registers nothing, so the chain
carries an entry per breaking change rather than per release, and an
older file with nothing behind it loads as it is.

A migration rewrites lines one for one and may not add or drop any,
which is what keeps a ParseError's line number and every source_map
entry naming a line of the file its author opened. The runner refuses
one that breaks the count and names it.

Both formats read this way. .qp and .wfl carry the same number, each
being the library version cut to major.minor, so one running version
bounds both chains, but the rewrites stay apart: `"pi" = Square(...)` in
a library and `play "drive" Square(...)` in a program are not the same
line. `file_format` picks the table, and a change to vocabulary the two
share is one rewrite registered twice.

Either reader now takes a header version to be exactly major.minor and
refuses anything above the running one. A newer file used to load when
its major matched; nothing runs backwards, so it is refused. A patch or
a bare major in a header is refused too: a patch release changes code,
never the format. The grammar's header terminal already said so, and the
hand-written parser now agrees with it. `require` lines are untouched.
`parse_file_version` joins `parse_major_minor` in `qprogram/_version.py`,
the strict reading for a file and the tolerant one for a package.
A `require <vendor>` line names a wire form, so it is read the way a
header is: exactly major.minor, anything the installed extension cannot
provide refused, anything older accepted. `register_vendor_migration`
gives the extension the same mechanism the format has, keyed to its own
version, and `_check_vendor_compat` runs that chain over the lines
before the body is read.

Two things change for a file. A line carrying a patch is refused rather
than rounded down, since a patch release of an extension has no wire
form of its own. A line naming an earlier major is migrated rather than
refused, which is what makes renaming an operation survivable, and it
merges `major versions must match` and `minor version too old` into one
message that says which version to install.

The ceiling for a vendor chain is the installed extension rather than
the library, so `migrate_vendor_lines` takes it as an argument, and the
apply loop the two runners share moves into `_apply`. What
`register_vendor_version` accepts is untouched: that is a package
version, patch and all.
@pullpo-for-slack

Copy link
Copy Markdown

🔗Pullpo.io Slack PR-Channel

@pullpo-for-slack

pullpo-for-slack Bot commented Sep 7, 2026

Copy link
Copy Markdown

AI Analysis

Purpose of the changes:

The changes introduce a comprehensive migration system that allows older file formats to be upgraded to the current version seamlessly, ensuring backward compatibility while reading files written against earlier syntax versions.

Detailed file changes

(dropdown):
  • In file src/qprogram/serialization/migrations.py:

    • Introduced functions for registering migrations for both standard and vendor-specific file formats.
    • Implemented the migration logic that allows files written against older versions to be upgraded to the current version during parsing.
    • Ensured that migrations preserve the line count of the original files to maintain accurate error reporting.
  • In file src/qprogram/serialization/parser.py:

    • Updated the loads function to include migration logic that processes files with older format versions before parsing.
    • Added error handling for unsupported major versions and malformed file versions that now raises appropriate exceptions.
  • In file src/qprogram/waveform_library.py:

    • Implemented migration handling for waveform library files, allowing older versions to be upgraded similarly to standard files.
    • Added checks to ensure that unsupported versions are rejected with clear error messages.
  • In file tests/test_migrations.py:

    • Created a comprehensive suite of tests to validate the migration functionality, covering various scenarios including valid and invalid versions.
    • Ensured that the tests check for correct error handling and the preservation of line counts during migrations.
  • In file tests/test_parser.py:

    • Updated tests to validate the new behavior of the loads function in handling migrations for older file formats.
    • Added tests to ensure that newer versions are properly rejected and that invalid version formats raise appropriate errors.

A `pytest.raises` block that calls the helper building its input has two
calls that could raise, and only one of them is the point.
@pullpo-for-slack

Copy link
Copy Markdown

AI Analysis

of the newly committed changes

Purpose of the changes:

Refactor test cases to enhance readability and maintainability by reducing code duplication.

Detailed file changes

(dropdown):
  • In file tests/test_migrations.py:
    • Introduced the variable 'ahead' to simplify the construction of version strings in 'test_a_newer_file_is_refused'.
    • Replaced repeated version string construction in 'test_a_newer_file_is_refused' with the 'ahead' variable.
    • Created the variable 'text' to avoid code duplication in 'test_a_migration_ahead_of_its_release_is_left_out' and 'test_a_migration_that_adds_a_line_is_refused'.

@pullpo-for-slack

Copy link
Copy Markdown

AI Analysis

of the newly committed changes

Purpose of the changes:

The changes introduce detailed documentation on handling migrations for waveform constructors and vendor operations, ensuring backward compatibility for older files in the serialization system.

Detailed file changes

(dropdown):
  • In file docs/developer/serialization-internals.md:

    • Added a section on how to add a migration for waveform constructor renaming, including code examples.
    • Clarified the use of decorators for registering migrations and their implications on different file formats.
    • Provided detailed explanations of test cases to ensure migrations work as intended.
  • In file docs/developer/vendor-extensions.md:

    • Introduced a new section explaining how older files load with the new required arguments for vendor operations.
    • Included code examples demonstrating how to implement vendor migrations and the testing of these migrations.
  • In file docs/developer/contributing.md:

    • Updated the contributing guide to clarify the need for migrations when grammar changes stop existing files from parsing.
    • Linked to relevant sections of documentation to assist developers in understanding migration requirements.

@sonarqubecloud

sonarqubecloud Bot commented Sep 8, 2026

Copy link
Copy Markdown

@flavie-lebars flavie-lebars left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

@fedonman
fedonman merged commit 1bceb82 into main Sep 8, 2026
9 checks passed
@fedonman
fedonman deleted the format-migrations branch September 8, 2026 09:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants