Script receivers 2/6: graph and checker take receivers from project.trb #513

Closed
opened 2026-10-26 21:47:21 +00:00 by orion · 11 comments
Collaborator

Slice 3 of docs/design/SCRIPT-RECEIVERS.md section 12 (on main). Part of the script-receivers epic #518 (step 2 of 6). Builds on slice 1 (#512, landed as 43a2b45f: the scripts line, Manifest.scripts, the pattern matcher matchesScriptPattern). Design PR: #519

Graph and checker take receivers from the mapping where there is one, and from literal loads where there is not.

Goal

A file that a scripts pattern of its project matches is a receiver script of its package: the graph adds it (also when nothing loads it), the checker checks it against the resolved receiver:/from: type, and the program's script table holds it. Unmapped files keep working exactly as today.

Design record

SCRIPT-RECEIVERS.md section 4 (the receiver type), section 5 (what a mapped file is; the ScriptReceiver shape), section 6 (load sites, only the mapped/unmapped split), section 12 (slice 3). Owner decisions of 2026-10-26 kept as decided: the mapping is optional, unmapped scripts stay valid (typed by inference from literal Sandbox.load calls, else untyped), and there is no strict mode.

Scope

  • compiler/src/semantics/module.trb: ScriptReceiver.Family(package: Int, index: Int) (the package in Workspace.packages and the line in its manifest.scripts); Loaded and Declared stay.
  • compiler/src/semantics/graph.trb, addReceiverScripts: project.trb as today; then for every package with scripts lines every file of package.files that matchesScriptPattern accepts (path relative to the package directory) gets Family, whether it is a plain script or an existing module (a file in src/ or a swept-in test directory, as Loaded already handles). The file is parsed even if no import reaches it (reachedFilesOf). Then the literal-load walk (scriptLoadsOf) for files that no pattern maps, unchanged. A file that two lines map to different receivers (receiver: plus from: as written) gets no Family receiver at all (checked against neither, D7 is reported by #514).
  • Files that are not scripts by what they are stay untouched: a match on a program entry, a *.test.trb or src/lib.trb is ignored here (reported as D6 by #514); project.trb/project.lock.trb are never in package.files.
  • compiler/src/semantics/checker/receiver.trb, scriptReceiverOf: Family resolves from: with the rules of a use relative to the directory of the project's project.trb (./, ../ inside the package, or a dependency's package name, resolveImport semantics; project.trb itself does not import anything), then looks up top-level receiver: in that module: a nominal type without type parameters, any top-level type if the module is of the project's own package, public only for another package. Anything else (no module, no type, a trait, generic, not public) gives None: the file stays untyped, silent in this slice (D8-D13 are #514).
  • The script table: scriptsOf (compiler/src/ir/lower/script.trb) and the embedded-sources loop in compiler/src/cli/host.trb take Loaded and Family, so scriptBody<Value>/scriptImports<Value> hold every file mapped to Value, in the VM and in a native binary.
  • Runtime loader (addRuntimeScripts, vm/run.trb, vm/host.trb): a path that lands on a file a pattern maps keeps its Family receiver; the type the running program names is compared with it, and a differing type ends in the existing "There is no script ... in this program" error (D17 gets its text in #514). A path that no pattern maps is checked against the type argument as today (Declared).
  • Not changed: the language server (language-server/* only keeps compiling and passing; its features are #515), Sandbox.load type-argument checks against the mapping (D14), the "first load decides" rule for unmapped files (replaced by D18 in #514, see the decision below), docs beyond the design record's status line.

Seed

Does not touch the seed: no syntax change, no new native, no driver flag, no new std name looked up by string (scriptBody/scriptImports already are). The compiler sources stay on existing syntax and no project.trb of this repository gets a scripts line, so the bootstrap does not need a published seed that contains slice 1. Not a two-commit change in the seed sense.

Acceptance

  • A mapped script that nothing loads and that holds a mistake (prot = 8080 for a port field) is reported by torb check at its position; the same file without the mistake checks clean.
  • A mapped script is a script wherever it lies, also in src/; a use that names it is refused (the existing refusal for a receiver script).
  • receiver:/from: resolve like a use: ./-relative module, and a module of a dependency package with a public type; a receiver that cannot be resolved leaves the file untyped without any diagnostic.
  • scriptBody<Value>/scriptImports<Value> answer a mapped file that no literal names, and a program that picks the path at run time (non-literal) finds it, in the VM and in a native binary (conformance program with a mapped family).
  • A path known at run time that lands on a mapped file is checked against the mapped type; an unmapped runtime path against the type argument as today.
  • An unmapped script with a literal Sandbox.load<T> still checks against T, and a project without any scripts line behaves as before (the existing receivers.test.trb/sandbox.test.trb cases stay green unchanged).
  • An unmapped script that no load names is accepted without any diagnostic, and no torb check option or project setting demands every script be mapped or typed (no strict mode).
  • Two lines that map one file to different receivers leave it untyped (no crash, no order dependence).
  • New cases in compiler/tests/receivers.test.trb and compiler/tests/sandbox.test.trb; forge CI tier A green (tier B because compiler/src/ir is touched).

Size

One PR, two Conventional Commits: feat(semantics): files a scripts line maps are receiver scripts (module, graph, receiver resolution, tests for torb check) and feat(ir): the script table holds the mapped family (table, host, runtime loader, tests).

Moved to #514

"First load decides" stays for unmapped files in this slice and is replaced there by D18, because spotting two different types in two loads needs the checker's resolved types and the exact message belongs to the diagnostics catalogue. D14 (type argument differs from the mapping) and D18 move with it, with the D4-D13, D16 and D17 texts. The examples/config-dsl scripts line belongs to the migration slice (#517).

Amended 2026-10-26 (owner): the scripts mapping is optional; unmapped scripts stay valid and are typed by inference from literal Sandbox.load calls, else untyped (SCRIPT-RECEIVERS.md sections 1, 5, 6).

Decided 2026-10-26 (owner): no strict mode (see the acceptance list).

Slice 3 of `docs/design/SCRIPT-RECEIVERS.md` section 12 (on `main`). Part of the script-receivers epic #518 (step 2 of 6). Builds on slice 1 (#512, landed as `43a2b45f`: the `scripts` line, `Manifest.scripts`, the pattern matcher `matchesScriptPattern`). Design PR: https://git.torb.dev/torbscript/language/pulls/519 Graph and checker take receivers from the mapping where there is one, and from literal loads where there is not. ## Goal A file that a `scripts` pattern of its project matches is a receiver script of its package: the graph adds it (also when nothing loads it), the checker checks it against the resolved `receiver:`/`from:` type, and the program's script table holds it. Unmapped files keep working exactly as today. ## Design record SCRIPT-RECEIVERS.md section 4 (the receiver type), section 5 (what a mapped file is; the `ScriptReceiver` shape), section 6 (load sites, only the mapped/unmapped split), section 12 (slice 3). Owner decisions of 2026-10-26 kept as decided: the mapping is optional, unmapped scripts stay valid (typed by inference from literal `Sandbox.load` calls, else untyped), and there is no strict mode. ## Scope - `compiler/src/semantics/module.trb`: `ScriptReceiver.Family(package: Int, index: Int)` (the package in `Workspace.packages` and the line in its `manifest.scripts`); `Loaded` and `Declared` stay. - `compiler/src/semantics/graph.trb`, `addReceiverScripts`: `project.trb` as today; then for every package with `scripts` lines every file of `package.files` that `matchesScriptPattern` accepts (path relative to the package directory) gets `Family`, whether it is a plain script or an existing module (a file in `src/` or a swept-in test directory, as `Loaded` already handles). The file is parsed even if no import reaches it (`reachedFilesOf`). Then the literal-load walk (`scriptLoadsOf`) for files that no pattern maps, unchanged. A file that two lines map to different receivers (`receiver:` plus `from:` as written) gets no `Family` receiver at all (checked against neither, D7 is reported by #514). - Files that are not scripts by what they are stay untouched: a match on a program entry, a `*.test.trb` or `src/lib.trb` is ignored here (reported as D6 by #514); `project.trb`/`project.lock.trb` are never in `package.files`. - `compiler/src/semantics/checker/receiver.trb`, `scriptReceiverOf`: `Family` resolves `from:` with the rules of a `use` relative to the directory of the project's `project.trb` (`./`, `../` inside the package, or a dependency's package name, `resolveImport` semantics; `project.trb` itself does not import anything), then looks up top-level `receiver:` in that module: a nominal `type` without type parameters, any top-level type if the module is of the project's own package, `public` only for another package. Anything else (no module, no type, a trait, generic, not public) gives `None`: the file stays untyped, silent in this slice (D8-D13 are #514). - The script table: `scriptsOf` (`compiler/src/ir/lower/script.trb`) and the embedded-sources loop in `compiler/src/cli/host.trb` take `Loaded` and `Family`, so `scriptBody<Value>`/`scriptImports<Value>` hold every file mapped to `Value`, in the VM and in a native binary. - Runtime loader (`addRuntimeScripts`, `vm/run.trb`, `vm/host.trb`): a path that lands on a file a pattern maps keeps its `Family` receiver; the type the running program names is compared with it, and a differing type ends in the existing "There is no script ... in this program" error (D17 gets its text in #514). A path that no pattern maps is checked against the type argument as today (`Declared`). - Not changed: the language server (`language-server/*` only keeps compiling and passing; its features are #515), `Sandbox.load` type-argument checks against the mapping (D14), the "first load decides" rule for unmapped files (replaced by D18 in #514, see the decision below), docs beyond the design record's status line. ## Seed Does not touch the seed: no syntax change, no new native, no driver flag, no new std name looked up by string (`scriptBody`/`scriptImports` already are). The compiler sources stay on existing syntax and no `project.trb` of this repository gets a `scripts` line, so the bootstrap does not need a published seed that contains slice 1. Not a two-commit change in the seed sense. ## Acceptance - [ ] A mapped script that nothing loads and that holds a mistake (`prot = 8080` for a `port` field) is reported by `torb check` at its position; the same file without the mistake checks clean. - [ ] A mapped script is a script wherever it lies, also in `src/`; a `use` that names it is refused (the existing refusal for a receiver script). - [ ] `receiver:`/`from:` resolve like a `use`: `./`-relative module, and a module of a dependency package with a `public` type; a receiver that cannot be resolved leaves the file untyped without any diagnostic. - [ ] `scriptBody<Value>`/`scriptImports<Value>` answer a mapped file that no literal names, and a program that picks the path at run time (non-literal) finds it, in the VM and in a native binary (conformance program with a mapped family). - [ ] A path known at run time that lands on a mapped file is checked against the mapped type; an unmapped runtime path against the type argument as today. - [ ] An unmapped script with a literal `Sandbox.load<T>` still checks against `T`, and a project without any `scripts` line behaves as before (the existing `receivers.test.trb`/`sandbox.test.trb` cases stay green unchanged). - [ ] An unmapped script that no load names is accepted without any diagnostic, and no `torb check` option or project setting demands every script be mapped or typed (no strict mode). - [ ] Two lines that map one file to different receivers leave it untyped (no crash, no order dependence). - [ ] New cases in `compiler/tests/receivers.test.trb` and `compiler/tests/sandbox.test.trb`; forge CI tier A green (tier B because `compiler/src/ir` is touched). ## Size One PR, two Conventional Commits: `feat(semantics): files a scripts line maps are receiver scripts` (module, graph, receiver resolution, tests for `torb check`) and `feat(ir): the script table holds the mapped family` (table, host, runtime loader, tests). ## Moved to #514 "First load decides" stays for unmapped files in this slice and is replaced there by D18, because spotting two different types in two loads needs the checker's resolved types and the exact message belongs to the diagnostics catalogue. D14 (type argument differs from the mapping) and D18 move with it, with the D4-D13, D16 and D17 texts. The `examples/config-dsl` `scripts` line belongs to the migration slice (#517). Amended 2026-10-26 (owner): the `scripts` mapping is optional; unmapped scripts stay valid and are typed by inference from literal `Sandbox.load` calls, else untyped (SCRIPT-RECEIVERS.md sections 1, 5, 6). Decided 2026-10-26 (owner): no strict mode (see the acceptance list).
Author
Collaborator

Design PR: #519 · Epic: #518

Design PR: https://git.torb.dev/torbscript/language/pulls/519 · Epic: https://git.torb.dev/torbscript/language/issues/518
Author
Collaborator

[loop:torbscript] intake: in scope (author orion, label loop, slice 2 of the script-receivers epic #518, design record docs/design/SCRIPT-RECEIVERS.md); no duplicate, no collision. It builds on the scripts line and static reader of slice 1 (#512, PR #557 in review). Just in time: refine and implement start when #512 has landed. next: waiting (#512 landed)

[loop:torbscript] intake: in scope (author orion, label `loop`, slice 2 of the script-receivers epic #518, design record docs/design/SCRIPT-RECEIVERS.md); no duplicate, no collision. It builds on the `scripts` line and static reader of slice 1 (#512, PR #557 in review). Just in time: refine and implement start when #512 has landed. next: waiting (#512 landed)
Author
Collaborator

[loop:torbscript] claim refine by session:loop-torbscript-20261029 until 2026-10-29T01:54:45Z

[loop:torbscript] claim refine by session:loop-torbscript-20261029 until 2026-10-29T01:54:45Z
Author
Collaborator

[loop:torbscript] decided by the loop: (1) Cut. Slice 3 as written in the design record (graph, resolution, table, runtime loader, D14, D18) is more than one PR; this issue keeps the part that makes mapped files work (graph, torb check, resolution of receiver:/from:, script table, runtime loader) in two commits, and the diagnostics move to #514 where the record's slice 4 already puts D1-D18. (2) Bad mappings (unresolvable from:/receiver:, a match on an entry, test or library, two lines with different receivers) leave the file untyped and silent in this slice, because their exact texts D4-D13 belong to #514; nothing is guessed or ordered silently: a conflicting file is checked against neither receiver, as the record says. (3) "First load decides" stays for unmapped files until #514 replaces it with D18: detecting two different types needs the checker's resolved types, and removing the rule earlier would leave a silent gap. Mapped files never use it. (4) Table: scriptsOf selecting every module whose receiver resolves to Value gives "a program that loads a Value at all holds the whole family" without a new table kind. (5) Seed: not touched, no published seed with slice 1 needed (no project.trb of this repository gets a scripts line; the compiler reads Manifest.scripts from its own sources). Checked on main 43a2b45f: slice 1 is in (Manifest.scripts, ScriptFamily, matchesScriptPattern, scriptProblemsOf).

[loop:torbscript] decided by the loop: (1) Cut. Slice 3 as written in the design record (graph, resolution, table, runtime loader, D14, D18) is more than one PR; this issue keeps the part that makes mapped files work (graph, `torb check`, resolution of `receiver:`/`from:`, script table, runtime loader) in two commits, and the diagnostics move to #514 where the record's slice 4 already puts D1-D18. (2) Bad mappings (unresolvable `from:`/`receiver:`, a match on an entry, test or library, two lines with different receivers) leave the file untyped and silent in this slice, because their exact texts D4-D13 belong to #514; nothing is guessed or ordered silently: a conflicting file is checked against neither receiver, as the record says. (3) "First load decides" stays for unmapped files until #514 replaces it with D18: detecting two different types needs the checker's resolved types, and removing the rule earlier would leave a silent gap. Mapped files never use it. (4) Table: `scriptsOf` selecting every module whose receiver resolves to `Value` gives "a program that loads a `Value` at all holds the whole family" without a new table kind. (5) Seed: not touched, no published seed with slice 1 needed (no `project.trb` of this repository gets a `scripts` line; the compiler reads `Manifest.scripts` from its own sources). Checked on main `43a2b45f`: slice 1 is in (`Manifest.scripts`, `ScriptFamily`, `matchesScriptPattern`, `scriptProblemsOf`).
Author
Collaborator

[loop:torbscript] refine: verified slice 1 on main 43a2b45f (scripts line, Manifest.scripts, matcher in place), read the graph, checker, script-table and runtime-loader code, and rewrote the body to the definition of ready: goal, nine testable acceptance criteria, design record sections, no-seed statement (no published seed with slice 1 needed), size one PR in two Conventional Commits. The diagnostic parts (D14, D18, removal of first-load for unmapped files, D4-D13, D16/D17) moved to #514 (noted there); decisions are in the comment above. Label ready set. Main CI on 43a2b45f was still running at 23:56Z (started 23:48Z, not red; the previous main commit is green), so implement should read it with ci-wait before pushing. Claim released. Tokens: about 90k. next: implement

[loop:torbscript] refine: verified slice 1 on main `43a2b45f` (scripts line, `Manifest.scripts`, matcher in place), read the graph, checker, script-table and runtime-loader code, and rewrote the body to the definition of ready: goal, nine testable acceptance criteria, design record sections, no-seed statement (no published seed with slice 1 needed), size one PR in two Conventional Commits. The diagnostic parts (D14, D18, removal of first-load for unmapped files, D4-D13, D16/D17) moved to #514 (noted there); decisions are in the comment above. Label `ready` set. Main CI on `43a2b45f` was still running at 23:56Z (started 23:48Z, not red; the previous main commit is green), so implement should read it with ci-wait before pushing. Claim released. Tokens: about 90k. next: implement
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261029 until 2026-10-29T06:08:38Z

[loop:torbscript] claim implement by session:loop-torbscript-20261029 until 2026-10-29T06:08:38Z
Author
Collaborator

[loop:torbscript] release implement: the worker was lost with its execution context after 13 min (gateway event, not a usage limit); check the branch loop/513-* for partial work. Respawning on the same profile, next: implement

[loop:torbscript] release implement: the worker was lost with its execution context after 13 min (gateway event, not a usage limit); check the branch loop/513-* for partial work. Respawning on the same profile, next: implement
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261029 until 2026-10-29T06:24:25Z

[loop:torbscript] claim implement by session:loop-torbscript-20261029 until 2026-10-29T06:24:25Z
Author
Collaborator

[loop:torbscript] release implement: the worker was lost with its execution context (gateway event, not a usage limit) after opening PR #562 (ce8b1e0e); its forge CI is running. next: waiting (forge CI on PR #562); a red lane caused by this change gets a fix worker

[loop:torbscript] release implement: the worker was lost with its execution context (gateway event, not a usage limit) after opening PR #562 (ce8b1e0e); its forge CI is running. next: waiting (forge CI on PR #562); a red lane caused by this change gets a fix worker
Author
Collaborator

[loop:torbscript] claim fix by session:loop-torbscript-20261029 until 2026-10-29T14:55Z

[loop:torbscript] claim fix by session:loop-torbscript-20261029 until 2026-10-29T14:55Z
Author
Collaborator

[loop:torbscript] review: forge CI was red on commit messages because the branch had a style(...) commit (style is not an allowed Conventional Commits type in tools/check-commits.sh); both of Argus's blockers (mismatch refusal untested, native-binary sources untested) were already covered by the test commit added after his review (sandbox.test.trb refusal case, programSourcesOf test in receivers.test.trb). I squashed the style and test-format commits into the feat(ir) commit (branch is 2 Conventional Commits on main 4b59fae, tree identical), force-with-lease pushed e2970afe, commit messages is now green, and re-requested review from argus. The remaining lanes (tier A/B, gates, bootstrap) were still queued after 29 min of waiting, not failed. Tokens: ~60k. next: waiting (forge CI and merge gate)

[loop:torbscript] review: forge CI was red on `commit messages` because the branch had a `style(...)` commit (`style` is not an allowed Conventional Commits type in `tools/check-commits.sh`); both of Argus's blockers (mismatch refusal untested, native-binary sources untested) were already covered by the test commit added after his review (`sandbox.test.trb` refusal case, `programSourcesOf` test in `receivers.test.trb`). I squashed the style and test-format commits into the `feat(ir)` commit (branch is 2 Conventional Commits on `main` 4b59fae, tree identical), force-with-lease pushed e2970afe, `commit messages` is now green, and re-requested review from argus. The remaining lanes (tier A/B, gates, bootstrap) were still queued after 29 min of waiting, not failed. Tokens: ~60k. next: waiting (forge CI and merge gate)
argus closed this issue 2026-10-29 13:14:26 +00:00
Sign in to join this conversation.
No labels
loop
ready
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language#513
No description provided.