Skip to content

fix(server): a walker answers with reports only; report outside a walk is E1135 - #8950

Open
kashmithnisakya wants to merge 5 commits into
jaseci-labs:mainfrom
kashmithnisakya:fix/walker-wire-contract
Open

fix(server): a walker answers with reports only; report outside a walk is E1135#8950
kashmithnisakya wants to merge 5 commits into
jaseci-labs:mainfrom
kashmithnisakya:fix/walker-wire-contract

Conversation

@kashmithnisakya

@kashmithnisakya kashmithnisakya commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Fixes #8906.

The contract

A walker's has fields are its request and report is its response. On the wire a walker is therefore its reports and nothing else: the result slot of a walker spawn is {}, and a walker nested anywhere in a payload serialises to {}. A function answers with its return value in result, and the only reports beside it are the ones produced by walkers it spawned. report outside a walker, node, or edge is now a compile-time error, E1135, so a function cannot report at all. Inside those three archetypes it is valid anywhere, including methods, nested defs, nested objs and impl bodies, since those all run inside a walk. A streaming function returns its generator (return stream();). A def:pub whose declared return type is a walker gets W6010, because its typed consumers receive an empty object.

Before this change every walker response carried its report list twice, once at data.reports and once inside data.result through the walker's intrinsic reports field. A 300-item list walker measured 149,771 bytes, of which 74,782 were the copy. The echo also carried three marker keys and every has field, none of which any client reads: the browser runtime returns the whole envelope and reads data.reports, and the sv-to-sv stub overwrote instance.reports from the envelope after rebuilding the instance from result.

Responses, old and new

All shapes are the data slot. The outer {"ok", "type", "data", "error"} envelope and the error envelope are unchanged.

1. Walker that reports POST /walker/Outer {"tag": "t1"} (spawns NoDecl inside, which reports {"deleted": 5})

Old:

{"result": {"_jac_type": "Outer", "_jac_id": "", "_jac_archetype": "walker",
            "reports": [{"deleted": 5}, {"outer": "t1"}], "tag": "t1"},
 "reports": [{"deleted": 5}, {"outer": "t1"}]}

New:

{"result": {}, "reports": [{"deleted": 5}, {"outer": "t1"}]}

2. Walker with no has fields POST /walker/NoDecl {}

Old:

{"result": {"_jac_type": "NoDecl", "_jac_id": "", "_jac_archetype": "walker",
            "reports": [{"deleted": 5}]},
 "reports": [{"deleted": 5}]}

New:

{"result": {}, "reports": [{"deleted": 5}]}

3. Walker that never reports POST /walker/CountThings {"prefix": "t"} (accumulates total and names in fields)

Old:

{"result": {"_jac_type": "CountThings", "_jac_id": "", "_jac_archetype": "walker",
            "names": ["t0", "t1", "t2"], "prefix": "t", "reports": [], "total": 3},
 "reports": []}

New:

{"result": {}, "reports": []}

4. Function with a return value POST /function/list_things {"n": 2}

Old and new, unchanged:

{"result": [{"name": "thing-0", "idx": 0}, {"name": "thing-1", "idx": 1}], "reports": []}

5. Function that returns nothing

Old and new, unchanged:

{"result": null, "reports": []}

6. Function that calls report

Old:

{"result": {"returned": 2}, "reports": [{"reported": 2}]}

New, at compile time:

error[E1135]: `report` is only valid inside a walker, node, or edge
  --> app.jac:30:5
   30 |     report {"from_function": True};
      |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: `report` appends to the reports of the walker whose walk is running, so it belongs to the
archetypes that take part in a walk: a walker, or a node or edge the walker visits, including their
methods and impls. A function answers with `return`; a streaming function returns its generator;
spawn a walker from a function if reported values should reach the caller.

7. Function that spawns a walker and returns its own value POST /function/spawn_inside {"n": 1} (the walker does report items with a one-item list)

Old, with the function's own report appended:

{"result": {"count": 1},
 "reports": [[{"name": "thing-0", "idx": 0, "payload": ""}], {"from_function": true}]}

New:

{"result": {"count": 1},
 "reports": [[{"name": "thing-0", "idx": 0, "payload": ""}]]}

8. Function that returns the walker it spawned POST /function/spawn_and_return_walker {"n": 1}

Old:

