Skip to content

Conversation

@ggreif
Copy link
Contributor

@ggreif ggreif commented Feb 27, 2025

TODO:

  • size could be more efficient with a loop
  • repeat ditto
  • tabulate is not tail-recursive (but constructor tailed)
  • concat is not tail-recursive (but constructor tailed)
  • merge is not tail-recursive (but constructor tailed)
  • fromIter is not tail-recursive (but constructor tailed)
  • reverse looks tail recursive (could be more efficient with a loop)
  • mapResult needs another look (somewhat convoluted in legacy base)

@ggreif ggreif requested a review from a team as a code owner February 27, 2025 16:52
@github-actions
Copy link

github-actions bot commented Feb 27, 2025

No description provided.

@ggreif ggreif force-pushed the pure/List-tailrec branch 2 times, most recently from af53a1f to 772d184 Compare February 27, 2025 17:10
@ggreif ggreif marked this pull request as draft February 27, 2025 17:36
@ggreif ggreif force-pushed the pure/List-tailrec branch 2 times, most recently from 7ed377d to f4ee7e2 Compare February 27, 2025 17:53
@@ -1,3 +1,5 @@
// @testmode wasi
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this just to speed up the tests? We might want to run this with the interpreter (default) to check for stack overflows.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To me it appeared that the interpreter was more tolerant in terms stack. Maybe we should have a // @testmode both directive, if not already possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll remove just before merging. Then we'll have both ways tested. But we should think about a combined strategy (maybe nightlies) for the future.

src/pure/List.mo Outdated
list,
func(item : T) {
if first {
if (text.size() > 1) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Are you sure text.size() is constant time when using ropes (in compiled code)? I don't recall off-hand. If not, revert!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point! I'll revert!

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 no 0(1), I checked)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Though, compiling the size() > 1 as a unit would be constant time even in the presence of ropes. I expect users committing the same mistake in real life, and getting quadratic runtime on code like this (i.e. comparisons of size() with small constants).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wait, do ropes carry a size around? That would help certainly :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice:

// Layout of a concat node:
//
// ┌────────────┬─────────┬───────┬───────┐
// │ obj header │ n_bytes │ text1 │ text2 │
// └────────────┴─────────┴───────┴───────┘

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So this is resolved in the good way!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reverting, those are bytes, not chars.

Comment on lines +61 to +66
public func size<T>(list : List<T>) : Nat = (
func go(n : Nat, list : List<T>) : Nat = switch list {
case (?(_, t)) go(n + 1, t);
case null n
}
)(0, list);
Copy link
Contributor

@crusso crusso Feb 28, 2025

Choose a reason for hiding this comment

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

Suggested change
public func size<T>(list : List<T>) : Nat = (
func go(n : Nat, list : List<T>) : Nat = switch list {
case (?(_, t)) go(n + 1, t);
case null n
}
)(0, list);
public func size<T>(list : List<T>) : Nat = {
var size = 0;
var cur = list;
loop {
switch tmp {
case (?(_, next)) {
size += 1;
cur:= next
};
case null { return size }
}
}
}

Is probably cheaper (no call to a helper).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, I was hoping that the compiler could create the same (or better) code by TCO. I.e. not sure if var allocates in this example (it shouldn't).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did the same transformation in repeat, though.

Copy link
Contributor

@crusso crusso Feb 28, 2025

Choose a reason for hiding this comment

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

reverse needs it too. TCO won't inline the auxiliary function so ...

Copy link
Contributor Author

@ggreif ggreif Feb 28, 2025

Choose a reason for hiding this comment

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

I actually test reverse for stack overflow and I haven't seen any 😖
With depth 100_000!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But for TCO one only have to turn the body of the helper into a loop, no need to inline it into the wrapper. Or am I mistaken?

Copy link
Contributor Author

@ggreif ggreif Feb 28, 2025

Choose a reason for hiding this comment

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

Of course the interpreter won't do TCO, and if we want constant stack there we'll have to manually TCO anyway... Or maybe due to CPS the interpreter will do the right thing. Questions, questions!

Copy link
Contributor

@crusso crusso Feb 28, 2025

Choose a reason for hiding this comment

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

I actually test reverse for stack overflow and I haven't seen any 😖 With depth 100_000!

If you test in the interpreter you won't see because the interpreter is in CPS... On wasm, you should see

Copy link
Contributor Author

@ggreif ggreif Feb 28, 2025

Choose a reason for hiding this comment

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

I test on Wasm, and don't see it :-) I'll look at IR/Wasm soon.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, you reverse is tail recursive, but it allocates and calls a helper that is probably slower than just using a loop and a temp, without the additional function call.

@crusso
Copy link
Contributor

crusso commented Feb 28, 2025

There's more TODO (see #195)

l := ?(f i, l);
i += 1
};
reverse l
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a shame. A clever optimisation would spot the linear heap allocation discipline arising from the recursive algorithm (did somebody say abstract interpretation?) and pre-allocate the list nodes. Then fill them in like an array...

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, that's called tail-modulo-cons optimization but we don't have it.

@crusso
Copy link
Contributor

crusso commented Feb 28, 2025

Is this still a draft?

@ggreif ggreif marked this pull request as ready for review February 28, 2025 18:42
@ggreif ggreif changed the title chore: make pure/List mostly tail-recursive chore: make pure/List mostly tail-recursive Feb 28, 2025
@ggreif ggreif merged commit b709b70 into main Feb 28, 2025
8 checks passed
@ggreif ggreif deleted the pure/List-tailrec branch February 28, 2025 19:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants