myst-lang / myst

A structured, dynamic, general-purpose language.
http://myst-lang.org
MIT License
119 stars 17 forks source link

[RFC] Consider adding `callinfo` for introspecting method calls #189

Open faultyserver opened 6 years ago

faultyserver commented 6 years ago

callinfo would be a language-managed variable, similar to self, that provides some introspective information about the caller of a method. As a language variable, callinfo would also be added as a new keyword.

Having access to meta information about the caller of a method is primarily helpful for debugging and tooling inside of Myst. The most obvious usecases are the Spec and Assert libraries.

Most spec libraries provide location information about failing specs to help users locate them quickly and easily. In Myst, this isn't a practical possibility yet, because the library has no way of accessing that location information without requiring the user to pass in __FILE__ and __LINE__ values for every it block.

Adding callinfo would provide that location information to these modules automatically, without any burden on the user. It would also avoid having conditional semantics for the magic constants, which I've never particularly liked.

Semantics

Referencing callinfo outside of a method definition will always return nil.

Inside of a method definition, callinfo returns a Map with these entries:

For simplicity, this Map is newly-created every time callinfo is invoked. This ensures both "psuedo-immutability", and that the values are accurate for the current call.

Example usage

Retrieving the location information of a Call using callinfo could look something like this:

def foo(a, b, c)
  {line: line, file: file} = callinfo
  STDOUT.puts("line <(line)>, file <(file)>")
end

foo(1, 2, 3) #=> line 6, file /Users/.../foo.mt
Jens0512 commented 6 years ago

In Crystal, you can define methods using things like this, like this:

put_line
def put_line(line=__LINE__)
    puts "Called from line #{line}"
end

Which outputs: Called from line 1 (https://play.crystal-lang.org/#/r/3xak). Is this not possible in Myst? If it is not, making it so, seems sufficient, and much more effective, to me.

I mean, this callinfo proposed is just a helper for defining methods like in the example, if this callinfo needs to be set for every call (which it has if I am not mistaken), I think we should be very careful. Take a recursive method like factorial, if the method calls itself a thousand times, wouldn't the stack have a thousand and one callinfos? factorial doesn't even need any callinfo.

I have by no means any proper proof that this will have a practical effect on performance, but I think that we must be careful about this kind of thing, and that there is no need for calls to contain this kind of callinfo without it being explicitly stated like in the examlpe.

Jens0512 commented 6 years ago

Maybe some sort of tag however can make this callinfo avaible, like

@[WithCallInfo]
def put_line
    STDOUT.puts(callinfo[:line])
end

(This example was just the first thing that popped into mind, and I have not thought properly about it at all. And I know we have no such attributes currently) EDIT: Given some thought; bad idea XD

faultyserver commented 6 years ago

I would really rather avoid re-using __LINE__ and __FILE__, since it puts a condition on what they mean depending on the context (i.e., it becomes "__LINE__ is replaced by the line number that the token appears on, except when it's in a method definition, where it means the caller's line number"). I don't like that overload, and it would be the only overload like that in the entire language.

As for how it gets set, it should be possible to calculate the value on the fly, without any need for a stack. Even if there was, it would probably be part of the callstack, which already tracks every Call that gets made, so the overhead would be pretty minimal.

Jens0512 commented 6 years ago

__LINE__ is replaced by the line number that the token appears on, except when it's in a method definition, where it means the caller's line number.

I have always seen it as: the token appears on the line number its called from, even though not passed, as it is the default parameter, like this:

foo (line:__LINE__) # Line 1
        # ^^^^^^^^ Token appears here
foo#(line:__LINE__)   Line 3
        # ^^^^^^^^ And here, at least that is how I have understood default parameter values

def foo(line=__LINE__); ... ; end

Personally at least, I think having it like that is pretty rational.

But, I agree now, seing how every Node already has their #location; not much reason not to have this callinfo keyword.

faultyserver commented 6 years ago

Huh, I hadn't seen that description of it before. That's certainly a little bit nicer than what I wrote. I'm still not a huge fan of those semantics, either, cause they are still a special case. For example, if you reference an instance variable as a default value, it still gets evaluated in the context where the method is defined, while __LINE__ is where the method is called.

There's definitely a need for discussion about what default arguments should look like, and maybe there's some nicer solution that will come with that, but for now I'd like to avoid overloading the constant (visually and/or implementation wise).

I'd be down to change callinfo into something like __CALLER__ to keep the look/feel of the magic constant, though.

Jens0512 commented 6 years ago

Oh god, please not __CALLER__ 😄 it’d look absolutely terrible.