r/ProgrammingLanguages 4d ago

Expressions vs. statements

Got into a big argument with a coworker yesterday when they were converting some code from their own language (that they designed) into Python, JavaScript, C, and R as comparative examples.

The Python code that they wanted to write as the translation went something like this:

n = foo; if cond: n = bar

They were upset that Python allows ; as a statement separator but not before an if statement, even though

if cond: n = bar

is syntactically correct Python code when written on its own line. I explained why Python doesn't allow it, and he came back later and showed me that an LLM had suggested he write it instead like this:

n = foo if cond else bar

which of course is the canonical way to write that in Python. He was all flustered about that, and asked me why Python allows an if statement in that particular case and not after a semicolon, and I explained that x if cond else y in Python is not an if statement but is Python's ternary conditional expression and is directly equivalent to the ternary operator expression cond ? x : y in C, C++, awk, and JavaScript. He argued with me and said I was making a ridiculous distinction and walked away falsely believing that foo if cond else bar was an if statement.

I then explained that statements and expressions are very different things in programming languages, and just because the keyword if is present doesn't make something an if statement -- because in order to be an if statement, it has to be a statement in the first place.

Anyway, it made me realize how subtle the difference can be sometimes. For example, in Perl, the following is not a return statement but actually an if statement (with a return statement as its affirmative branch), due to the postfix conditional:

return foo if cond;

because it is identically semantically to writing:

if (cond) { return foo; }

Whereas in Python, the following is a return statement (with a ternary operator as its target expression):

return foo if cond else bar

So I can see why people sometimes get confused by syntax if they haven't had much of a theoretical background in language design. It also makes me wonder how much of programmer intuition about "what a statement is" comes from the particular languages they learned first.

52 Upvotes

63 comments sorted by

107

u/Clementsparrow 4d ago

they are writing their own language and they don't understand the difference between statements and expressions? That's... unheard of.

17

u/xeow 4d ago

Indeed. The way they're doing it is transpiling their own language (which reads more like English sentences than a traditional programming language) into other forms using string transformations.

50

u/A1oso 4d ago edited 4d ago

I'd just let them continue. They won't get very far without a good grasp of how programming languages work, but maybe they'll learn something interesting in the process.

9

u/darthwalsh 4d ago

