Skip to content

Conversation

@The0x539
Copy link
Contributor

@The0x539 The0x539 commented Sep 17, 2025

Description

Yeah, this is pretty big. I wasn't able to identify a good "midway checkpoint", and I definitely regret not rejoining the chatroom before/during my work to discuss it. I hope this goes over well regardless.

The refactors to this module while I was away were very nice, and definitely helped pave the way for these changes (especially getting rid of parent pointers).

I'm perfectly willing to squash+rebase if/when this is just about ready to merge, but I'm keeping it as the original commits at first on the off chance it helps whatsoever with reading the diff.

Big-picture summary:

  • The FreestandingArgumentList type is gone. Only the freestanding_arguments field on Populator remains.
  • List nodes have been demoted from structs to humble typed aliases for boxed slices. Not only does this reduce code complexity, it allows for less indirection, most visibly in the Kind enum.
  • Migrated from a fn visit_mut(populator: &mut Populator, node: &mut Node) pattern to fn parse(pop: &mut Populator) -> Node. KindMut, NodeVisitorMut, and many other mut-flavored ast internals evaporated as a result.
    • Parsing of leaf nodes got a fair bit more straightforward.
    • Parsing of branch nodes still has a control flow reminiscent of the old style, due to the parser's unique error handling mechanism, but the dispatch for traversing the tree while constructing it should all be static now.
  • The trait-impl macros are now derive-style while still being defined with macro_rules!, with the help of macro_rules_attribute. (Isolated to smaller PR and merged.)
  • JobList is still a list of JobConjunctions, but that might be an easier situation to unravel now.

TODOs:

(No intended user-facing changes.)

@zanchey zanchey added this to the fish-future milestone Sep 17, 2025
"fish-printf",
"libc",
"lru",
"macro_rules_attribute",
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm perfectly willing to squash+rebase if/when this is just about
ready to merge, but I'm keeping it as the original commits at
first on the off chance it helps whatsoever with reading the diff.

We don't treat pre-commit and post-commit review differently.
We optimize for both the same way. We're not gonna do a big-bang squash
but rather move (incrementally) towards a history that is good enough
and merge that.

]

[[package]]
name = "macro_rules_attribute"
Copy link
Contributor

Choose a reason for hiding this comment

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

I have not finished reviewing everything from
"ast: initial Parse trait rework" onwards,
I suspect these things can be made a bit easier to review
(the fact that there's things like back and forth renames left in history makes
me careful about investing time in review until these issues are fixed.
There might be other ways to efficiently explain the Parse-trait changes to
me but minimal and atomic diffs are a tried and tested way.)
So I'm leaving some comments now.
What I've read through looks good.

fn kind(&self) -> Kind<'_>;

/// Return the kind of this node, as a mutable reference.
fn kind_mut(&mut self) -> KindMut<'_>;
Copy link
Contributor

Choose a reason for hiding this comment

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

7ecd2d0 (ast: move kind_mut back into AcceptorMut, 2025-09-03)
introduces LIST_KIND only to delete it later. Should probably
remove it from history outright (to make the sum of deltas smaller).

Also, this seems to be doing multiple things at once:

  • moving kind_mut() to AcceptorMut
  • replacing empty list objects with LIST_KIND
  • demoting JobList? (does that belong to a different one?)

Can it be split up to separate conerns?

JobList(&'a JobList),
}

pub enum KindMut<'a> {
Copy link
Contributor

Choose a reason for hiding this comment

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

stray intermediate println!("parsing an arg"); in this commit


/// A freestanding_argument_list is equivalent to a normal argument list, except it may contain
/// TOK_END (newlines, and even semicolons, for historical reasons).
/// In practice the tok_ends are ignored by fish code so we do not bother to store them.
Copy link
Contributor

Choose a reason for hiding this comment

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

it's weird that there was no difference to ArgumentList,
because ArgumentList definitely should not contain newlines or semicolons.
AFAICT that difference is not touched by your changes, so that's fine.

But we should definitely consider preserving the comment describing this historical weirdness.
Can you move the comment to fn parse_argument_list ?

