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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proof of Concept: explore per-module custom infix operators. #6076

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

cristianoc
Copy link
Collaborator

@cristianoc cristianoc commented Mar 15, 2023

Add local support for custom infix operators.

Properties:

  • Per-file, not dependent on project settings.
  • Opt-in by the user, not mandated (by e.g. a library).
  • No semantic change, just local view of the same ast.
  • Supports hover, and jump to definition. Or just scan the file visually to find the definition of symbols (no search codebase for e.g. "!" defined in some far away file)
  • No effect on parsing and printing performance on normal use.

Example:

let plus = (x, y) => x + y
let minus = (x, y) => x - y

@@infix(("馃榾", "plus"))
@@infix(("馃挬馃挬", "minus"))

let q = 3 馃榾 4 馃挬馃挬 5

The scope of @@infix is from where it appears until the end of the module (or file if at toplevel).
There's currently no restriction on what can be used as a symbol: any string including keywords. Restrictions can be placed later on, but the core mechanism is well defined regardless (just as with a macro mechanism).

Remove infix.add and infix.remove

infix.add should not affect the parser (only the printer)

Implement per-module scoping mechanism

The annotations only apply to the module they appear in.
@hoichi
Copy link

hoichi commented Mar 15, 2023

Out of curiosity, will this work?

module Foo = {
  let plus = (x, y) => x + y
  let minus = (x, y) => x - y

  @@infix(("馃榾", "plus"))
  @@infix(("馃挬馃挬", "minus"))
}

module Bar = {
  include Foo

  let q = 3 馃榾 4 馃挬馃挬 5  
}

@cristianoc
Copy link
Collaborator Author

cristianoc commented Mar 15, 2023

Out of curiosity, will this work?

module Foo = {
  let plus = (x, y) => x + y
  let minus = (x, y) => x - y

  @@infix(("馃榾", "plus"))
  @@infix(("馃挬馃挬", "minus"))
}

module Bar = {
  include Foo

  let q = 3 馃榾 4 馃挬馃挬 5  
}

No. In other words, the user decides, not the implementor of the module.

See the above property: "not dependent on project settings", and "no semantic change". Include across files, as I assume what the example intends to mean, would need to break both properties.

@mooreryan
Copy link

Couple of questions...(If it is better, I could move these questions in the discuss forum, to not clutter up the pull request.)

What are the precedence and associativity rules for custom infix ops? Will they match OCaml's rules?

So, include doesn't bring the operators into another module...what about open, will that bring operators into scope?

See the above property: "not dependent on project settings", and "no semantic change". Include across files, as I assume what the example intends to mean, would need to break both properties.

^ I'm not really sure what this means...the normal semantic/meaning of open/include is give me everything in the module I'm opening/including. So, making it so the operators will not be included when using include or open, isn't that what is changing the semantics of module inclusion?

@cristianoc
Copy link
Collaborator Author

cristianoc commented Mar 15, 2023

What are the precedence and associativity rules for custom infix ops? Will they match [OCaml's]

It's user-defined operators, not a set of canned predefined operators. So there's no set of rules to follow.
It's currently using left-associative like for ++, but making that controllable in the @@infix annotation should not be a problem.

So, include doesn't bring the operators into another module...what about open, will that bring operators into scope?

See the above property: "not dependent on project settings", and "no semantic change". Include across files, as I assume what the example intends to mean, would need to break both properties.

^ I'm not really sure what this means...the normal semantic/meaning of open/include is give me everything in the module I'm opening/including. So, making it so the operators will not be included when using include or open, isn't that what is changing the semantics of module inclusion?

OCaml includes values, modules and types from the included module. A module-level annotation (i.e. a thing starting with @@) is not one of those things. A good analogy is: ocaml include does not include the doc comments (which are represented internally as annotations).
No semantic change means that the ast is EXACTLY the same as if you were not using inifx, so by design there's no way to tell whether a file I'm using has some of those inside.

@cristianoc cristianoc changed the title Proof of Concept: explore per-file custom infix operators. Proof of Concept: explore per-module custom infix operators. Mar 15, 2023
@mooreryan
Copy link

Thanks for the clarification about the AST and the module level annotations (and the ocaml doc comments analogy). It makes sense to me now, what you are meaning.

When you say "making that controllable in the @@infix annotation", by "that" do you mean controlling precedence and associativity or just associativity? Or, am I just overthinking this, and the use of local infix ops it more a situation of, user shouldn't be making complicated infix ops and combining them in ways that you would even need to worry about it precedence and associativity?

@cristianoc
Copy link
Collaborator Author

cristianoc commented Mar 15, 2023

Thanks for the clarification about the AST and the module level annotations (and the ocaml doc comments analogy). It makes sense to me now, what you are meaning.

When you say "making that controllable in the @@infix annotation", by "that" do you mean controlling precedence and associativity or just associativity? Or, am I just overthinking this, and the use of local infix ops it more a situation of, user shouldn't be making complicated infix ops and combining them in ways that you would even need to worry about it precedence and associativity?

Both precedence and associativity could be made controllable. While associativity is standard, making sense of precedence would require documenting the way this is represented internally (what numbers are used for the language's operators). Both are trivial to do in terms of implementation, the question is documentation and complexity of use. It really depends on how frequently those controls would be needed. In doubt, it does not hurt to start small and add features gradually. Much easier than changing/removing later.

@mooreryan
Copy link

Thanks, I appreciate the explanation.

@ornamentist
Copy link

Would this change also allow more use of Unicode characters in identifiers? The use-case I'm thinking of is mathematical objects with widely accepted Unicode character representations (e.g. real numbers, set union, pi, tau, etc).

@DZakh
Copy link
Contributor

DZakh commented Mar 16, 2023

I wonder what's a use-case for this feature. I can't find any.

@ornamentist
Copy link

I would use it today if available in my tests and assertions about mathematical objects where there is already an accepted infix symbol for the operation (e.g. set union, logical equivalence, group operator, etc).

@cristianoc
Copy link
Collaborator Author

cristianoc commented Mar 16, 2023

Would this change also allow more use of Unicode characters in identifiers? The use-case I'm thinking of is mathematical objects with widely accepted Unicode character representations (e.g. real numbers, set union, pi, tau, etc).

A unicode variable identifier can be thought of as an "operator with zero arguments", so the same kind of mechanism would cover it. That said, this application seems very niche.

There's a separate question on whether it would be possible to extend the language with unicode identifiers (a semantic change). Technically, this is possible. However, they are not valid variable names in JS, so the language would have to encode somehow, and produce what effectively would look like garbled output.

String.unsafe_get scanner.src (scanner.offset + 3)
else hackyEOFChar

let make ~filename src =
{
filename;
src;
srcLen = String.length src;

Choose a reason for hiding this comment

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

Thanks 馃憤

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

Successfully merging this pull request may close these issues.

None yet

6 participants