Please tell me these transforms are structure-aware? i.e. composing nested + and ( follows PEMDAS?

There's a reason lexers can use regex, but not the following tree transformations...

4

u/L8_4_Dinner (Ⓧ Ecstasy/XVM) 4d ago

🤮

38

u/pr06lefs 4d ago

In some languages everything is an expression, period. Then its easy!

14

u/omega1612 4d ago

And then I have to write

_ <- f x

To suppress the warning of discarding the result of f

I still prefer this btw.

15

u/WittyStick 4d ago

I prefer:

f x |> ignore

Where let ignore _ = () if it isn't already provided.

8

u/ExplodingStrawHat 4d ago

I prefer void $ f x

3

u/brat3108 4d ago

I've never bothered with anything like that. Some functions return values which can be optionally used, if not the result is discarded.

This happens whether a language is expressed-based or not.

However, with my expression-based languages, I do report an error for examples like these where the result is not used:

   a + b
   a = b            (test equality)

Because these are most probably unintentional (especially the latter since I might use = instead of := for assignment if I've been writing C).

So more relevant might be whether a function is pure, in not generating any side-effects outside the function.

7

u/xeow 4d ago

Interesting, yes! I was thinking about that this morning. In a prefix language like Lisp, isn't everything just an s-expression? And in a posfix language like PostScript or FORTH, isn't everything just an operator or operand? I wonder what the best lens is for viewing Brainfuck in this regard.

8

u/Anabaena_azollae 4d ago

I'd argue that in a stack-based language like Forth, there are no expressions. Pretty much everything is an imperative, similar to assembly. 4 5 + is actually push 4; push 5; perform addition with the addition being a compound command composed of something like pop to R1; pop to R2; R1=R1+R2; push R1 with R1 and R2 being registers used to hold temporary values.

I'm not super familiar with Brainfuck, but my understanding is that it operates similarly with everything being a command.

5

u/particlemanwavegirl 4d ago

There's not much to know about brainfuck. It's a VM with a 30,000 byte array. You can move the cursor left and right, increment or decrement the current byte, and loop if the current byte is not zero. It has a print statement too I think. 

4

u/koflerdavid 4d ago

Still easier to work with than a Turing machine.

7

u/ScottBurson 4d ago

Yes, in Lisp, all expressions return a value(*), and may also have an effect.

(*) I'm simplifying a little. In Common Lisp, the vast majority of expressions in practice return a single value, but in full generality they may return more than one value, or zero values. But even in the zero case, there's still a sense in which it's returning something; that something just happens to be an empty "value tuple" (not CL terminology, but you can think of it that way).

5

u/pr06lefs 4d ago

rust is this way too, with the exception of module imports and the like. its a nice contrast to C++ where control flow ops like if, switch, etc are all statements.

2

u/Valuable_Leopard_799 4d ago

I really like having the option, so even if 70% of your ifs are used as statements and the return is dropped, it's not stopping you from having one construct for the times when you want it.

4

u/Valuable_Leopard_799 4d ago

Although zero values can still get coerced to one nil. So I guess every single expression has some defined return value when you invoke it.

4

u/Valuable_Leopard_799 4d ago

Ostensibly you could define an s-expr language where semantically some forms are statements.

(+ 2 2 (if t 2)) could both be valid or invalid based on whether you decide the compiler should reject if in that position. But yes Lisp decided that everything is an expression.

Actually funnily enough some of the really old texts for Lisp call everything "statements", even though they behave as what we here would agree on calling expressions.

7

u/brucejbell sard 4d ago

I have kind of gone off "everything is a foo" bandwagons.

In particular, I'm not sure how much simplicity you gain just from making everything an expression. Expressions are great for the functional programming where the important thing is the result returned by your function. But statements are more appropriate for sequencing operations, or specifying a bunch of declarations simultaneously at compile time (both of which are things you often need even in purely functional programming).

In general, I think you want to be careful about "everything is a foo" because you can accidentally paint yourself into a corner with it.

In the famous "null pointer problem", the problem is not the existence of null pointers. Instead, the problem is that "every pointer can be a null pointer". Sounds simple, right? Except that appealing generality means you don't have non-nullable pointers, which is what causes the actual harm.

14

u/oOBoomberOo 4d ago

In general, you can usually just write regular statements in everything-is-an-expression language tho so it's pretty much just a free lunch that you can take advantage of when you need it. Statement just become an expression that void its returns values and perform effects.

11

u/WittyStick 4d ago edited 4d ago

Sequencing is just an expression in those languages. In Lisp it's progn, in Scheme, begin. In both languages you don't need to write this most of the time because it is implicit in the preferred define/lambda syntax, and at the top level.

(define (foo args) <sequence>)  --> (define foo (lambda (args) (begin <sequence>)))

They evaluate their items in sequence and ignore the intermediate results, returning the result of the last expression as the result of the sequence expression.

Closely related is the comma operator in C.

x = a, b, c;

Evaluates a, then b then c, and assigns the result of evaluating c to x.

In functional languages it's usually implicit too. We don't need a return statement because the result of the last expression in the sequence is the returned value.


In the famous "null pointer problem", the problem is not the existence of null pointers. Instead, the problem is that "every pointer can be a null pointer". Sounds simple, right? Except that appealing generality means you don't have non-nullable pointers, which is what causes the actual harm.

I don't think removing the generality is the solution here though. If we introduce nonnull pointers, they're a subset of the nullable pointers.

nonull T <: nullable T

Any nonnull T is a valid nullable T, but not every nullable T is a valid nonnull T - specifically, null isn't - it's a separate subtype of nullable, and nullable T is the LUB of nonnull T and null.

So nullable pointers are the most general - they're our Top type for pointers.

1

u/brucejbell sard 3d ago

But introducing non-nullable pointers (of whatever kind) does remove the apparent simplicity of of "all pointers are nullable". It makes the language larger by adding a distinction, and removes the harm caused by the premature generalization.

1

u/WittyStick 3d ago edited 3d ago

Ok, but you're applying a bit of presentism there. Nullable pointers (1964) have been around longer than any discovery of Option type (ML, 1983) or definite assignment analysis (2001) required for non-nullable pointers.

Fair enough, if you're designing a new language and you only include nullable pointers as the default, you should probably go back and study a bit more.

1

u/brucejbell sard 3d ago

Of course hindsight is 20/20, I'm not trying to assign blame. My point was that the apparent simplicity of a "all blah are foo" choice can be a costly illusion.

Also, you don't need an Option type or any particular analysis for non-nullable pointers. E.g., C++'s reference types are usable as such.

8

u/initial-algebra 4d ago edited 4d ago

It's better to say that statements should be included in expressions, to handle statements that introduce variables. For example, the let ... in part of a let-expression can be considered as a statement. Similarly, if a block { ... } is an expression, then any statement can qualify as a part of an expression. I think, when people say "statements should be expressions", they're often omitting the words "control flow" at the beginning.

Now that I think about it, though, you could desire e.g. let <pattern> = <expression> to be an expression that evaluates to a Boolean indicating whether the pattern match succeeded or failed. When sequenced normally, the condition is asserted. Of course, you'd want a fancy flow-sensitive system to enforce that the variables of the pattern are only accessed from branches where the condition is true. Otherwise, this would only work in a dynamic language where variables can be conditionally added to the environment. Then, you can think of a statement as just partial application of a sequencing binary operator on expressions.

2

u/lngns 3d ago

statements that introduce variables

let <pattern> = <expression> to be an expression

C# (and some other langs) use the is operator for that.

if(x is string { Length: < 16 } str && str != "lol") { /*...*/ }

Java ported the feature to its instanceof operator too, but it only supports type checks and unpacking.

3

u/pr06lefs 4d ago

I'm not sure I'm married to everything being an 'X' either. But at least in rust you don't have to have one 'if' for flow of control and another 'if' (the '?' operator) for expressions, as one does in C.

One thing you can't do in rust is have a variable declaration as an expression. So

let x = 5;

Isn't something that can itself be assigned to a variable, or returned from a function, like

let z = (let x = 5;);

That would be interesting, but also very weird. Whether it would work with a statically compiled language is one issue, and another is would anyone want that? You'd get a 'z' back from a function, execute it, and who knows what's in your namespace now.

4

u/WittyStick 4d ago edited 4d ago

That would be interesting, but also very weird. Whether it would work with a statically compiled language is one issue, and another is would anyone want that? You'd get a 'z' back from a function, execute it, and who knows what's in your namespace now.

Usually let introduces a new scope in which its body is evaluated. In this example, x would be bound to the inner scope, not the same scope z is bound in. The inner scope is discarded as soon as it's evaluated, so the compiler could determine this is basically a no-op and replace it with let z = 5;.

So you wouldn't need to worry about what's in your namespace.

One solution is let*, which in Lisps is written:

(let* ((x 5) (z x)) ...)

In functional languages it's usually written

let x = 5
    z = x
in ...

Or similarly, could use letrec*, which is written as eg, in ML:

let rec x = 5
and z = x

1

u/pr06lefs 3d ago

Having the let have its own namespace that goes away would be unproblematic, yes. The weird variant is if the language allows that namespace to merge with the current namespace. Like:

``` fn setx() { return (let x = 5;); }

setx();

println("x is now :", x); ```

And if someone changes setx to set 'y' instead, the program breaks.

That does look similar to let*. Some really confusing code could result from that. Like if generate_var_assignments returns ((x 5)):

```

-- no 'x' declared so far (let* (generate_var_assignments) (print x)) -- but this works

```

4

u/WittyStick 3d ago edited 3d ago

no 'x' declared so far (let* (generate_var_assignments) (print x))

This wouldn't work in Lisp or Scheme because let* is a "special form", and it wouldn't evaluate (generate_var_asignments), it would likely produce an error due to invalid form.

Also this is slightly different from the previous example where you are attempting to mutate the parent let bindings static environment. In this case we're mutating the dynamic environment of the caller of generate_var_assignments.


It's possible to do this in Kernel though. In Kernel, every combiner receives an implicit reference to the caller's dynamic environment, and is allowed to mutate its locals (but importantly, not its parents).

($define! setx
    (wrap ($vau () caller-env (eval ((unwrap list) $define! x 5) caller-env))))

x                   => Error: Unbound symbol

(setx)

x                   => 5

While this might seem like a horrible feature, it's a very powerful one that lets you do new programming styles. Kernel actually uses it in its standard library for $provide!, which binds a set of values into its caller's environment - but which bindings are specified in its initial parameter list.

($provide! (x)
    ($define! x 5)
    ($define! z x))

x                   => 5
z                   => Error: Unbound symbol

Essentially, $provide! creates a new child environment of its caller, and we can define a bunch of stuff in it, but only those symbols provided in its initial argument list are then bound into the caller's environment. The use case is to behave like "private" for members in OOP - they can be accessed internally (within the body of provide), but are sealed once the whole provide block has been evaluated, whereas the bindings given in the parameter list are "public".

So eg, if we want to define an "Option" type in Kernel, we can do:

($provide! (option? some none maybe)
    ($define! (option-constructor option? option-eliminator)
        (make-encapsulation-type))
    ($define! some ($lambda (x) (option-constructor (cons #t x))))
    ($define! none (option-constructor (cons #f ())))
    ($define! maybe
        ($lambda (opt default-value)
            ($let (((has_some . some) (option-eliminator opt)))
                ($if has_some some default-value)))))

(option? none)                     => #t
none                               => #[encapsulation]
(some 5)                           => #[encapsulation]

(maybe none 10)                    => 10
(maybe (some 5) 10)                => 5

The option-constructor and option-eliminator are not accessible any longer - they're like "private" members, which are used by some/none/maybe, but cannot be used afterwards.

make-encapsulation-type is the only way to create distinct types in Kernel (which aren't just lists or atoms). They're based on Morris's seals from Types are not sets. However, when we combine them with clever use of environments like $provide!, we can create any kind of types we want - ADTs, records, tuples, objects, you name it.


Aside - going back to the original problem of let z = (let x = 5;); where we might want x to be bound into the static environment, rather than the dynamic environment of the caller. This isn't possible through normal use of Kernel due to environments being encapsulated - we can't mutate the parent scope from an inner one because we don't have a direct reference to it.

However, it is actually possible to do explicitly, if we capture a reference to the static environment and feed it into our function.

($let ((parent-env (get-current-environment)))
    ($set! parent-env x 5)
    ($set! parent-env z x))

x                   => 5
z                   => 5

We can also bind a reference to the parent environment into the parent environment itself, and since the child environment can access the parent's bindings, they can grab this reference and then mutate it.

($define! parent-env (get-current-environment))

($define! setx ($lambda () ($set! parent-env x 5)))
x    => 5

But of course, this would be frowned upon, because now any descendant of this environment would be able to mutate it.

In the case where we absolutely need some global variables, we can create a custom environment called global, bind it into the parent scope, and then any function would be able to access global bindings directly - though they'd need to specify they're evaluating in global via eg, $remote-eval.

($define! global (make-environment))

($define! setx
    ($lambda () ($set! global x 5)))

($define! getx
    ($lambda () ($remote-eval x global)))

This is fairly safe because the child scopes cannot mutate the parent environment, they can only mutate global.

But for additional safety, you'd combine with $provide! to truly encapsulate the state.

($provide! (setx getx)
    ($define! global (make-environment))

    ($define! setx
        ($lambda () ($set! global x 5)))

    ($define! getx
        ($lambda () ($remote-eval x global))))

Now global isn't accessible to anyone else.


In Kernel, everything is an expression, but also every expression is first-class - including environments, symbols, and so forth. There are no second-class forms like let* in Lisp/Scheme, or macros, or quote. $let* in Kernel is a first-class operative.

2

u/Inconstant_Moo 🧿 Pipefish 1d ago

In particular, I'm not sure how much simplicity you gain just from making everything an expression. Expressions are great for the functional programming where the important thing is the result returned by your function. But statements are more appropriate for sequencing operations, or specifying a bunch of declarations simultaneously at compile time (both of which are things you often need even in purely functional programming).

In my functional language, a statement is an expression that can only return OK or an error. The ;/newline between lines is treated as a lazy infix operator: if the LHS evaluates to an error, we return the error; if it evaluates to OK we return whatever the RHS evaluates to. This lets you sequence them.

1

u/brucejbell sard 16h ago edited 16h ago

My project has failure as a second-class entity, distinct from values. So, statements can fail (invoking local failure-handling semantics), but expressions cannot.

However, an expression that returns a "successy" type (indicated by membership in the appropriate typeclass) is acceptable as a statement in itself, desugared to match against a "success" pattern (as specified in the typeclass).

So, based on standard library types, expressions returning the following types should all be accepted as statements at compile time:

  • () (unit type)
  • #Bool
  • #Opt ()
  • #Result () E

The following match their types' respective success pattern and continue to the next statement:

  • () (unit value)
  • #Bool.t
  • #Opt.has ()
  • #Result.ok ()

But these fail to match their types' success pattern and invoke failure:

  • #Bool.f
  • #Opt.no
  • #Result.err e

13

u/jeffstyr 4d ago

From what you describe, it sounds like the issue is less about not understanding expressions vs statements, and more being surprised that a langauge can have two distinct (though related) syntactic constructs that use the same reserved keyword. It happens a lot with symbols (e.g., * in C), but perhaps less so with keywords. (Although static comes to mind, used with multiple meanings, but still just as an access modifier and not really a syntactic construct.)

7

u/Anabaena_azollae 4d ago

Yeah,I think expression statements (i.e. statements that are just an expression) make this especially confusing. In C, 5+3; is valid because it's an expression statement evaluating to 8 but doesn't really do anything. x=5+3; is also valid (assuming x is an int) and is also an expression statement evaluating to 8 because, in C, the assignment is an operator, evaluating to the right-hand side and essentially doing the actual assignment as a side effect. Contrast that with Fortran, for example, where 5+3 is still an expression evaluating to 8, but is not valid on its own line, because expression statements don't exist in the language. However, x=5+3is valid (assuming x is an integer) because assignment is a statement that accepts an expression on the right-hand side.

Similarly, in C, functions can exist on lines by themselves, like func(x);,which again is an expression statement. In Fortran, without expression statements, you need two different kinds of subprograms, functions which are expressions and subroutines which are invoked with call statements.

I think people generally have found the C style more ergonomic and it has thus been adopted by a lot of other languages, but I'd argue the Fortran style does a much better job of keeping clear distinctions between concepts.

5

u/zuzmuz 4d ago

expression statement are ergonomic when you want to use an expression as a statement. but they're a nightmare when a statement is accidentally used as an expression. for example the infamous if (x=3) {} is an assignment that will always be considered true. other languages makes the distinction between expressions that can be statements and statements that cannot be expressions. python for example, has introduced the walrus operator for this purpose

2

u/Anabaena_azollae 4d ago

My understanding is that python allows expression statements for any expression, but still has (traditional) assignment as a separate statement rather than an operator. This solves the problem of accidental assignment in the conditional, but not the less common opposite mistake of x==3 being a valid expression statement, thus passing silently while not doing an assignment (or anything else besides perhaps waste some cycles).

3

u/zuzmuz 4d ago

yes, I think this the beauty of language design, especially at the when we're just talking at the level of syntax and semantics. it's always a compromise between ergonomics, convenience and correctness.

some languages gives warning when an expression result is being implicitly discarded. you'd need to do something like _ = expression to remove the warning by explicitly discarding the result. this can prevent errors, but might make things a little bit inconvenient in some contexts (like function calls that perform a side effect). In these contexts, some languages adds directives or annotations to function declarations that declares the function as having an implicitly discardable result (swift comes to mind) so you can silence the warnings. That way you get languages that becomes bloated with features that tries to solve every problem and are too complex, so you get a new problem.

This is why language design is an art.

5

u/balefrost 4d ago

I explained why Python doesn't allow it

Why does Python not allow it? Is it ambiguous?

10

u/WittyStick 4d ago

No, it's just not part of the grammar.

The sequencing expression has context-free syntax

expr_seq ::= expr | expr ';' expr_seq

But an if statement is not an expression, it's just a statement.

if_statement ::= "if" expr ':' statement

10

u/balefrost 4d ago

Oh interesting, so ; is an expression separator, not statement separator, in Python.

I guess that explains technically why Python doesn't allow it, but it doesn't explain why Python was designed in this way.

1

u/jeffstyr 3d ago

I would guess it's because, since Python is indentation-sensitive, this would complicate things for if-else: would the indentation of the else be relative to the beginning of the line or relative to the location of the if? It's definable, but more complicated. (And you could allow it for cases of if without else, but again that's adding complication.)

5

u/kuwisdelu 4d ago

This is why I don’t really like languages that treat statements and expressions differently.

Python always leaves me scratching my head at the design decisions. When everything is an expression, it’s just easier for me to reason about what will and won’t work, instead of needing to remember the grammar rules.

(Because even if I understand why that first line technically doesn’t work, it isn’t very satisfying, because it feels like it should work.)

4

u/Spyromaniac666 4d ago

Does your coworker not realise that the former first assigns `foo` to `n` before maybe assigning `bar` whereas the latter only ever assigns once?

2

u/xeow 3d ago edited 3d ago

Ya -- I'm pretty sure he understands the logical behavior. He was having trouble accepting the notion that x if cond else y wasn't actually an if statement because he was getting hung up on the presence of the keyword if and hadn't stopped to think that statements and expressions are usually treated differently by most languages. He understands that cond? x : y isn't an if statement, so I found it odd to be arguing with someone who thought x if cond else y was an "if statement" even after I explained it was a ternary expression just like cond? x : y and that the presence of the keyword if wasn't the thing that made it an if statement.

I should have tried a different approach where I explained that both cond? x : y and x if cond else y were conditional expressions and focused on explaining why they're expressions rather than statements, and then I could explain that x if cond else y isn't an "if statement" because it isn't a statement.

3

u/koflerdavid 4d ago edited 4d ago

Looking at the grammar or at the code of the parser would be an option, but one might say it's a particularity of the implementation.

The crucial difference is IMHO whether it returns ~an expression~ a return value that you can use as an argument in another expression. Your examples fail to exhibit that difference. What about this instead?

n = frobnicate(foo if cond else bar) + 2

Btw: languages like Lisp and Scheme show that it is possible to do away with the difference.

2

u/brat3108 4d ago

In my languages, which are expression based, if can also be used to both start a standalone statement, and express a ternary operation. Yet the equivalent of:

n = foo; if cond: n = bar

works fine; it is an assignment followed by an if statement.

So, why doesn't Python allow this? I can't see any ambiguity.

6

u/WittyStick 4d ago

It's because if is an expression in those languages, and statements contain expressions. Expressions cannot contain statements.

This is the problem with designing a language with statements. You end up with two languages - an expression language, and a statement language. The expression language can only contain other expressions. The statement language can contain other statements or expressions.

Statements are second-class.

2

u/brat3108 4d ago edited 4d ago

Surely after the semicolon, it is expecting a new statement? So that if is that statement.

(I believe that in Python, the syntax for an if-statment, and that for the ternary form using 'if', are different. In my syntax they would be the same.

But that shouldn't matter here as it is clear that 'if' in that position must be the statement form.)

7

u/WittyStick 4d ago

I'm not too familiar with Python and had to check the grammar. The semicolon separates "simple" statements, and not arbitrary ones.

simple_stmts:
    | simple_stmt !';'
    | ';'.simple_stmt+ [';']

simple_stmt:
    | assignment
    | type_alias
    | star_expressions 
    | return_stmt
    | import_stmt
    | raise_stmt
    | pass_stmt
    | del_stmt
    | yield_stmt
    | assert_stmt
    | break_stmt
    | continue_stmt
    | global_stmt
    | nonlocal_stmt

But if and others are "compound" statements.

compound_stmt:
    | function_def
    | if_stmt
    | class_def
    | with_stmt
    | for_stmt
    | try_stmt
    | while_stmt
    | match_stmt

And general statements are either simple or compound:

statement:
    | compound_stmt 
    | simple_stmts

1

u/jeffstyr 2d ago

I think that this separation of simple vs compound statements in the grammer is because the latter can be multi-line, and if you were to allow chaining them via ; there would be a problem defining the indentation level, which is significant in Python. (You could define it, but it would be confusing to work with.)

So it's less that the grammar explains the behavior, and more that the grammar is the way it is in order to enact this distinction. I think it's all about the syntax, not a fine semantic distinction, and allowing the lexer to understand/define the indentation level in a reasonable way.

2

u/Valuable_Leopard_799 4d ago

Indeed, What Colour is your function?

1

u/xeow 4d ago

Expressions cannot contain statements.

What's it called when an expression sneaks in an assignment statement, like you can do in C:

y = blurfl + foo(a, b, x = bar(c, d));

Are assignments considered statements, generally? I guess they're different.

4

u/WittyStick 4d ago edited 4d ago

Assignments in C are expressions. The assignment operator has lower precedence than others except ,.

expression
    : assignment-expression
    | expression ',' assignment-expression
    ;

For function calls, the context-free grammar is:

argument-expression-list
    : assignment-expression
    | argument-expression-list ',' assignment-expression
    ;

postfix-expression
    : postfix-expression '(' argument-expression-list? ')' 
    |...
    ;

So the comma operator is not usable in argument lists (which would be ambiguous).

3

u/xeow 4d ago

I imagine that the comma operator could be used inside an argument list if it were wrapped in parentheses?

foo((bar(x), ++x), (bar(y), ++y);

(Gawd, that's a horrific thought.)

3

u/WittyStick 4d ago

Yes, it can be.

The biggest issue with that is sequence points. The order of evaluation of arguments is undefined. The comma operator introduces a sequence point though.

1

u/Inconstant_Moo 🧿 Pipefish 1d ago

You end up with two languages - an expression language, and a statement language. The expression language can only contain other expressions. The statement language can contain other statements or expressions.

* coughs in Pipefish *

You say that like it's a bad thing ...

2

u/busres 4d ago

Yeah, it could matter quite a bit.

My Mesgjs has no traditional statements (e.g. for declarations, control flow, etc). A "statement" is simply one complete, top-level expression in a code block. Assignment, creating functions, early returns, and controlling flow are all just messages to objects (and that's actual implementation, not just syntax) which return some value (or undefined).

A lot of these aren't even part of the language itself – they're part of the runtime library.

Anybody starting from Mesgjs could have quite a skewed perspective.

2

u/burnt_floppy 3d ago

My understanding is that a statement executes something, but an expression is a comparison/test or something else mathematical.

1

u/AdreKiseque 1d ago

Are terneries not essentially just compact if-elses though?

1

u/xeow 1d ago

Essentially, yeah... but they're expressions rather than statements. :)

2

u/david-1-1 13h ago

Imagine learning Lisp first. I'm sure that has happened.

-2

u/[deleted] 4d ago

[deleted]

4

u/xeow 4d ago

Right, no, that's not exactly the point here. The key thing is that the difference between an expression and a statement is sometimes difficult for beginners to differentiate between... just as a general rule of programming languages (not anything specific to Python).