{"result": {"_jac_type": "ListThings", "_jac_id": "", "_jac_archetype": "walker", "n": 1,
            "reports": [[{"name": "thing-0", "idx": 0, "payload": ""}]]},
 "reports": [[{"name": "thing-0", "idx": 0, "payload": ""}]]}

New:

{"result": {}, "reports": [[{"name": "thing-0", "idx": 0, "payload": ""}]]}

9. sv-to-sv remote spawn rg = Greet(name="ada") across a service cut

Old: the consumer-side instance was rebuilt from the result echo, then rg.reports was overwritten from the envelope.
New: the instance is built from the fields the caller passed (deep-copied), the provider's literal has defaults for anything left out, and the provider's reports. rg.name == "ada", rn.body == "" for a defaulted field, and rg.reports == ["hello, ada"] all hold. Field state the provider's walk mutated stays on the provider. With no stub class (the embedded gateway's walker proxy) the helper hands back the {result, reports} payload as is.

What changed

  • jaclang/data/impl/serializer.impl.jac: in api_mode a WalkerArchetype serialises to {}. This is the envelope encoder's only caller, so it covers the result slot, walkers nested in a return value, and walkers inside reports. The 'walker' branch of _jac_archetype was unreachable after that and is gone.
  • diagnostics.jac, type_checker_pass.jac, type_checker_pass.impl.jac: E1135 is one lexical climb, _enclosing_walk_archetype, that stops at the nearest walker, node or edge and follows impl declarations on the way, so helpers, nested defs, nested objs and forward-declared impl chains inside those archetypes stay valid. The old _enclosing_walker_report_elem_type split into that climb plus _walker_report_elem_type.
  • boundary_analysis_pass, codeinfo.jac, compiler.impl.jac, jcir_gen_pass.impl.jac: walker bindings now carry the provider's literal has defaults, emitted on the stub as __jac_field_defaults__; import facts carry every provider walker name so a def:pub returning one warns W6010 at the consumer.
  • jaclang/server/sv_client.jac: unwrap_envelope, function_result and hydrate_walker_envelope are the one set of sv-to-sv envelope decoders. The core runtime and scale/runtime/rpc/rpc.jac call them for both the function and walker paths; the three private copies of the unwrap logic are gone.
  • scale/runtime/gateway/embedded_gateway.impl.jac: the walker proxy forwards the provider's {result, reports} payload instead of wrapping the walker under result.
  • Docs: microservices.md, jac-sv-endpoints.md, interop.md, jac-sv-streaming.md (a streaming def returns its generator), and jac-scale-http.md (plugin override guidance now names the shared decoders).

Tests

  • tests/runtimelib/test_serve_walker_wire.jac covers cases 1, 3, 4, 5, 7 and 8 against a live test client.
  • tests/runtimelib/test_sv_walker_hydration.jac exercises the compiler-generated stub classes from sv_relative_consumer.jac: caller fields, literal defaults, no aliasing of caller arguments, the no-stub payload, and the error envelopes of both decoders.
  • tests/language/test_language.jac: E1135 for a function and for an obj method, plus a fixture proving report reaches the walk from a node method, a nested def, a nested obj and an impl chain.
  • tests/compiler/test_typed_interop.jac: W6010 for launch -> Probe.
  • scale/tests/microservices/test_microservice.jac reads a caller field, a defaulted field and a report off remote walkers end to end; test_embedded_gateway.jac pins the walker proxy payload.
  • scale/tests/fixtures/cl_fullstack/test_echo.jac was the one reader of result.reports in the tree and now reads data.reports.

Compatibility

This is a wire-shape change and ships with a breaking release note. Raw REST consumers that read has fields or marker keys out of data.result on a walker spawn must move to report. Generated clients are unaffected: the browser runtime already reads data.reports, and the sv-to-sv stub carries the caller's own arguments plus provider literal defaults. In-process spawn under jac run is unchanged; w.total still works there. A def:pub that returns a walker keeps compiling but warns W6010, since typed consumers get an empty object.

@kashmithnisakya kashmithnisakya changed the title fix(server): a walker answers with reports only; report outside a walker is E1135 fix(server): a walker answers with reports only; report outside a walk is E1135 Sep 4, 2026
kashmithnisakya and others added 2 commits September 4, 2026 11:48
…alker/node/edge, stub defaults, one envelope decoder, gateway walker payload, W6010
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Every walker response serializes reports twice; no consumer reads the copy inside result

2 participants