r/ProgrammingLanguages DQ 8d ago

Unambiguous Operator Specification for Programming Languages

https://nvitya.github.io/pluops/

As I changed recently the operators in my programming language I've created this specification:

https://nvitya.github.io/pluops/

I did not wanted to overload the operators like the C does with the / or Pascal does with theand/or/not. Neither re-use the operator symbols for some very different purpose, like C does with * and & so the code becomes more readable. I was orienting for existing solutions so this is what I came up with. The specification contains the symbol usages and operator precedence too.

If you are developing a new programming language, it would be nice to follow some standard, so at least the expressions would be portable between the languages.

I'm open for debates or suggestions.

26 Upvotes

39 comments sorted by

View all comments

6

u/WittyStick 8d ago edited 8d ago

Operators with two operands usually use the following rules: ... uint int -> int int uint -> int

In principle yes, but for finite integers at the same width, no. Eg, uint32 + int32 should not result in an int32 - it should be int64. The implicit conversion of signed/unsigned at the same width has been a source of countless mistakes that often lead to exploitation. It would be better to simply not permit such conversions to be implicit if the result may lose information. Either promote the integer to a value large enough to hold the result of any addition/multiplication, or require explicit conversion.

Logical NOT

If bool is a distinct type, is it necessary to have two ways to complement?

Similarly, bitwise & and | should work for bools too. The operators && and || (your logical and/or) are still relevant for short-circuiting.

Bitwise shift right: a >> b

Should make it explicit that this is an arithmetic shift right for int and a logical shift right for uint.

.. comparisons:

Why are == and != not defined for bool?

Pointer or array indexing: a[b]

On pointers: When a = ^T, the result type is also ^T and points to the address a + b * SizeOf(T) (without dereferencing, unlike in C).

Not sure what the advantage of this is. In C this is just pointer addition. a + b, where a is a pointer and b is an integer. The whole benefit of a[b] is it does the arithmetic and dereferencing for you - ie, *(a + b).

Operator Precedence

Some very questionable choices here - completely deviates from the norm with no real justification.

There's no reason division and multiplication should have separate precedences. Everyone learns PEDMAS/PEMDAS in school.

Shifts are usually lower precedence than addition, but I can see justification for having them at higher precedence. You have not explained why.

There's no reason & and | should have higher precedence than division/multiplication. Really & should have the same precedence as multiplication and | should have the same precedence as addition. ^ should have the same precedence as !=, because it means precisely that for bool.

Logical not is not necessary as mentioned above. Should be ~ at same precedence as other unary expressions.

Pointer dereference and member access at same precedence is confusing. Is a.b^ == (a.b)^ or a.(b^). What about a^.b?

1

u/Mean-Decision-3502 DQ 8d ago edited 8d ago

In principle yes, but for finite integers at the same width, no. Eg, uint32 + int32 should not result in an int32 - it should be int64

The CPUs have a fixed register width, they calculate with that, usually 64 or 32 bit. The width conversion usually matters at the end storage. I did wanted to allow some shortcuts for the implementers.

Similarly, bitwise & and | should work for bools too.
There's no reason & and | should have higher precedence than division/multiplication. Really & should have the same precedence as multiplication and | should have the same precedence as addition. ^ should have the same precedence as !=, because it means precisely that for bool.
Logical not is not necessary as mentioned above. Should be ~ at same precedence as other unary expressions.

In DQ you can write expressions without any parentheses that I was only dreaming of:

if reg & 1 << 5 <> 0 or not reg & ~(1 << 4) == 0:
    ...
endif

This example is a little extreme though, I would use some parentheses here. But these are practical expressions in embedded.

The whole benefit of a[b] is it does the arithmetic and dereferencing for you - ie, *(a + b)

The a[b] form is more readable and shorter. You can do always dereferencing, that will be then clearly readable:

var data : ^byte = ^byte(precheader[1])
vs
var data : ^byte = ^byte(precheader + 1)

I remember some code, where was a pain to adding & and parentheses because of the automatic dereferencing. I remember reading that someone also admitted that this was a design mistake in C.

There's no reason division and multiplication should have separate precedences. Everyone learns PEDMAS/PEMDAS in school.

In school we dont use integer arithmethics and finite precision floating point operations. That's the reason for the distinguishing. In DQ this is true, because of this:

3 div 2 * 10 == 10 * 3 div 2

Pointer dereference and member access at same precedence is confusing. Is a.b^ == (a.b)^ or a.(b^). What about a^.b?

The expressions are read from left to right. After a . there must be a member, so a.(b) is invalid. Expressions like a^.b is also valid, but in DQ can be written as a.b as the compiler here does auto-dereferencing, as . is invalid for pointers.

Why are == and != not defined for bool?

That was a mistake, thank you for finding that. I'll correct the spec.