Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convenient syntax for shuffling (incl. removal) of values on the stack #1

Open
dumblob opened this issue Jun 4, 2021 · 9 comments
Open

Comments

@dumblob
Copy link

dumblob commented Jun 4, 2021

I like how Morty approaches the "implicit" stack content (with comments in parens). There is one more thing which is verbose in stack languages and at the same time potentially inefficient. Namely shuffling of values on the stack if they're not in the order one needs them.

I was looking for a way how to solve it and found Quark with its stack manipulation syntax.

I think it's worth thinking about making such shuffling easier for the user (actually the fact, that all "functions" in Morty depend on the order of arguments becomes the main driver behind this need).

Another idea would be to not introduce generic stack manipulation syntax, but rather a built-in function with variable number of arguments which would do the shuffling.

Yet another idea would be to simply allow special shuffling syntax only at the place when calling a function. Basically saying "if the function is called without explicit arguments, it behaves like standard combinator, but if it's called with at least one explicit argument it's regarded as these were first popped from the stack and then passed explicitly as arguments (this latter case would allow for compile-time check whether the number of arguments is correct which could improve general app safety).


Btw. there are some other more or less crazy ideas in henrystanley/Quark#2 . Feel free to take a look 😉.

@dumblob
Copy link
Author

dumblob commented Jun 4, 2021

The "yet another idea" would then look like the following.
original:

def poly2 (x a b c -- y) :c :b :a :x (capture params)
    x x mul a mul (axx)
    x b mul (axx bx) add
    c add
end
...
(use it)
... really ugly looking and hardly understandable swap/dup/pop/... chain here
poly2

with convenient shuffling:

def poly2 (x a b c -- y) :c :b :a :x (capture params)
    x x mul a mul (axx)
    x b mul (axx bx) add
    c add
end
...
(use it while shuffling with stack args to achieve correct mapping - the underscore means "throw the value away from stack")
poly2 :c :a _ :b :x

Btw. if lambdas allowed explicit arguments like def ... functions do, then no special syntax for shuffling would be needed as one would simply write a lambda which would only return the given arguments in the desired order.

Another question (admittedly only partially related and significantly less important than shuffling) is how default argument values could be supported without introducing the ferocious null/nil/none aka billion dollar mistake. They are very important especially for higher-level APIs. In this context it's good to note, that some languages solve it by syntax sugar around passing a "spliced" map (or struct if the language supports default values for struct members in struct definition).

@mobarski
Copy link
Owner

mobarski commented Jun 6, 2021

Btw. if lambdas allowed explicit arguments like def ... functions do, then no special syntax for shuffling would be needed as one would simply write a lambda which would only return the given arguments in the desired order.

Interesting idea. For sure I'll think about it to make stack shuffling easier. It already should be easy as variables can be captured not only at the beginning of the function but anywhere in the code. You can:

def foo
 ( ... )
 ( x b c a ) :a :c :b (x) a b c poly2
 ( ... )

but this will overwrite variables x a b c if they were defined before in the foo function. With lambdas, this will not happen.

Another question (admittedly only partially related and significantly less important than shuffling) is how default argument values could be supported without introducing the ferocious null/nil/none aka billion dollar mistake.

I don't have yet any plans for handling empty/missing values.

BTW: thanks for the info about Quark.

@mobarski
Copy link
Owner

mobarski commented Jun 13, 2021

I'm thinking about adding specialized stack shuffling syntax similar to Quark.
The above example will look like this:

def poly2 (xabc--y) ... end

def foo
  ( ... )
  (xbca) xbca|xabc poly2 (y)
  ( ... )

It will support only one letter aliases BUT I think that the syntax is:

  • clean
  • compact
  • concatenative

@dumblob
Copy link
Author

dumblob commented Jun 13, 2021

That's great news, thanks!

It will support only one letter aliases

Hm, could this be lifted? Maybe xbca|xbc (a was thrown away) could become something like {my1}b{my2}{my3}|{my1}b{my2} or perhaps my1|b|my2|my3~my1|b|my2 or my1|b|my2|my3//my1|b|my2 or my1|b|my2|my3++my1|b|my2?

This is to use already existing names as in my opinion that'll be almost a necessity considering Morty's coolest feature are "argument names as variables". My assumption is argument names will be ubiquitous as that's more or less the easiest way how to nicely structure programs written in Morty (and besides everybody loves names 😉).

What do you think?

@mobarski
Copy link
Owner

Hmm, I'll think about it. Efficient stack machine code should avoid shuffling BUT since it makes programs more readable I want to provide some shuffling mechanisms.
As for using names there are already two other mechanisms for easy shuffling:
a) local variables
b) lambdas with local variables

Mechanism a) can be used when values are already captured into named variables.
Mechanism b) can always be used

Your example with mechanism b) would look like
[ :my3 :my2 :b :my1 my1 b my2 ] call
With the new mechanism it would look like:
abcd|abc
Given example might not be perfect as it can be replaced with a single drop.

When I was designing local variables I was thinking about allowing just function parametrs to be named.
With such restriction you won't have to reverse the order of the parametrs but you won't be able to create new variables in the function body.
I was also thinking about having both mechanism - named parametrs and local variables but it seamed against the simplicity principle.
I migh rethink that.

( named variables - current implementation )
def foo (a b c -- y) :c :b :a
    (... d) :d (...)
	a b mul c d mul add 
end

( named parameters )
def foo (a b c -- y) :a :b :c
    (...) a b mul c (... d) mul add 
end

( both - named params and local variables )
def foo < a b c -- y >
	(... d) :d (...)
	a b mul c d mul add
end

OR 

( both - named params and local variables )
def foo (a b c -- y) .a .b .c
	(... d) :d (...)
	a b mul c d mul add
end

OR other notation

@dumblob
Copy link
Author

dumblob commented Jun 20, 2021

[ :my3 :my2 :b :my1 my1 b my2 ] call

True. I forgot about this. Looks actually good to me though I'm afraid if this won't be optimized away by the compiler, it might be slower than abcd|abc. Or am I missing something? But if the compiler could optimize it away, I'd even consider abandoning abcd|abc syntax as it'd seem redundant.

Regarding "both - named params and local variables" functionality I'm a bit confused what the difference between a named param and a local variable is in this context. Could you clarify? Intuitively I'd say named params are local variables but their naming is enforced at the call site somehow (this is being usually used for default values of arguments). But I'm unsure you meant exactly this.

@mobarski
Copy link
Owner

mobarski commented Jun 20, 2021

it might be slower than abcd|abc. Or am I missing something?

Yes, local variables are not as easy to optimize. Named params on the other hand are easy to optimize (all named params can be captured in a single vm instruction) but still, shuffling expressions are the easiest to optimize.

I'm a bit confused what the difference between a named param and a local variable is in this context.

Named parameters can be captured only at the beginning of the function. The capture can be optimized (all params in one vm instruction) and more natural ordering can be used: function with the signature ( a b c -- y ) can capture named params in the same order as the signature .a .b .c while capturing the with local variables must be specified in the reversed order :c :b :a. Values captured in both ways act in the same way - VGET.X will get variable/param X of the local frame and VSET.X will set it.

With named params stack reshuffling with lambda functions will look more natural:

[ .my1 .b .my2 .my3   my1 my2 my3 ] call

( vs )

[ :my3 :my2 :b :my1   my1 my2 my3 ] call

( or abcd|acd using shuffling expression )

BTW: the "dot notation" is just an example - most likely the notation will be different.

@dumblob
Copy link
Author

dumblob commented Jun 21, 2021

Ah, now I see. Thanks for explanation!

In that case I'd prefer to support both named params as well as local variablles. But because named params can be "efficient enough" or even identically efficient, I'd abandon special syntax for shuffling.

@dumblob
Copy link
Author

dumblob commented Feb 2, 2023

@antirez I just read your readme in https://github.com/antirez/aocla and thought you might be interested in knowing there was definitely some prior art to the "local variables" 😉.

I am pretty sure many people came up with such ideas but not many tried to implement it and thus we might have not heard about their thoughts.

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

No branches or pull requests

2 participants