Skip to content

Commit 99b1209

Browse files
authored
Merge pull request #2751 from actonlang/guide-constraint-signatures
Explain constraint errors with signatures
2 parents 8c5baa9 + 4416fff commit 99b1209

2 files changed

Lines changed: 158 additions & 3 deletions

File tree

docs/acton-guide/src/types/intro.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,8 @@ types. You will see generic binders, protocol constraints, optional
8686
types, tuple rows, and effect markers. Reading a signature means
8787
reading both the data shape and the callable behavior. The compiler's
8888
output is often the clearest summary of what an API actually promises,
89-
so <code>--sigs</code> is a useful first step before deciding whether to
90-
make that promise explicit.</p>
89+
so <code>acton sig</code> and <code>--sigs</code> are useful first
90+
steps before deciding whether to make that promise explicit.</p>
9191
</div>
9292

9393
This is especially useful when:

docs/acton-guide/src/types/troubleshooting.md

Lines changed: 156 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ instead of searching for generated files or guessing from memory.
88
Use this workflow:
99

1010
1. Read the type error and find the module or type name involved.
11-
2. Run `acton sig` for that module or public name.
11+
2. Run `acton sig` for the smallest exact module or public name.
1212
3. Compare the available attributes and methods with the failing code.
1313
4. Fix the source and rebuild.
1414

@@ -94,3 +94,158 @@ override you would pass to `acton build`:
9494
```sh
9595
acton sig --dep directory=../directory directory.Contact
9696
```
97+
98+
## Example: selecting attributes on the wrong value
99+
100+
Some type errors mention an "unknown type" even when the value has a
101+
known expected type. This can happen when Acton checks a method body and
102+
collects constraints from dot selections before it has simplified the
103+
whole constraint set.
104+
105+
The important part is not the unknown type name alone. Read the whole
106+
error and look for the known type that must satisfy those constraints.
107+
108+
Suppose one module defines an item type, a context type, and a base class
109+
whose method takes both:
110+
111+
```python
112+
# pipeline.act
113+
class WorkItem(object):
114+
title: str
115+
tags: set[str]
116+
output_path: str
117+
118+
def __init__(self, title: str, tags: set[str], output_path: str):
119+
self.title = title
120+
self.tags = tags
121+
self.output_path = output_path
122+
123+
class RunContext(object):
124+
run_id: str
125+
user: str
126+
127+
def __init__(self, run_id: str, user: str):
128+
self.run_id = run_id
129+
self.user = user
130+
131+
class Step(object):
132+
def apply(self, item: WorkItem, ctx: RunContext):
133+
raise NotImplementedError()
134+
```
135+
136+
A subclass can omit the local annotations because the inherited method
137+
shape already gives `item` and `ctx` their expected types:
138+
139+
```python
140+
# steps.act
141+
from pipeline import Step
142+
143+
class DraftStep(Step):
144+
def apply(self, item, ctx):
145+
print("processing {ctx.title}")
146+
147+
if "draft" in ctx.tags:
148+
return ctx.output_path
149+
150+
raise ValueError("not a draft")
151+
```
152+
153+
The method uses `ctx.title`, `ctx.tags`, and `ctx.output_path`, but the
154+
inherited signature says `ctx` is a `pipeline.RunContext`. The resulting
155+
error has a common "simultaneous constraints" shape:
156+
157+
```text
158+
[error]: Cannot satisfy the following simultaneous constraints for the unknown types
159+
+--> steps.act@4:5-10:1
160+
|
161+
4 | +> def apply(self, item, ctx):
162+
5 | | print("processing {ctx.title}")
163+
: | ^--------
164+
: | `- The type of the indicated expression (which we call t0) must have an attribute title with type t4; no such type is known.
165+
6 | |
166+
7 | | if "draft" in ctx.tags:
167+
: | ^----------^-------
168+
: | | |- The type of the indicated expression (which we call t0) must have an attribute tags with type t1; no such type is known.
169+
: | | `- The type of the indicated expression (which we call t1) must be a subtype of t2
170+
: | |- The type of the indicated expression (inferred to be __builtin__.str) must be a subtype of t3
171+
: | `- The type of the indicated expression (which we call t2) must implement __builtin__.Container[t3]
172+
8 | | return ctx.output_path
173+
: | ^--------------
174+
: | `- The type of the indicated expression (which we call t0) must have an attribute output_path with type None; no such type is known.
175+
: | The type of the indicated expression (which we call t4) must be a subclass of ?__builtin__.value
176+
9 | |
177+
10 | |> raise ValueError("not a draft")
178+
: |
179+
: `- pipeline.RunContext must be a subclass of t0
180+
-----+
181+
```
182+
183+
Do not read this as "Acton cannot infer the type of `ctx`". In this
184+
example, the inherited method signature already gives `ctx` the expected
185+
type `pipeline.RunContext`. The unknown `t0` is the type variable that
186+
collected the requirements introduced by the dot selections on `ctx`.
187+
The final line says that `pipeline.RunContext` must satisfy those
188+
collected requirements.
189+
190+
The next step is to inspect the exact known type named in the error:
191+
192+
```sh
193+
acton sig pipeline.RunContext
194+
```
195+
196+
That is usually better than asking for the whole module, because a large
197+
module can produce a lot of unrelated output. The signature shows the
198+
compiler-visible fields:
199+
200+
```python
201+
class RunContext (object, value):
202+
@property
203+
run_id : str
204+
@property
205+
user : str
206+
__init__ : (run_id: str, user: str) -> None
207+
```
208+
209+
Now compare the failing selections with the signature. The code asked
210+
for `title`, `tags`, and `output_path`, but `RunContext` has only
211+
`run_id` and `user`. When none of the selected attributes are present,
212+
the code may be selecting on the wrong value. In this example, those
213+
attributes belong to `WorkItem`, so inspect that type too:
214+
215+
```sh
216+
acton sig pipeline.WorkItem
217+
```
218+
219+
The fix is not to add a redundant annotation to `ctx`. The fix is to use
220+
the value that actually has the fields:
221+
222+
```python
223+
class DraftStep(Step):
224+
def apply(self, item, ctx):
225+
print("processing {item.title}")
226+
227+
if "draft" in item.tags:
228+
return item.output_path
229+
230+
raise ValueError("not a draft")
231+
```
232+
233+
This pattern also appears with dependency types. If the known type comes
234+
from a dependency, still run `acton sig` for the exact public name shown
235+
in the error:
236+
237+
```sh
238+
acton sig package.module.TypeName
239+
```
240+
241+
If you are using a local dependency override, pass the same override to
242+
`acton sig` that you pass to `acton build`:
243+
244+
```sh
245+
acton sig --dep package=../package package.module.TypeName
246+
```
247+
248+
The compiler already sees the dependency signatures when it reports the
249+
error. Running `acton sig` does not make those signatures visible to the
250+
compiler; it makes them visible to you, so you can compare the API Acton
251+
is checking against the attributes and methods your code selects.

0 commit comments

Comments
 (0)