@@ -0,0 +1,327 @@
/// Implement the node trait.
macro_rules! Node {
Copy link
Contributor

Choose a reason for hiding this comment

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

I haven't yet finished reviewing,
so I don't know what the final module layout should look like,
but I'm not sure if the benefit to moving macros to a separate
file is worth tearing it apart from ast.rs.
IIRC, none can live without the other, so I'm not sure if it helps.

Of course we do want to look at extracting a fish-syntax crate in
future, which should be able to parse fish code.
So we should do whatever helps but this seems unrelated

$contents:ty => $name:ident {
chomp_newlines: $newlines:expr,
chomp_semis: $semis:expr,
$(stop_unwind: $unwind:expr,)?
Copy link
Contributor

Choose a reason for hiding this comment

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

The name of this is let $pop_:ident; in an earlier commit, which
is confusing to read. Can you fix that, i.e. use the better name from
the start?

fn kind(&self) -> Kind<'_> {
N::list_kind(self)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

(Just a brain dump:)
I wonder if we really want every Box<[N]> to implement Node?
That would make it easy to construct a new list node from existing
nodes, without going through the parser.
I guess that's not a problem in practice.
But if we actually wanted to guard this, we could use the newtype
pattern (similar to what we had before, but make it generic, i.e.

struct List<N> ( Box<[N]> )

.is_err()
{
for job in &brace_statement.jobs {
for job in &brace_statement.jobs[..] {
Copy link
Contributor

Choose a reason for hiding this comment

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

this (a0d9c23 (ast: fix MSRV breakage, 2025-09-11))
should be squashed, so that intermediate commits still build with MSRV

assert_eq!(line_offset, expected);
}

let pipelines: Vec<_> = ps.ast.walk().filter_map(ast::JobPipeline::cast).collect();
Copy link
Contributor

Choose a reason for hiding this comment

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

this commit should be squashed into the commit that removes Castable
(or precede that one),
so that every commit improves consistency

@The0x539
Copy link
Contributor Author

The0x539 commented Oct 3, 2025

Apologies for my absence. I've been meaning to get back to this, but my attention has been elsewhere and difficult to wrangle.

Regarding the final state of the code:

it's weird that there was no difference to ArgumentList,

Yes, I was also surprised by this. It seems that the important behavior has actually been governed by the freestanding_arguments field for whatever amount of time. Either way, the tests still pass, and if we're not confident, we can add more test coverage. (I'd have maybe done so myself if I had a better understanding of the behavior at hand.)

I'm not sure if the benefit to moving macros to a separate file is worth tearing it apart from ast.rs.
IIRC, none can live without the other, so I'm not sure if it helps.

In my personal experience, ast.rs was a long enough file that it was pretty difficult to work with, cognitively. Splitting it up into a couple sub-concerns helped quite a bit. I want there to be a clear place to go to see the structs that make up the AST and the relationship between them, but that's mixed in with all the Populator stuff and the macros with their really dense syntax.

I wonder if we really want every Box<[N]> to implement Node?

I think the NodeKind enum, and the fact that every &impl Node can be converted to one, is the limiting factor for being able to do this.

Regarding the commit history:
This is kind of what's kept me away from this PR for the past week. I very much appreciate all the scrutiny, and I'd love to have a cleaner history for this that can be merged as-is, but actually the prospect of actually doing that tests the limits of my Git abilities - really, I'm not sure where to even start.
I'd appreciate some help on that front, so as to avoid needing to remake all the changes by hand on a fresh branch.

.ast
.walk()
.filter_map(|node| node.kind().try_into().ok())
.collect();
Copy link
Contributor

Choose a reason for hiding this comment

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

Regarding the final state of the code:

it's weird that there was no difference to ArgumentList,

Yes, I was also surprised by this. It seems that the important behavior
has actually been governed by the freestanding_arguments field for
whatever amount of time. Either way, the tests still pass, and if we're
not confident, we can add more test coverage. (I'd have maybe done so
myself if I had a better understanding of the behavior at hand.)

if they are using the same code, there's nothing to test.
IIRC that commit was obviously correct, maybe you can send a separate PR with only that.

I'm not sure if the benefit to moving macros to a separate file is worth tearing it apart from ast.rs.
IIRC, none can live without the other, so I'm not sure if it helps.

In my personal experience, ast.rs was a long enough file that it was
pretty difficult to work with, cognitively.
Splitting it up into a couple
sub-concerns helped quite a bit. I want there to be a clear place to go to
see the structs that make up the AST and the relationship between them,
but that's mixed in with all the Populator stuff and the macros with
their really dense syntax.

Hmm, I'd need to take a look.
Whatever the outcome may be, extracting this code to a different file is unrelated to the main point of the PR, so to make review easier and faster, move it to a separate PR.

I wonder if we really want every Box<[N]> to implement Node?

I think the NodeKind enum, and the fact that every &impl Node can be
converted to one, is the limiting factor for being able to do this.

but we could hide the fact behind a newtype wrapper around Box<[N]> I tried to sketch.
Then other modules could not directly create a new AST node by putting that into a box

Regarding the commit history:
This is kind of what's kept me away from this PR for the past week. I very much appreciate all the scrutiny, and I'd love to have a cleaner history for this that can be merged as-is, but actually the prospect of actually doing that tests the limits of my Git abilities - really, I'm not sure where to even start.
I'd appreciate some help on that front, so as to avoid needing to remake all the changes by hand on a fresh branch.

For one thing, create minimal PRs, then things become easier.
For example move the first three commits to their own PR. Worry about the rest later.

some helpful docs for history rewriting are:

Creating a fresh branch shouldn't be necessary because your changes are close to ready.
You can get there incrementally by tweaking/removing individual commits.

For example I'd start with getting the first three commits in,
while keeping your other changes rebased on top of that.
If you use git, that's maybe

git branch use-macro_rules_attribute 719de30e547 # (ast: use macro_rules_attribute for the Acceptor trait, 2025-09-03)
# rebase to update to lastest origin/master
git rebase --update-refs
# interactive rebase; stop at the first three commits to address any review comments
git rebase --update-refs --interactive
git push $your_remote_name use-macro_rules_attribute
gh pr create --head The0x539:use-macro_rules_attribute

When using "git rebase --update-refs --interactive"
to edit some earlier commits, then you might see some conflicts in later commits.
Take a careful look at each conflict (I recommend using "git config merge.conflictStyle diff3")
until you understand why it happened; then it should be easy how to resolve it.

An the beauty of this approach is that, conflicts only happen to a small minority of changes, where Git can't automatically do the right thing.
So this approach minimizes the time spent "re-doing things by hand".

Let me know what if anything you need.
History cleanup problems are always man-made, so they are always solvable.

@The0x539 The0x539 force-pushed the parse-trait-draft2 branch from 5c30198 to 5c3010b Compare October 3, 2025 20:53
// If we have seen a "--" argument, color all options from then on as normal arguments.
let mut have_dashdash = false;
for v in &stmt.args_or_redirs {
for v in &stmt.args_or_redirs[..] {
Copy link
Contributor

Choose a reason for hiding this comment

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

can you extract some more of the unrelated changes to separate PRs?
BTW we also bumped the MSRV, so the changes in a0d9c23 (ast: fix MSRV breakage, 2025-09-11) are probably no longer needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Very nice, that should be helpful.
And sorry, yes, I've been meaning to isolate more changes, but I've been quite busy with other things. Hope to make something happen this week.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants