From f9a9361f91bb0e77dce79bad915d3429fec0d294 Mon Sep 17 00:00:00 2001 From: Carter Stach Date: Mon, 8 Jun 2026 17:13:50 -0400 Subject: [PATCH 1/3] Update project configuration and documentation - Added `.venv/` to `.gitignore` to exclude virtual environment files. - Updated installation instructions in `contributors.md` to use `uv` for setting up the environment and running tests. - Enhanced release notes in `contributors.md` to clarify the automated release workflow and manual publishing steps. - Refined the CI workflow in `.github/workflows/ci.yml` to utilize `uv` for dependency installation and test execution. - Improved function scheduling in `compile_interpreter.py` to manage function generation more effectively. - Added version normalization for release tags in `check_release_versions.py` to ensure consistency between tag and project versions. --- .github/workflows/ci.yml | 9 +- .github/workflows/release.yml | 74 ++++++++++++++ .gitignore | 1 + contributors.md | 17 ++-- .../compiler/builtin_functions.py | 2 + .../compiler/compile_interpreter.py | 43 ++++++-- scripts/check_release_versions.py | 33 ++++++- tests/test_tree_shaking.py | 97 +++++++++++++++++++ todo.md | 44 --------- uv.lock | 29 ++++++ 10 files changed, 288 insertions(+), 61 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 tests/test_tree_shaking.py create mode 100644 uv.lock diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f716cb7..e129f44 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,14 +22,19 @@ jobs: with: python-version: ${{ matrix.python-version }} + - name: Set up uv + uses: astral-sh/setup-uv@v5 + - name: Install dependencies - run: pip install -e . -r requirements-dev.txt + run: | + uv venv + uv pip install -r requirements.txt -r requirements-dev.txt -e . - name: Check release versions run: python scripts/check_release_versions.py - name: Run tests - run: pytest -q + run: uv run pytest -q highlighter: runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..422c43c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,74 @@ +name: Release + +on: + push: + tags: + - "v*" + +permissions: + contents: write + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Set up uv + uses: astral-sh/setup-uv@v5 + + - name: Install dependencies + run: | + uv venv + uv pip install -r requirements.txt -r requirements-dev.txt -e . + + - name: Check release versions + run: python scripts/check_release_versions.py --tag "${{ github.ref_name }}" + + - name: Run tests + run: uv run pytest -q + + publish-npm: + needs: verify + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + registry-url: "https://registry.npmjs.org" + + - name: Verify npm package version matches tag + run: | + TAG_VERSION="${GITHUB_REF_NAME#v}" + NPM_VERSION="$(node -p "require('./npm/package.json').version")" + if [ "$TAG_VERSION" != "$NPM_VERSION" ]; then + echo "Tag version ($TAG_VERSION) does not match npm/package.json ($NPM_VERSION)" + exit 1 + fi + + - name: Publish to npm + working-directory: npm + run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + github-release: + needs: verify + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Create GitHub release + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true diff --git a/.gitignore b/.gitignore index 9ea02c8..40114b9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea/ +.venv/ twine_upload setup.py test_file.py diff --git a/contributors.md b/contributors.md index bb59808..357a47c 100644 --- a/contributors.md +++ b/contributors.md @@ -9,7 +9,8 @@ This fork is maintained at [SpyC0der77/Minecraft-Script](https://github.com/SpyC ```commandline git clone https://github.com/SpyC0der77/Minecraft-Script.git cd Minecraft-Script -pip install -e . -r requirements-dev.txt +uv venv +uv pip install -r requirements.txt -r requirements-dev.txt -e . ``` 2. Confirm the CLI works: @@ -33,7 +34,7 @@ Open `highlighter/` in VS Code and press `F5` to launch an Extension Development From the repository root: ```commandline -pytest -q +uv run pytest -q ``` CI runs the same suite on Python 3.10 and 3.12, plus a highlighter build check. @@ -82,7 +83,11 @@ git push origin main git push origin v0.3.5 ``` -Create a GitHub release from the tag on [SpyC0der77/Minecraft-Script/releases](https://github.com/SpyC0der77/Minecraft-Script/releases). Use the tag name as the release title (for example `v0.3.5`). +Pushing a `v*` tag triggers the [Release workflow](.github/workflows/release.yml). It verifies aligned versions, runs tests, creates a GitHub release with generated notes, and publishes the npm package. + +Add an `NPM_TOKEN` repository secret (npm access token with publish rights) before the first automated publish. + +You can still create or edit releases manually on [SpyC0der77/Minecraft-Script/releases](https://github.com/SpyC0der77/Minecraft-Script/releases) if needed. The npm wrapper installs the matching Python package with: @@ -94,7 +99,7 @@ So the GitHub tag must exist before users install that npm version. ### Publish to npm -From the `npm/` directory: +The Release workflow publishes automatically when a matching `v*` tag is pushed. To publish manually from the `npm/` directory: ```commandline cd npm @@ -115,8 +120,8 @@ npm install -g minecraft-script - [ ] `pyproject.toml` and `npm/package.json` versions match - [ ] `python scripts/check_release_versions.py` passes - [ ] Tag pushed (`v`) -- [ ] GitHub release created from the tag -- [ ] `npm publish` from `npm/` succeeded +- [ ] Release workflow succeeded (GitHub release + npm publish) +- [ ] Or, if publishing manually: GitHub release created and `npm publish` from `npm/` succeeded The VS Code extension (`highlighter/`) uses publisher **`SpyC0der77`** and is published to the Marketplace separately — see [Publishing the VS Code extension](#publishing-the-vs-code-extension). diff --git a/minecraft_script/compiler/builtin_functions.py b/minecraft_script/compiler/builtin_functions.py index a68c53d..66f0fcb 100644 --- a/minecraft_script/compiler/builtin_functions.py +++ b/minecraft_script/compiler/builtin_functions.py @@ -330,6 +330,8 @@ def give_clickable_item(interpreter, args, context) -> function_output: name: MCSString = args[1] if len(args) > 1 else None custom_model_data: MCSNumber = args[2] if len(args) > 2 else None + interpreter.schedule_function_generation(click_function) + click_function_id = interpreter.click_item_lookup.get(click_function.name) if click_function_id is None: click_function_id = ( diff --git a/minecraft_script/compiler/compile_interpreter.py b/minecraft_script/compiler/compile_interpreter.py index 1b37e9e..9682473 100644 --- a/minecraft_script/compiler/compile_interpreter.py +++ b/minecraft_script/compiler/compile_interpreter.py @@ -134,12 +134,37 @@ def __init__(self, datapack_id): self.version = get_version_context() self.commands = CompileCommands() self.used_context_ids = set() - self.functions_to_generate = set() + self.defined_functions: dict[str, MCSFunction] = {} + self.generated_functions: set[MCSFunction] = set() + self.pending_function_generation: list[MCSFunction] = [] self.click_item_lookup = dict() self.used_math_builtins = set() self.used_builtin_functions = set() self.scoreboard_event_functions = [] + def schedule_function_generation(self, function: MCSFunction) -> None: + if function.body is None: + return + if function in self.generated_functions: + return + if function not in self.pending_function_generation: + self.pending_function_generation.append(function) + + def generate_scheduled_functions(self) -> None: + for entry_name in ("init", "main", "kill"): + function = self.defined_functions.get(entry_name) + if function is not None: + self.schedule_function_generation(function) + for function in self.scoreboard_event_functions: + self.schedule_function_generation(function) + + while self.pending_function_generation: + function = self.pending_function_generation.pop(0) + if function in self.generated_functions: + continue + self.generated_functions.add(function) + function.generate_function(self) + def get_scoreboard_event_hooks(self) -> list[dict[str, str]]: hooks = [] for index, function in enumerate(self.scoreboard_event_functions): @@ -206,9 +231,10 @@ def visit_DefineFunctionNode(self, node, context: CompileContext) -> CompileResu if SCOREBOARD_CRITERIA_PATTERN.fullmatch(event_criteria) is None: raise ValueError(f"Invalid scoreboard criteria {event_criteria!r}") function = MCSFunction(context.get_function_name(fnc_name), fnc_body, fnc_parameter_names, context, event_criteria) - self.functions_to_generate.add(function) + self.defined_functions[function.name] = function if event_criteria is not None: self.scoreboard_event_functions.append(function) + self.schedule_function_generation(function) context.declare(fnc_name, function) return CompileResult(function) def visit_VariableDeclareNode(self, node, context: CompileContext) -> CompileResult: @@ -459,11 +485,13 @@ def visit_ImportNode(self, node, context: CompileContext) -> CompileResult: def visit_FunctionCallNode(self, node, context: CompileContext) -> CompileResult: fnc: MCSFunction = self.visit(node.get_root(), context).get_value() arguments: tuple[mcs_type, ...] = tuple(map(lambda x: self.visit(x, context).get_value(), node.get_arguments())) - commands, return_value = fnc.call(self, arguments, context) - is_builtin = ( - hasattr(fnc.call, "__module__") and - fnc.call.__module__.endswith(".builtin_functions") + is_builtin = ( + hasattr(fnc.call, "__module__") + and fnc.call.__module__.endswith(".builtin_functions") ) + if not is_builtin and fnc.body is not None: + self.schedule_function_generation(fnc) + commands, return_value = fnc.call(self, arguments, context) if is_builtin: self.used_builtin_functions.add(fnc.call.__name__) if commands is not None: @@ -533,8 +561,7 @@ def _mcs_compile(ast, functions_dir: str, datapack_id): context = CompileContext('init', top_level=True) interpreter = CompileInterpreter(datapack_id) interpreter.visit(ast, context) - for mcs_fnc in interpreter.functions_to_generate: - mcs_fnc.generate_function(interpreter) + interpreter.generate_scheduled_functions() version = get_version_context() for context_id in interpreter.used_context_ids: commands = [] diff --git a/scripts/check_release_versions.py b/scripts/check_release_versions.py index 0d66055..fc1b144 100644 --- a/scripts/check_release_versions.py +++ b/scripts/check_release_versions.py @@ -36,7 +36,26 @@ def read_npm_version() -> str: return data["version"] -def main() -> int: +def normalize_tag_version(tag: str) -> str: + if not tag.startswith("v"): + raise SystemExit(f"Release tag must start with 'v', got {tag!r}") + return tag[1:] + + +def main(argv: list[str] | None = None) -> int: + args = list(sys.argv[1:] if argv is None else argv) + tag = None + if "--tag" in args: + index = args.index("--tag") + try: + tag = args[index + 1] + except IndexError: + raise SystemExit("Missing value for --tag") from None + del args[index:index + 2] + + if args: + raise SystemExit(f"Unknown arguments: {' '.join(args)}") + pyproject_version = read_pyproject_version() npm_version = read_npm_version() @@ -49,6 +68,18 @@ def main() -> int: ) return 1 + if tag is not None: + tag_version = normalize_tag_version(tag) + if tag_version != pyproject_version: + print( + "Tag version mismatch:\n" + f" git tag: {tag_version}\n" + f" pyproject.toml: {pyproject_version}\n" + f" npm/package.json: {npm_version}", + file=sys.stderr, + ) + return 1 + print(f"Release versions aligned at {pyproject_version}") return 0 diff --git a/tests/test_tree_shaking.py b/tests/test_tree_shaking.py new file mode 100644 index 0000000..0dd28e5 --- /dev/null +++ b/tests/test_tree_shaking.py @@ -0,0 +1,97 @@ +def test_unused_user_functions_are_not_generated(compile_datapack): + source = """ +function used() { + log("hello"); +} + +function unused() { + log("never called"); +} + +function init() { + used(); +} + +function main() { +} +""" + + datapack = compile_datapack(source, "Test Tree Shake Unused") + + assert datapack.has_function("user_functions", "used.mcfunction") + assert datapack.has_function("user_functions", "init.mcfunction") + assert not datapack.has_function("user_functions", "unused.mcfunction") + + +def test_transitive_reachability_keeps_called_helpers(compile_datapack): + source = """ +function helper() { + log("helper"); +} + +function entry() { + helper(); +} + +function init() { + entry(); +} + +function main() { +} +""" + + datapack = compile_datapack(source, "Test Tree Shake Transitive") + + assert datapack.has_function("user_functions", "entry.mcfunction") + assert datapack.has_function("user_functions", "helper.mcfunction") + + +def test_scoreboard_event_functions_are_always_generated(compile_datapack): + source = """ +function reward_miner() on "minecraft.mined:minecraft.diamond_ore" { + log("reward"); +} + +function unused_helper() { + log("unused"); +} + +function init() { + command("# tree shake event init"); +} + +function main() { + command("# tree shake event main"); +} +""" + + datapack = compile_datapack(source, "Test Tree Shake Event") + + assert datapack.has_function("user_functions", "reward_miner.mcfunction") + assert not datapack.has_function("user_functions", "unused_helper.mcfunction") + + +def test_click_handler_functions_are_generated_when_referenced(compile_datapack): + source = """ +function use_item() { + command("say clicked"); +} + +function unused_click_handler() { + command("say unused"); +} + +function init() { + @a give_clickable_item(use_item, "Hello Stick", 12); +} + +function main() { +} +""" + + datapack = compile_datapack(source, "Test Tree Shake Click") + + assert datapack.has_function("user_functions", "use_item.mcfunction") + assert datapack.has_function("clickable_items", "0.mcfunction") + assert not datapack.has_function("user_functions", "unused_click_handler.mcfunction") diff --git a/todo.md b/todo.md index a8a1595..16b809c 100644 --- a/todo.md +++ b/todo.md @@ -1,45 +1 @@ # MCS Todo list - -## Infrastructure -- [x] Add GitHub Actions CI (pytest + example compiles to `build_test/`) -- [x] Enable GitHub Issues on the fork for user bug reports -- [x] Add release automation notes or scripts for npm + GitHub tags - -## Developer Experience -- [x] Align `config set default_output_path` with `compile` (auto-create missing directories) -- [x] Publish VS Code extension (`highlighter/`) to the Marketplace -- [x] Add highlighter CI build check - -## Language / Compiler -- [ ] Track Minecraft version updates and refresh version profiles as needed -- [x] Expand test coverage for lexer/parser edge cases - -## Documentation -- [x] Finish syntax documentation -- [x] Finish data types documentation -- [x] Add a contributor guide (local setup, test compile to `build_test/`, release flow) - -## Completed (v0.3.x) - -### Lexer -- [x] Add async while loop (keyword) -- [x] Make entity selector work in lexer instead of parser -- [x] Fix entity selector not supporting spaces - -### Parser -- [x] Add async while loop (variant to normal while loop) -- [x] Update entity selector - -### Interpreter -- [x] Add async while loop (exact same as normal while loop) - -### Compiler -- [x] Add async while loop (uses a function subscribed to tick.mcfunction) - -### Shell Commands -- [x] Update ``compile`` command - - [x] Change args to \ \[\] \[\] - - [x] Added errors in case a path doesn't exist -- [x] Add config for verbose option -- [x] Add config for default output path -- [x] Add ``config default`` command to reset config to default (needs confirmation) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..e182924 --- /dev/null +++ b/uv.lock @@ -0,0 +1,29 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "minecraft-script" +version = "0.3.5" +source = { editable = "." } +dependencies = [ + { name = "pybars3" }, +] + +[package.metadata] +requires-dist = [{ name = "pybars3", specifier = ">=0.9.7" }] + +[[package]] +name = "pybars3" +version = "0.9.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pymeta3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/1a/2fb847db017f9f89ab8519d96e35fb3dacb6170a0643fddba3b366af0af1/pybars3-0.9.7.tar.gz", hash = "sha256:6ac847e905e53b9c5b936af112c910475e27bf767f79f4528c16f9af1ec0e252", size = 29203, upload-time = "2019-11-05T09:45:24.07Z" } + +[[package]] +name = "pymeta3" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/af/409edba35fc597f1e386e3860303791ab5a28d6cc9a8aecbc567051b19a9/PyMeta3-0.5.1.tar.gz", hash = "sha256:18bda326d9a9bbf587bfc0ee0bc96864964d78b067288bcf55d4d98681d05bcb", size = 29566, upload-time = "2015-02-22T16:30:06.858Z" } From 0ecf8348677a828ef231b26a1ffa451213eed827 Mon Sep 17 00:00:00 2001 From: Carter Stach Date: Mon, 8 Jun 2026 17:35:31 -0400 Subject: [PATCH 2/3] Update CI and release workflows for improved configuration - Added permissions for content access in CI and release workflows. - Updated action versions for `checkout`, `setup-python`, `setup-node`, and `action-gh-release` to specific commits for stability. - Enhanced `ci.yml` to ensure proper content permissions for pull requests. --- .github/workflows/ci.yml | 5 ++++- .github/workflows/release.yml | 23 +++++++++++++---------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e129f44..8d560cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [main] +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest @@ -23,7 +26,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Set up uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5 - name: Install dependencies run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 422c43c..d550262 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,22 +5,21 @@ on: tags: - "v*" -permissions: - contents: write - jobs: verify: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.12" - name: Set up uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5 - name: Install dependencies run: | @@ -37,10 +36,12 @@ jobs: needs: verify runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: "20" registry-url: "https://registry.npmjs.org" @@ -63,12 +64,14 @@ jobs: github-release: needs: verify runs-on: ubuntu-latest + permissions: + contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 - name: Create GitHub release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 with: generate_release_notes: true From d87d42d9f2cdd49e705d0952ef8d850c65990e41 Mon Sep 17 00:00:00 2001 From: Carter Stach Date: Mon, 8 Jun 2026 17:54:27 -0400 Subject: [PATCH 3/3] Enhance raycasting functionality and update CI workflow - Updated the GitHub Actions workflow to ensure the `github-release` job depends on both `verify` and `publish-npm`. - Improved the `raycast_block` and `raycast_entity` functions to schedule function generation for loop functions when referenced. - Added a new test to verify that raycast callback functions are generated correctly when referenced in the datapack. --- .github/workflows/release.yml | 2 +- .../compiler/builtin_functions.py | 8 +++++ tests/test_tree_shaking.py | 29 +++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d550262..48e7746 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,7 +62,7 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} github-release: - needs: verify + needs: [verify, publish-npm] runs-on: ubuntu-latest permissions: contents: write diff --git a/minecraft_script/compiler/builtin_functions.py b/minecraft_script/compiler/builtin_functions.py index 66f0fcb..396ddea 100644 --- a/minecraft_script/compiler/builtin_functions.py +++ b/minecraft_script/compiler/builtin_functions.py @@ -129,6 +129,10 @@ def raycast_block(interpreter, args, context) -> function_output: raycast_range: mcs_type = args[1] raycast_loop_function: MCSFunction | None = args[2] if len(args) > 2 else None + interpreter.schedule_function_generation(raycast_function) + if raycast_loop_function is not None: + interpreter.schedule_function_generation(raycast_loop_function) + interpreter.add_commands( local_context.mcfunction_name, version.render_lines( @@ -165,6 +169,10 @@ def raycast_entity(interpreter, args, context) -> function_output: raycast_range: mcs_type = args[1] raycast_loop_function: MCSFunction | None = args[2] if len(args) > 2 else None + interpreter.schedule_function_generation(raycast_function) + if raycast_loop_function is not None: + interpreter.schedule_function_generation(raycast_loop_function) + interpreter.add_commands( local_context.mcfunction_name, version.render_lines( diff --git a/tests/test_tree_shaking.py b/tests/test_tree_shaking.py index 0dd28e5..1be0c65 100644 --- a/tests/test_tree_shaking.py +++ b/tests/test_tree_shaking.py @@ -95,3 +95,32 @@ def test_click_handler_functions_are_generated_when_referenced(compile_datapack) assert datapack.has_function("user_functions", "use_item.mcfunction") assert datapack.has_function("clickable_items", "0.mcfunction") assert not datapack.has_function("user_functions", "unused_click_handler.mcfunction") + + +def test_raycast_callback_functions_are_generated_when_referenced(compile_datapack): + source = """ +function on_hit() { + log("hit"); +} + +function on_loop() { + log("loop"); +} + +function unused_raycast_handler() { + log("unused"); +} + +function init() { + @a raycast_block(on_hit, 10, on_loop); +} + +function main() { +} +""" + + datapack = compile_datapack(source, "Test Tree Shake Raycast") + + assert datapack.has_function("user_functions", "on_hit.mcfunction") + assert datapack.has_function("user_functions", "on_loop.mcfunction") + assert not datapack.has_function("user_functions", "unused_raycast_handler.mcfunction")