Skip to content

feat: WASM compatibility rule checks#9587

Open
dmadisetti wants to merge 1 commit into
mainfrom
dm/lint-wasm
Open

feat: WASM compatibility rule checks#9587
dmadisetti wants to merge 1 commit into
mainfrom
dm/lint-wasm

Conversation

@dmadisetti
Copy link
Copy Markdown
Collaborator

@dmadisetti dmadisetti commented May 18, 2026

📝 Summary

Adds new class of lint rules MW for marimo wasm compatibility checks, codifying https://github.com/marimo-team/skills/tree/main/skills/wasm-compatibility

Note: these skills are opt-in (marimo check --select MW) and are somewhat expensive (MW003: incompatible-package in particular requires network access to determine dependencies).

The new rules are:

  1. MW001: incompatible-import
    • Checks for stdlib incompatible packages such as multiprocessing, and subprocess
  2. MW002: unsafe-system-call
    • Checks for known library calls that may cause issues in WASM (e.g. os.system)
  3. MW003: incompatible-package
    • Checks package dependencies (against pypi) to see whether all packages and transitive packages have supported emscripten builds.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
marimo-docs Ready Ready Preview, Comment May 18, 2026 7:23pm

Request Review

@github-actions github-actions Bot added the documentation Improvements or additions to documentation label May 18, 2026
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 22 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="marimo/_lint/rules/wasm/incompatible_packages.py">

<violation number="1" location="marimo/_lint/rules/wasm/incompatible_packages.py:67">
P1: Treating `.tar.gz`/`.zip` files as WASM-compatible makes the rule miss incompatible native packages. Only compatible wheels (or known Pyodide packages) should satisfy this check.</violation>
</file>

<file name="marimo/_lint/rules/wasm/incompatible_imports.py">

<violation number="1" location="marimo/_lint/rules/wasm/incompatible_imports.py:96">
P2: The diagnostic text says these modules "will fail to import", but some flagged modules (notably `multiprocessing`/`subprocess`) are importable in Pyodide and fail at runtime usage instead.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant CLI as CLI (marimo check)
    participant Export as html_wasm Export
    participant Selector as rule_selector.resolve_rules()
    participant Init as rules/__init__.py
    participant Engine as RuleEngine
    participant Lint as Linter
    participant MW001 as MW001 IncompatibleImportsRule
    participant MW002 as MW002 UnsafeSystemCallsRule
    participant MW003 as MW003 IncompatiblePackagesRule
    participant PyPI as PyPI JSON API
    participant Pyodide as pyodide-lock.json

    Note over CLI,MW003: NEW: WASM rule category (MW) – off by default

    CLI->>Engine: create_default()
    Engine->>Selector: resolve_rules(config)
    alt No --select provided
        Selector->>Init: Read DEFAULT_RULE_CODES
        Init-->>Selector: Breaking + Runtime + Formatting only
        Selector-->>Engine: Default rule instances (no MW)
    else --select MW or --select ALL
        Selector->>Init: Read RULE_CODES (includes WASM)
        Init-->>Selector: All rules including MW001-003
        Selector-->>Engine: Selected rule instances
    end
    Engine-->>CLI: RuleEngine ready

    alt WASM rules selected
        CLI->>Lint: lint(notebook)
        Lint->>MW001: check() – scan imports
        MW001->>MW001: Match cell imports against INCOMPATIBLE_MODULES
        alt Found incompatible import
            MW001-->>Lint: Diagnostic MW001
        end

        Lint->>MW002: check() – scan AST for unsafe calls
        MW002->>MW002: Visit Call nodes for os.*, signal.*, breakpoint()
        alt Found unsafe call
            MW002-->>Lint: Diagnostic MW002
        end

        Lint->>MW003: check() – resolve dependency tree
        MW003->>Pyodide: fetch_pyodide_package_versions()
        Pyodide-->>MW003: Set of available packages
        MW003->>MW003: Extract PEP 723 dependencies from notebook
        MW003->>MW003: Walk transitive deps via importlib.metadata
        loop Each unique package
            alt Package in Pyodide set
                Note over MW003: Package available in Pyodide
            else Check PyPI
                MW003->>PyPI: GET /pypi/{name}/json
                PyPI-->>MW003: Wheel metadata
                alt Has py3-none-any or emscripten wheel
                    Note over MW003: Compatible wheel found
                else Only native wheels
                    MW003->>MW003: Add to incompatible list
                end
            end
        end
        alt Incompatible packages found
            MW003-->>Lint: Diagnostic MW003
        end
        Lint-->>CLI: All diagnostics
    end

    Note over Export,MW003: CHANGED: html_wasm export runs MW lint pass
    Export->>MW003: run_check(select=["MW"])
    MW003-->>Export: WASM compatibility diagnostics (stderr)
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

):
return True
# Source distributions can be built as pure-python by micropip.
if filename.endswith((".tar.gz", ".zip")):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Treating .tar.gz/.zip files as WASM-compatible makes the rule miss incompatible native packages. Only compatible wheels (or known Pyodide packages) should satisfy this check.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At marimo/_lint/rules/wasm/incompatible_packages.py, line 67:

