Skip to content

untyped arguments #1114

@metagn

Description

@metagn

From the example in #1044:

template useCpuRegisters(body: untyped) =
  var p {.inject.} = r.p
  let eof {.inject.} = r.eof
  body
  r.p = p # store back

proc skipWhitespace(r: var Reader) =
  useCpuRegisters:
    while p < eof:
      case ^p

When the call to useCpuRegisters is being semchecked, the argument is semchecked by itself, which generates errors due to p and eof not being defined yet. This will not cause problems in overload resolution and the untyped parameter will just accept it, however when the template is instantiated the error will remain in the AST and it won't compile.

I think for maximum simplicity while retaining most of the compatibility, instead of attempting to "walk back" the errors (which would either be done for typed as well or have to specifically be differentiated), we can just store and pass the original argument AST before semchecking to the untyped param rather than the semchecked AST which contains errors (which get ignored).

Then the only problem is that the semchecking of the untyped param might have side effects. This is mostly a problem for macros but it means we need something like the "shadow scope" behavior from the original compiler for symbols defined in untyped args:

foo(
  (let x = 123; 456),
  x)
discard x

In original Nim the "shadow scope" is fully merged to the above scope on a successful match. This might cause a problem here if the first argument of foo is untyped, it might not get added to the scope above, and the use of x in the second argument might become invalid as well. We could maybe always open a new scope for call arguments to prevent this from being possible, or maybe a new scope for each argument. Both of these would break code but the first one would not break as much, using symbols defined in other arguments is probably more common than using a symbol defined in a call argument after the call, if either of these happen at all.

Edit: Maybe something like this is a common example:

if (let x = 123; x > 0) and (let y = 456; y > 0) and y > x:
  ...
# although you could also write this as: (let x = 123; x > 0 and (let y = 456; y > 0 and y > x))

If we open a scope inside a call but not for each individual argument, then this would break because it parses as (a and b) and c, but it would work if it was written as a and (b and c). This is too weird imo. Not to mention we might want to use x and y later which is not possible at all with this.

Maybe we can track the introduced symbols in each argument, then only merge them to the upper scope if they didn't match as an untyped param.

If side effects of semchecking still end up being too much of a problem, with type bound ops being added after matching as in #1112, the "lazy semchecking" behavior of the old compiler would not necessarily be incompatible here anymore. It could be added for compat mode, and there are still tweaks that could be made to it to mitigate the old issues with it (for example nim-lang/Nim#24555).

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions