feat(language)!: a call names the columns of a relation as relation[column], and a sum through a relation is a sum over the axes its join opens - #664
Conversation
… and every curve is written out before a model becomes one Resolved is gone. Lowering builds the Program as a model loads and holds it on the Spec; to_program(spec) returns that object, and returns the expansion's for a model with a curve, so a program holds the rows a curve states and never the curve. Named joins program.Expression, so a use of an expressions: entry stands where it is read and the typesetter prints from the program beside the file. ExpressionDeclaration carries its dims, and piecewise.curve reads a block's typed links and frame off the model for the expansion and the walk. PiecewiseDeclaration, Program.piecewise, Spec.resolved, lowering.inline and the refusal of a model still carrying a curve are removed. The typesetter's output is unchanged: the golden files and the generated pages regenerate byte for byte. Docs sentences, after (before): reading.md n 75 avg 16.1 median 15 over25 10 (72, 16.4, 14, 13); piecewise.md n 70 avg 17.6 median 15 over25 13 (69, 17.8, 15, 14). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrvhjFQCQJ6ATBQkfhcoMi
…s included, and the typesetter reads it alone
to_program lowers the model as it arrived: a piecewise: block still in it
is a curve on the program, typed, with its links, signs, method, gate, mask
and frame, and the program of spec.expand('piecewise') carries the rows
instead. Every declaration carries its description, and the program the
file's. The typesetter takes a Program and reads nothing else; the walk no
longer re-resolves a curve's links at print time, and piecewise.curve is
gone. relations_of moves from Spec to Program. advice() writes curves out
itself and refuses a program still carrying one.
The typeset output is unchanged: the golden files and the generated pages
regenerate byte for byte.
Docs sentences, after (before): reading.md n 75 avg 16.8 median 15
over25 12 (75, 16.1, 15, 10); piecewise.md n 70 avg 17.6 median 15
over25 14 (70, 17.6, 15, 13).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TrvhjFQCQJ6ATBQkfhcoMi
… lowering, before any expression is read The nine after-validators on Spec that read one declaration against the others — name collisions, frames over declared dimensions, relation targets, bound names, set shapes and bounds, curve references and the names an expansion would collide with — are functions in validation.py, collected by reference_errors and run first by lowering.lower. Spec keeps the shape rules pydantic decides per block, and no longer imports sos or the operator table; side_columns is the public name of the relation helper the rules share. Every message is the same string, and every refusal the same class. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrvhjFQCQJ6ATBQkfhcoMi
… is advised as its rows are The guard landed in the previous commit without the test that fails without it. With the guard deleted, the new test fails: a program with a block is advised on the file's own rows as if the curve stated none. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrvhjFQCQJ6ATBQkfhcoMi
…t-faraday-coc7mj-3
… the program owns the vocabulary the file declares in Loading no longer expands every curve to validate the expansion: the rows a curve states are held to the language when expand() writes them out, since an expansion is a model like any other. A model with a curve is validated once at load rather than twice. The dtype, domain, absence, sense, set-order and method vocabulary moves from model.py into program.py, which model.py imports. program.py no longer imports model.py, so the type-only import of Program on Spec and its noqa go, and sos.py imports Spec plainly instead of inside a function. Load cost, before (after): examples/piecewise.yaml validated 2 (1) times and parsed 9 (4) expressions; examples/sos.yaml the same. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrvhjFQCQJ6ATBQkfhcoMi
…he one door to both states is to_spec to_program is gone. Spec.program is the model typed, section for section, built once as the model loads; spec.expand(...).program is its rows. A consumer building rows reads the sections it takes and refuses a curve or a set it finds, the way it already had to for a set; advice() does so for a Program handed to it, and writes curves out itself for a file or a Spec. typeset and typeset_declaration take a Program as before. Docs sentences, after (before): reading.md n 75 avg 17.1 median 15 over25 14 (76, 16.8, 15, 12); piecewise.md n 70 avg 17.7 median 15 over25 13 (70, 17.6, 15, 14); what-counts-as-public-api.md n 18 avg 16.0 median 14 over25 3 (unchanged count); limits.md n 55 avg 18.3 median 19 over25 14 (unchanged count). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrvhjFQCQJ6ATBQkfhcoMi
…the model loads The private attribute lowering filled and the property asserting over it are one cached_property computing lower(self); the after-validator forces it, so a Spec in hand has still passed the whole language. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrvhjFQCQJ6ATBQkfhcoMi
…ules beside the namespace _Resolver, one class over both grammars, is ExpressionResolver in _expression_resolver.py and WhereResolver in _where_resolver.py, the where walk building a side that is an expression through the expression walk. resolution.py keeps the Namespace and the doors lowering calls. The three methods the where walk reads from the expression walk are public on it; names_in and the literal-number helper move to the parser module beside the other helpers over parsed nodes. Every message is the same string. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrvhjFQCQJ6ATBQkfhcoMi
…he program carries, and nothing else The Curve alias, a tacit protocol between the pydantic block and the program declaration, is gone: assumptions_of, Emitted.of and the curvature rule take a PiecewiseDeclaration. The expansion keeps the block for the link text its rows repeat and takes the declaration for the frame and the names it writes. The two emitted-name collision rules read the program rather than the file, so they run once the declarations exist; every message is the same string. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrvhjFQCQJ6ATBQkfhcoMi
…s use off the program, and the walk keeps no record The glossary and the three kinds of note move from walk.py to legend.py. What they explain is read off the program before anything prints, by notice(), so the walk no longer fills a Noticed record as it prints and a subscript no longer mutates the walk through its context. Walk.line refuses a name declared as none of the five kinds or as two, so typeset_declaration is one call. Symbols is a frozen record built by symbols_for. The typeset output is unchanged: the golden files match byte for byte. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrvhjFQCQJ6ATBQkfhcoMi
…t-faraday-coc7mj-3
…t-faraday-coc7mj-4
…t-faraday-coc7mj-6
…t-faraday-coc7mj-7
…t-faraday-coc7mj-8
…t-faraday-coc7mj-9
…, the errors, the advice and the typesetter Thirteen names leave math_spec's top level: the message builders call_shape_error, edge_error, unknown_operator_message and schema_error, the tables BUILTIN_NAMES and EDGE_WRAP, the vocabulary sets ADVICE_KINDS, DIMENSION_DTYPES, PARAMETER_DTYPES, VARIABLE_DOMAINS, VARIABLE_ABSENCE and CURVATURES, and SosBlock. The message builders and tables stay in their modules for the resolver; the vocabulary sets are deleted, since the Literals they were the set form of are what a consumer pins against, and nothing in the package read them. did_you_mean stays: it is the one wording a consumer's own refusals share with the language's. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrvhjFQCQJ6ATBQkfhcoMi
…vocabulary with no Literal form A consumer pins its operator table against BUILTIN_NAMES, and unlike the dtype vocabularies it has no Literal on math_spec.program to pin against instead; the module it lives in is package-private. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrvhjFQCQJ6ATBQkfhcoMi
…d advice writes nothing out on the caller's behalf UnexpandedCurveError carries the one sentence every consumer building rows says of a program still carrying a piecewise: block, naming the blocks and the expansion to pass. The language does not raise it at load, since a model with a curve is printed and edited as written; advice() raises it for a file, a Spec or a Program alike, and no longer expands a file or a Spec itself. The check verb and the check how-to expand first, as a front end may. Docs sentences, after (before): reading.md n 76 avg 17.1 median 15 over25 14 (75, 17.1, 15, 14); check.md n 20 avg 12.5 median 14 over25 1 (19 sentences before, one added). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrvhjFQCQJ6ATBQkfhcoMi
…ck verb takes --expand like every other The check verb read a file with its curves written out, the one verb that read a file differently from the rest. Every verb now reads the file as written and takes --expand; check refuses a curve model in the one wording until asked. The premise is on the public-API page, beside the other things every function keeps, and the reading page and the check how-to say it where a consumer meets it. Docs sentences, after (before): check.md n 22 avg 13.0 median 14 over25 2 (20, 12.5, 14, 1); what-counts-as-public-api.md n 22 avg 16.4 median 14 over25 4 (18, 16.0, 14, 3); reading.md n 77 avg 17.1 median 15 over25 14 (76, 17.1, 15, 14). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrvhjFQCQJ6ATBQkfhcoMi
UnexpandedCurveError is gone. advice words the refusal of a curve left as written itself, as a LanguageError, and a consumer building rows words its own; the reading page shows the idiom with the consumer's own error. A sentence is not a thing one tool should export for another to reuse, where a computation such as did_you_mean is. Docs sentences, after (before): reading.md n 76 avg 17.4 median 15 over25 15 (77, 17.2, 15, 14); piecewise.md n 70 avg 17.7 median 15 over25 13 (70, 17.7, 15, 14). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrvhjFQCQJ6ATBQkfhcoMi
…in opens The change of #641, carried onto #661 as one commit. #641 carries #638 as its separate commits, and main holds #638 squashed, so its net diff against its merge base with main (1e010ca) is applied here, not its history. The resolver edits move into _expression_resolver and _where_resolver, where #661's stack split resolution.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PHNF149r8B4tGmGUKmAdUd
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PHNF149r8B4tGmGUKmAdUd
…, and a join is never written apart from its reduction The relations page says by= is not the grouping, so a reader does not read it as a group-by key. The predicate at() paragraph drops the consumed and produced wording the join design retired. The limits page records a join operator apart from sum and at as refused, with the reason: the axis a join opens must close in the call that opens it. Docs sentences, after (before): relations.md n 45 avg 16.3 median 15 over25 8 (43, 16.2, 14, 8); expressions.md n 92 avg 15.3 median 14 over25 11 (91, 15.4, 14, 11); limits.md n 55 avg 18.3 median 19 over25 14 (unchanged; the new text is a table row). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PHNF149r8B4tGmGUKmAdUd
…s expand "states rows rather than being one" was hard to parse on first read, and the sentence named the fix with a different word from the method the user calls. The refusal now says the block is still a curve, that advice reads the rows a curve is expanded into, and to expand first. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrvhjFQCQJ6ATBQkfhcoMi
The unboundedness pass read a set as it is, every variable it restricts being named by a row, and refused a curve. A curve states its rows the same way: each link names the variables a link row would, so the pass reads the links and the answer is the expansion's with nothing expanded. The refusal goes, and check loses --expand, a flag that would change no answer. Guard: with the line reading the links deleted, three tests fail: the advice test over the file, the Spec and the Program; the boundedness case carried-by-a-curve; and the existing test that a curve holds its variables, which now runs on the block. Docs sentences, after (before): check.md n 20 avg 12.8 median 14 over25 1 (21, 13.3, 14, 2); what-counts-as-public-api.md n 20 avg 18.7 median 15 over25 4 (20, 18.1, 15, 4). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrvhjFQCQJ6ATBQkfhcoMi
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PHNF149r8B4tGmGUKmAdUd
Ports #662 onto main's rename to mathspec, its autoref docstrings and the model-to-spec wording, and adds its changelog line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YUK4sePJqJfxJJZLhdvZnS
…662 Rewrites main's newer calls, pypsa topic files and library example to relation[column]. Keeps main's over= lists and its refusal of a dimension named twice; a where read of several columns is by=relation[a, …]. Adds the changelog line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YUK4sePJqJfxJJZLhdvZnS
…664 Main's given-term, declared-frame and composition checks compare frames of axes. Adds the changelog line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YUK4sePJqJfxJJZLhdvZnS
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TMqXLvfDGYm8hRND63cs8
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TMqXLvfDGYm8hRND63cs8
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MKjqJocyxdRYvx7xin6NCM
…wru-3, with #662 and #664 as one changelog line Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MKjqJocyxdRYvx7xin6NCM
…nds for (#665) Axis(dimension, column) replaces the dotted axis names in Sum.over and JoinColumns.axes. A join's axis names its Column(relation, name), a dimension's own axis has no column, and frames stay sets of dimension names. Folded into #664, so its changelog line is #664's. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YVJQzL5G4boHcJDwrhgfag
…e 0.2.1 release, written in relation[column] The 25 conflicts take main's structure (adds_to:, given: expressions:, the objective each fragment sets) with this branch's spelling of each relation call. The one old-syntax expression main added outside the conflicts, in tests/test_pypsa_split.py, and the prose line on the library generator page are rewritten the same way. The changelog gets a fresh "Upcoming version" heading above 0.2.1 for this PR's line. The generated pages, the schema and the golden output were regenerated and did not change beyond the conflict resolution. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YVJQzL5G4boHcJDwrhgfag
…the old relation syntax stops loading Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YVJQzL5G4boHcJDwrhgfag
…n, so a relation may gain a column without changing a call Beside by=, each name in over= is read as a column of the relation where it has one, and as a dimension summed away after the group-by otherwise. The declaration already refuses a column named after a dimension it is not over, so the two readings never disagree. The over=relation[column] form and the two refusals that came with it, a dimension two columns are over and over= and by= naming two tables, are gone: the column's bare name says which. Every column a call touches is written in it, so adding a value column to a relation leaves every call as it was, which is what composing a relation across files needs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YVJQzL5G4boHcJDwrhgfag
|
I like what has happened since i last looked. The Program nodes are used for typesetting and dimensions checks and so on. I think that was inevitable. cool that this has landed (even though this was not part of this PR). The lowered code in this PR is a lot more straightforward: and #665 with the I still have a bit of troubles with the interpretation of the actual syntax, i do like the arrow stuff better. Here it is a bit unlcear that |
|
It would probably be good to rename |
If you are talking about syntax, are you talking about the yaml syntax or the "program syntax" ( |
…oin is the expression it builds PulledBackPredicate is renamed, so the two nodes at() lowers to pair the way Translate and TranslatedPredicate do. The prose that called a lookup a pullback follows, on the notation page's heading and in the golden model's comment. Asked for by coroa on #664. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YVJQzL5G4boHcJDwrhgfag
|
The program syntax is a definite improvement. The actual yaml syntax i am still at odds with, but hopefully we can improve that with the better program syntax. |
|
@FabianHofmann We have @coroa 's go to merge this as its a clear improvement, although he might want to discuss some follow up syntax changes as he has had some more time to look into the details. |
…PRs, written in relation[column] The one conflict is the changelog, which keeps both sides under the "Upcoming version" heading. Three expressions main added in the line and transformer fragments, and in the one-file pypsa spec, wrote at() with by=, over= and into=; they are rewritten as at(x, by=snapshot_period[period]). The generated pages were regenerated from the rewritten examples. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YVJQzL5G4boHcJDwrhgfag
Note
The following content was generated by AI.
A call reads one relation, and names its columns as
relation[column, …]. The program writes each such call as aJoin, which opens oneAxisfor each column it drops. ASumover those axes closes them. This PR includes #662 and #665.What this changes
The syntax
sum(p, by=gen_bus, over=generator, into=bus)sum(p, over=generator, by=gen_bus[bus])sum(p, by=slot_of, over=[generator, period], into=[bus, technology])sum(p, over=[generator, period], by=slot_of[bus, technology])at(cap, by=gen_zone, over=zone, into=generator)at(cap, by=gen_zone[zone])shift(p, …, by=cal, within=[day, week])shift(p, …, within=cal[day, week])position(h, by=cal, within=day) == 0position(h, within=cal[day]) == 0calendar.month == 'jan',ends.bus0 != ends.bus1calendar[month] == 'jan',ends[bus0] != ends[bus1]over=names what leaves. Besideby=, a name inover=is a column of the relation where it has one, and a dimension otherwise, summed away after the group-by. Sosum(p, over=[generator, snapshot], by=gen_bus[bus])is one call, andsum(x, over=from, by=nbr[to])says which of two columns over one dimension the operand is joined on.by=already matches that dim. Soat(x, by=rep_of[rep])readsxatrep(t).sum(x, over=d, by=rel[col])bindsrelandcolat the call site.joinoperator apart fromsumandatis refused, andlimits.mdrecords this. The axis must close in the call that opens it.The program
JoinandJoinColumnsreplaceDirection,GroupSumandPullback. A sum through a relation lowers toSum(Join(x, columns), over=axes), andat()lowers to the bareJoin. feat(program): a sum through a relation is a sum over the axes its join opens #641 has the design and its guard table.JoinedPredicatereplacesPulledBackPredicate.at(has_curve, by=zone_of[zone])in awhereis the predicate's lookupJoin, and the two names pair asTranslateandTranslatedPredicatedo.Column(relation, name)andAxis(dimension, column=None)are public inmathspec.program.Sum.overandJoinColumns.axesaretuple[Axis, ...]. A declared dimension isAxis(dimension). A join opensAxis(dimension, column)for each column it drops, so a map into its own dimension givesAxis('snapshot', Column('rep_of', 'snapshot')), which is notAxis('snapshot'). The type keeps them apart, where feat(program): a sum through a relation is a sum over the axes its join opens #662 used a dotted name.str()gives the user spelling,generatororzone_of[generator].dims_ofreads a sum over a join as one rule, so a join's axes never reach a frame (chore(program): a frame stays a set of dimension names, and only the sum over a join reads the axes it opens #799).Join, and separability readsaxis.columnto tell a grouping from a plain sum.Why
This is a break. Every file that writes
by=R, over=, into=orR.cstops loading, with no hand-written message for the old spelling. The release that carries it raises the minor version.Every relation call lowers to one
Joinand a reduction over the axes it opens. The syntax now has the same shape.over=keeps its meaning from a plainsum.by=holds the group-by key, which a pandas or SQL reader expects. The relation is written once, so a call cannot mix tables.over=reads column names so that a relation can grow. An earlier version of this PR readover=snapshotas a dimension and inferred the column over it, withover=rep_of[snapshot]for the case where two columns are over one dimension. Under that rule, addingalt: snapshottorep_ofmade an existingsum(x, over=snapshot, by=rep_of[rep])stop loading, because two columns now matched. A column name never matches twice.With
Axis, a consumer readsaxis.dimensionoraxis.column, and pyrefly checks it. It does not parse strings. The axes are needed only between a join and the sum that closes it, and the resolver is the only code that builds that pair, so frames do not carry axes.Method, departures, alternatives, new refusals, coverage moved, gates, not done
How this PR was made
This PR was three stacked PRs. #662 moved #641's program change onto
main, with the syntaxby=R, over=, into=. This PR replaced that syntax and kept dotted axis names in the program. #665 replaced those names withAxis. The review of the stack found that this PR removed about a third of the lines #662 added, and that #665 removed a convention this PR introduced, so both are folded in: #662's commits are in this branch, and #665 is the squash commit6a899c9. The branch is not rebased and not force-pushed.33b8eabmergesmain, which had moved by #763 (adds_to:), #795, #809 and the 0.2.1 release. The 25 conflicts take main's structure with this branch's spelling of each relation call. The one old-syntax expression main added outside the conflicts, intests/test_pypsa_split.py, and a prose line on the library generator page are rewritten the same way. The changelog gets a fresh## Upcoming versionheading above 0.2.1.24c7354changesover=besideby=from dimensions to column names, after a comparison of the two syntaxes on 29 situations found that a value column added to a relation broke an existing sum under the dimension reading and nothing else. The three PyPSA maintenance constraints that wroteover=X_maintenance_cover[start]now writeover=start.7bb84bcrenamesPulledBackPredicatetoJoinedPredicate, as coroa asked in the comments, and the prose that called a lookup a pullback follows.9d68a67mergesmainagain, with the nine PyPSA docs PRs #786 to #817. The one conflict is the changelog. Threeat()calls those PRs added in the line and transformer fragments, and in the one-file spec, are rewritten fromby=snapshot_period, over=period, into=snapshottoby=snapshot_period[period], and the generated pages follow.#662 took #641's net diff against its merge base with
mainand applied it withgit apply --3way. #641 still carried #638 as 13 commits thatmainholds squashed, so a rebase would have applied #638 twice. #662's body lists how each conflict was resolved.Departures from the discussion
at(x, by=R[c])keeps a keyword. The discussion wroteat(cap, gen_zone[zone]), positional. limits.md says everything a modeller passes goes in a keyword value, so that a macro can pass it.by=is that keyword, and it reads the same insumandat.into=is a call-shape error, andR.cis a parse error.Considered and not done: a lookup as an index,
cap[gen_zone[zone]]The index form reads like the printed math,$\mathrm{cap}_{\mathrm{zone_of}(g)}$ , and it is shorter. It changes nothing that a file can say, and it has five costs:
name[...]here means "columns of this relation".cap[...]would mean "read this array at". The parser cannot tell which applies, and the reader has to.at(p * eff, by=…)would become(p * eff)[gen_bus[bus]]: a postfix operator on arithmetic, with a precedence against**and unary minus.sum.sum(x, over=d, by=R[c])andat(x, by=R[c])are the two directions through one table. The sameby=shows this.cap['north'],p[t - 1]andp[0]would read as if they should work. Those are label selection,shiftandposition.New refusals, each with the test that fails without it
Each guard was deleted in turn, the named test file run, and the tree restored. #641 has the guard table for the program change.
test_a_call_site_binds_the_relation_and_its_columns[names]test_a_formal_inside_a_selection_takes_a_name_and_nothing_elsetest_a_formal_stands_where_a_call_site_will_bind_it[a-relation-and-its-columns]test_a_selection_bound_into_arithmetic_is_refusedover=is refused, with the bare nametest_a_refused_call_is_not_read_by_the_call_around_it,[a-selection-in-over-beside-by],[a-selection-in-over-without-by]over=that is no column and no dimension[a-name-neither-a-column-nor-a-dimension][a-sum-reading-nothing-through-its-relation],[a-dimension-two-columns-are-over-is-no-column][at-through-a-bare-relation][a-read-of-a-key-column]test_dim_inference[and-so-does-its-lookup]test_a_relation_column_is_named_in_bracketstest_dim_inference[a-map-into-its-own-dimension-keeps-the-frame]test_a_grouping_that_sums_the_axis_away_couples_ittest_a_value_column_added_to_a_relation_changes_no_callis the case24c7354was made for. On the tree before it, the test fails with "over=snapshot matches 2 columns of 'rep_of'".Two asserts in
dimensions.pyfrom #799 have no test that fails without them: a sum over a join closes exactlyjoin.columns.axes, and a join that opens axes is read only by the sum that closes them. The resolver makes both true, and no probe builds such a node by hand.Coverage that moved
at(zone_cap, by=gen_zone[zone]), wherezone_caplacks the key'ssnapshot, andat(load, by=diag[rep]), whererepand the key sharesnapshot. Both are intest_a_lookup_joins_on_the_key_columns_the_operand_carries_and_the_read_does_not_match, with their frames.over=are gone with it. "matches 2 columns" cannot arise from a column name. "over= and by= name two tables" cannot be written. "sums away the dim it groups onto" is now the case "over= and by= both name the column",[over-and-by-the-same-column].by=[lk, lk2]is refused as "takes columns of one relation".within=now carries the relation.GroupSumandPullbacktests readJoin.tests/test_lowering.pyandtests/test_separability.pytake feat(program): a sum through a relation is a sum over the axes its join opens #641's names and messages.tests/test_dimensions.pycompares frames to string sets. The self-map test assertsnode.over == (Axis('snapshot', Column('rep_of', 'snapshot')),)and the frame of the sum, and no longer the frame of the bare join, which no longer exists.tests/test_lowering.pyandtests/test_expansion.pybuildSumwithAxisvalues.tests/typesetting/test_golden.pylistsAxisandColumninCARRIERS.Generated files
tools.schema,tools.gallery,tools.notation,tools.spec_math,tools.home_math,tools.expansion_mathandtests.typesetting.goldenwere run again after each merge ofmainand after each later commit. The schema carries the relation block's docstring; the three maintenance pages carryover=start; the notation page's heading reads "Shift and lookup in a condition". The one golden change againstmainis the legend'sshift(within=relation[c]).Gates, on
9d68a67pixi run lintpixi run testpixi run typecheckdocs-build,compile-texdocs.python.organd the tectonic bundle host.render-texrendered all 55 documents. CI runs both.Not done
JoinColumns,_role_name,key_roles).JoinColumns.kept_dimshas no reader. That cleanup is separate.over=are summed after the group-by, asSum(Sum(Join(x)), plain). Summing them before the join would makesum(lim * p, over=[generator, bus], by=gen_bus[bus])legal, where today the clash refusal fires. That is a language decision, separate from this PR.mean(x, over=a, by=R[b])would lower the same way. It needs a language decision first: whether an empty group is absent.GroupSum,Pullback,DirectionandPulledBackPredicateare gone, andSum.overholdsAxisvalues.🤖 Generated with Claude Code
https://claude.ai/code/session_01MKjqJocyxdRYvx7xin6NCM
https://claude.ai/code/session_01YVJQzL5G4boHcJDwrhgfag