<comment>Treating `.tar.gz`/`.zip` files as WASM-compatible makes the rule miss incompatible native packages. Only compatible wheels (or known Pyodide packages) should satisfy this check.</comment>

<file context>
@@ -0,0 +1,224 @@
+            ):
+                return True
+        # Source distributions can be built as pure-python by micropip.
+        if filename.endswith((".tar.gz", ".zip")):
+            return True
+
</file context>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

No, pypi doesn't distribute non-wheels and finding a value with this raw extension is probably an indication it was intentional

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the clarification. If you can share a reference or an example, that would help ensure we handle this case correctly.

Comment thread marimo/_lint/rules/wasm/unsafe_system_calls.py
Diagnostic(
message=(
f"Module '{top_level}' is not available in "
"WASM/Pyodide and will fail to import."
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: The diagnostic text says these modules "will fail to import", but some flagged modules (notably multiprocessing/subprocess) are importable in Pyodide and fail at runtime usage instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At marimo/_lint/rules/wasm/incompatible_imports.py, line 96:

<comment>The diagnostic text says these modules "will fail to import", but some flagged modules (notably `multiprocessing`/`subprocess`) are importable in Pyodide and fail at runtime usage instead.</comment>

<file context>
@@ -0,0 +1,102 @@
+                        Diagnostic(
+                            message=(
+                                f"Module '{top_level}' is not available in "
+                                "WASM/Pyodide and will fail to import."
+                            ),
+                            line=line,
</file context>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fair

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the feedback!

@dmadisetti dmadisetti added the enhancement New feature or request label May 19, 2026
@dmadisetti dmadisetti marked this pull request as ready for review May 20, 2026 20:13
@dmadisetti dmadisetti requested a review from akshayka as a code owner May 20, 2026 20:13
Copilot AI review requested due to automatic review settings May 20, 2026 20:13
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new opt-in “WASM compatibility” lint category (MW001–MW003) so marimo can detect common Pyodide/WASM incompatibilities, and wires these rules into rule selection, docs, and the WASM export path.

Changes:

  • Introduces Severity.WASM and a new marimo._lint.rules.wasm rule set (MW001 incompatible imports, MW002 unsafe system calls, MW003 incompatible packages).
  • Splits rule registration into DEFAULT_RULE_CODES (enabled by default) vs RULE_CODES (includes opt-in categories like WASM), updating selector/engine/tests accordingly.
  • Updates docs + mkdocs nav, and runs a WASM lint pass during marimo export html-wasm --execute.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tests/_lint/test_wasm_rules.py Adds tests for MW rules being off-by-default and for MW001/MW002 behavior.
tests/_lint/test_rule_selector.py Updates selector tests for default vs ALL rules; adds MW selection tests.
tests/_lint/test_lint_config_integration.py Updates integration assertions to use DEFAULT_RULE_CODES for default engine/linter creation.
tests/_lint/test_files/wasm_incompatible.py Adds a fixture notebook containing WASM-incompatible imports/calls.
scripts/generate_lint_docs.py Extends lint-doc generation to include WASM severity and MW code validation.
mkdocs.yml Adds navigation entries for new MW rule docs pages.
marimo/_lint/rules/wasm/unsafe_system_calls.py Implements MW002 to flag unsupported runtime calls (e.g., os.system, breakpoint).
marimo/_lint/rules/wasm/incompatible_packages.py Implements MW003 to flag dependency-tree packages lacking WASM-compatible artifacts.
marimo/_lint/rules/wasm/incompatible_imports.py Implements MW001 to flag imports of stdlib modules unavailable in Pyodide.
marimo/_lint/rules/wasm/init.py Registers WASM rules and exports WASM_RULE_CODES.
marimo/_lint/rules/init.py Introduces DEFAULT_RULE_CODES and expands RULE_CODES to include opt-in WASM rules.
marimo/_lint/rule_selector.py Changes default behavior to select DEFAULT_RULE_CODES unless select is provided.
marimo/_lint/rule_engine.py Uses DEFAULT_RULE_CODES when creating the default rule engine without config.
marimo/_lint/diagnostic.py Adds Severity.WASM.
marimo/_lint/context.py Adds WASM severity to diagnostic priority mapping.
marimo/_lint/init.py Adds WASM severity ordering for filtering/aggregation.
marimo/_cli/export/commands.py Runs MW lint selection during html-wasm --execute.
docs/guides/lint_rules/rules/unsafe_system_call.md Adds MW002 documentation page.
docs/guides/lint_rules/rules/incompatible_package.md Adds MW003 documentation page.
docs/guides/lint_rules/rules/incompatible_import.md Adds MW001 documentation page.
docs/guides/lint_rules/index.md Documents the new WASM rule category and lists MW rules.
development_docs/adding_lint_rules.md Documents WASM severity and the default vs opt-in rule registration model.

Comment thread marimo/_cli/export/commands.py
Comment on lines +51 to +57
# Step 1: base set — use only default rules unless explicitly selected
if select:
codes = {c for c in codes if _matches_any_prefix(c, select)}
codes = {c for c in all_rules if _matches_any_prefix(c, select)}
else:
from marimo._lint.rules import DEFAULT_RULE_CODES

codes = set(DEFAULT_RULE_CODES.keys()) & set(all_rules.keys())
Comment thread marimo/_lint/rules/wasm/incompatible_packages.py
Comment on lines +36 to +52
@functools.cache
def _has_wasm_compatible_wheel(package_name: str) -> bool:
"""Check PyPI for a pure-python or emscripten wheel.

Returns True if micropip can install this package (has a
py3-none-any wheel, a py2.py3-none-any wheel, or an
emscripten/wasm32 wheel). Returns True on network failure
(fail open). Cached so a single export with N transitive deps
hits PyPI at most once per unique package name.
"""
url = f"https://pypi.org/pypi/{package_name}/json"
try:
with urllib.request.urlopen(url, timeout=10) as resp:
data = json.loads(resp.read())
except Exception:
return True # Can't check — assume compatible.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It is gated, it's opt in with --select. But I do think we should try to respect the index

Comment on lines +86 to +90
def test_select_wasm_rules(self):
rules = resolve_rules({"select": ["MW"]})
assert all(r.code.startswith("MW") for r in rules)
assert len(rules) == 3

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Agreed

Comment on lines +1 to +6
# Copyright 2026 Marimo. All rights reserved.
"""Tests for WASM compatibility lint rules (MW001, MW002, MW003)."""

from __future__ import annotations

from marimo._ast.parse import parse_notebook
Comment on lines +53 to +66
urls = data.get("urls", [])
if not urls:
return True # No files — likely a namespace package.

for file_info in urls:
filename = file_info.get("filename", "")
if filename.endswith(".whl"):
if (
"none-any" in filename
or "emscripten" in filename
or "wasm" in filename
):
return True
# Source distributions can be built as pure-python by micropip.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants