2
$\begingroup$

A few languages, most notably Rust, have tuple-indexing syntax that looks like this:

tuple.0 // Access the first element of tuple

This syntax is quite similar to that for property access:

foo.bar()

Is it better to implement this as a separate parser rule, or to parse it as a property access, and add some special handling for the tuple type internally so that it returns the appropriate value?

$\endgroup$
9
  • 1
    $\begingroup$ What if it's a tuple of functions? You could call foo.0() potentially $\endgroup$ Commented May 31, 2023 at 14:28
  • $\begingroup$ It's more similar to field accesses like foo.bar, no? $\endgroup$
    – kaya3
    Commented May 31, 2023 at 14:29
  • 1
    $\begingroup$ There are languages like Java where arbitrary expressions like foo.bar can't be called with (), so a method call foo.bar() is an irreducible expression like MethodCall(Ident("foo"), "bar", []); and there are languages like Python where foo.bar() is Call(Attr(Ident("foo"), "bar"), []), i.e. calling the result of the property access foo.bar. Then there are languages like Javascript where foo.bar() could be parsed like the latter but isn't because it's semantically different from (foo.bar)(). I think the ambiguity only occurs in syntaxes like Javascript's. $\endgroup$
    – kaya3
    Commented May 31, 2023 at 14:36
  • 2
    $\begingroup$ @kaya3 Where does ECMAScript make that distinction? §13.3.6.1 says a call is a MemberExpression followed by arguments, and a MemberExpression can be a member access (x.f) or a PrimaryExpression, which includes identifiers and parenthesised expressions. It also says the MemberExpression is evaluated first. $\endgroup$
    – Longinus
    Commented Jun 2, 2023 at 2:38
  • 1
    $\begingroup$ @Longinus I was mistaken, the form that's semantically different in Javascript is let m = foo.bar; m(); or as an expression using the comma operator, (0, foo.bar)(). The difference is between a CallMemberExpression and a CallExpression, which are two different parse rules even though a CallMemberExpression could be represented instead as a CallExpression with a MemberExpression as its first child. The semantic difference is in the binding of this when the function body executes. $\endgroup$
    – kaya3
    Commented Jun 2, 2023 at 12:09

3 Answers 3

5
$\begingroup$

One thing to note about this syntax for tuple field accesses, is that it can clash with the lexical syntax for floating-point or decimal numbers. In many languages (e.g. Java, Javascript and Python), .1 is a single token which means the number 0.1.

In this case, the source x.1 will be lexed as two tokens: an identifier x and a number .1; on the other hand, x . 1 will be lexed as three tokens. So the parser will need to handle both cases; this is messy, and possibly even ambiguous:

  • If your language has function calls without parentheses, then f .1 also means calling the function f with the argument .1.
  • If your language has casts with parentheses, then (T) .1 also means converting .1 to type T.

Therefore if you want this syntax for tuple field accesses, it's probably best to not allow decimal literals to begin with a decimal point. Rust, for example, gives the following syntax error if you write .1 instead of 0.1 where a number is expected:

error: float literals must have an integer part

$\endgroup$
2
$\begingroup$

You only need to parse it differently if it would be "in the cracks" between method accesses and field accesses. If you don't have that distinction you can parse them the same way.

In a language like Python where property access and method access are the same, simply allowing properties to start with a number during the parsing stage would be enough.

On the other hand, in a language where the difference between properties and methods needs to be known at parse time you need a more complex grammar. You would typically have a separate parser rule for all 4 cases. Something like:

method_call := expression "." identifier "(" argument_spec ")"
attribute_access := expression "." identifier
tuple_attribute_access := expression "." number
function_call := "(" expression ")" "(" argument_spec ")" | identifier "(" argument_spec ")"

There is no real possible confusion between method calls and attribute access. But the confusion between function calls combined with function calls and method calls requires breaking the syntax up in many different blocks, and thus will require special casing the tuple access.

This is why Rust allows tuple.0() but not struct.attribute().

If you don't have methods, disambiguate method vs property accesses later than parsing, or use a different syntax for methods than properties, you may not need to special case tuple access.

$\endgroup$
7
  • $\begingroup$ I don't think method calls and field-function calls must be handled separately in all languages. Not saying they mustn't at all, but I wouldn't claim that as the general case $\endgroup$
    – abel1502
    Commented May 31, 2023 at 14:44
  • 1
    $\begingroup$ @abel1502 Like I said in my last paragraph if it's not handled separately you don't need to handle tuple accesses separately either $\endgroup$ Commented May 31, 2023 at 14:45
  • 1
    $\begingroup$ I'm not sure Python technically fits into any of the carve-outs in the last paragraph ─ it does have methods, it doesn't disambiguate methods vs. property accesses at all (because method accesses are property accesses), and it doesn't use a different syntax. In Python, if foo is a method of obj's class, then the expression obj.foo evaluates to a bound method on the instance. Well, CPython has special bytecode instructions LOAD_METHOD and CALL_METHOD for it for performance reasons, but that's an implementation detail; these aren't part of the language's semantics. $\endgroup$
    – kaya3
    Commented May 31, 2023 at 14:50
  • 1
    $\begingroup$ @kaya3 it falls under "disambiguate later than parsing" $\endgroup$ Commented May 31, 2023 at 14:51
  • $\begingroup$ In python methods and fields are parsed the exact same way but only at runtime is it decided if it's a method or not. The method could be converted to a field at runtime through various means $\endgroup$ Commented May 31, 2023 at 14:52
1
$\begingroup$

Unless the language defines something completely weird and orthogonal to other forms of accessing members, I'd use a parse rule beside the regular member access and create a tuple access AST node that has a common super type with the member access AST node. In essence, it's just a record with implicit names taken from integer literals.

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .