💾 Archived View for dioskouroi.xyz › thread › 29351989 captured on 2021-11-30 at 20:18:30. Gemini links have been rewritten to link to archived content

View Raw

More Information

-=-=-=-=-=-=-

Features of PL/I not realized in a modern language

Author: vitplister

Score: 96

Comments: 100

Date: 2021-11-26 17:10:38

Web Link

________________________________________________________________________________

adrian_b wrote at 2021-11-26 20:33:49:

There are many other features of PL/I besides those mentioned there, which are not encountered in modern languages.

Because PL/I was designed by IBM for their computers and operating systems, there were no concerns for portability, so the language definition included a large amount of features that are left unspecified in standards for languages like C/C++, because they are available only for certain operating systems or computers, so the standard must be restricted to specify only the minimum features that are available everywhere.

For example the C++ threads are limited only to the features provided by the POSIX pthreads, even if most operating systems have additional features for multi-threading. PL/I on the other hand, already in 1965 had more powerful features than the C++ threads.

One feature of PL/I that is completely absent in all modern languages is what I consider to be the right way for error handling.

Instead of being forced to test the return value of a function to determine what error might have happened, which is a redundant operation, as this was already tested once in the invoked function, and which clutters the code, obscuring the processing of the normal path, the PL/I method was to group the error handlers at the end and pass the labels of the error handlers to the invoked function as alternative return labels.

While this looks visually very similar to a _try_ block with exception handlers, the hardware implementation in PL/I is far more efficient than in modern languages, because there are no conditional branches and no extra code to determine where is the appropriate error handler, just the return, which is done anyway when the invoked function finishes, jumps to different addresses in case of errors, instead of returning to the point of invocation.

PL/I also had exceptions, but those were used for really exceptional conditions, like numeric overflow, illegal memory accesses etc., not for correctable error conditions, like a mistyped user input or a file that cannot be found, which are best handled at the point where a function has been invoked, because only there you have complete information about the intention of the failed operation.

mdmi wrote at 2021-11-30 19:02:09:

The "error handling" feature is a particular use of nonlocal GOTO plus a LABEL data type. It may be more efficient than an exception, because a label value includes its stack frame, while an exception must check whether the exception is handled in each frame as the stack is unwound.

On the downside, label variables entail the danger of jumping to an uninitialized or dead label value. This is a counterexample to another comment's claim that PL/I is a "safe" language.

p_l wrote at 2021-11-26 21:59:37:

PL/I model was inspiration for Common Lisp condition system, which handles both recoverable (continuable) and non-recoverable conditions (akin to exceptions). Continuable exceptions are also a thing in Windows NT, though rarely seen in practice.

All of those hark back to PL/I error handling, possibly through Project MAC (which was probably bigger PL/I user than anything from IBM for a time)

froh wrote at 2021-11-27 09:58:54:

Somehow I find it fitting to quote the Britannica on Project MAC:

https://www.britannica.com/topic/Project-Mac

"Project MAC, in full Project on Mathematics and Computation, a collaborative computer endeavour in the 1960s that sought to to create a functional time-sharing system. Project MAC, founded in 1963 at the Massachusetts Institute of Technology (MIT), was funded by the U.S. Department of Defense’s Advanced Research Projects Agency (ARPA) and the National Science Foundation. The goal for the project was to allow many users access to the programs of a single computer from various locations. Project MAC’s pioneering exploration of the working methods of multiple-user access became a foundation for modern computer networking and online collaboration."

josephg wrote at 2021-11-26 23:44:11:

> the PL/I method was to group the error handlers at the end and pass the labels of the error handlers to the invoked function as alternative return labels.

Huh. I wonder if it would be possible to modify LLVM to support this for rust. You'd be inlining the match / try / unwrap() calls that usually happen after returning from a method which returns an enum variant. And you'd be inlining it across the function call boundary.

This would change the calling convention for methods that return enum variants, but rust doesn't publish a calling convention yet anyway. And for internal only methods, this might be a significant performance win in IO-heavy workloads.

You might need some complex heuristics to figure out when this is worth it though. Not all enums are Option / Result. I bet plenty functions are faster without this trick.

And if you jump based on the value in a register, is this fast in modern CPUs? How would that compare to making a high prediction accuracy branch?

volta83 wrote at 2021-11-27 07:59:40:

You mean "multi-return" functions?

https://github.com/bytecodealliance/wasmtime/issues/1057

We have looked at these before. Modern CPUs have a RAS, and you dont want to mess with that. But there are ways to implement this, and some papers that cover the performance impact.

We'd need a very motivated individual to add support for this to LLVM and Cranelift.

tux3 wrote at 2021-11-27 08:32:58:

RAS: Return Address Stack, a simple predictor that tracks return addresses.

adrian_b wrote at 2021-11-27 11:25:21:

The fact that the indirect jump that returns when an error is encountered is not predicted, is never worse than when testing an error code returned by the function, because that conditional jump is also always mispredicted.

The method used by modern languages requires both an indirect jump for return, hopefully predicted, _and_ a mispredicted conditional jump. Moreover, if the invoking function is executed once more, but the next time there is no error, the conditional jump that tests the error code may be mispredicted once more.

Using multiple return labels requires only a single mispredicted unconditional jump and it also omits a bunch of redundant instructions for generating an error code and testing it.

For functions that may encounter multiple types of errors, the economy is much larger because testing the returned error code would have needed either multiple conditional branches or an indirect indexed jump for a switch/case structure.

volta83 wrote at 2021-11-27 13:58:05:

I think that right now, there is sufficient motivation to implement enough of this to be able to try it out, and then see how it actually behaves in practice.

But Rust works on a really wide range of kinds of processors - CPUs, GPUs, FPGAs - and hardware architectures (dozen CPU architectures, dozen GPU architectures, etc.), and nobody has shown that this is a net win for all of them.

Figuring out for which, if any, this is a net win, is something that will have to be demonstrated.

monocasa wrote at 2021-11-27 19:50:07:

When you say FPGA, do you mean running on soft cores or do you mean high level synthesis from Rust directly?

volta83 wrote at 2021-11-29 11:02:06:

Running on soft cores.

ratboy666 wrote at 2021-11-26 23:03:18:

In FORTRAN IV, multiple return labels could be passed to a subroutine. This

was carried into PL/I. In FORTRAN 66, this was not supported. But, FORTRAN IV and 66 both support assigned go to, which can be used to replace this -- WATFIV supported this (local routines that use ASSIGN, with a bit of syntax sugar).

Note that this is a LONG time ago (mid sixties)...

ratboy666 wrote at 2021-11-27 18:54:06:

And... for reference. In my Microsoft F80 github string repository:

https://github.com/ratboy666/string

(Microsoft F80 was a FORTRAN 66, slightly subset compiler -- no COMPLEX

that Microsoft sold from the late 70s to early 80s).

In there find pop.mac which is a short 4 byte assembly routine to discard

a stack level. That can be used as follows:

    ASSIGN 1 TO I        -- STORE ADDRESS OF LABEL 1 TO I
   CALL F(I)            -- PASS I TO SUBROUTINE F
   ...                  -- WE WILL NEVER GET HERE
 1 ... 
 
   SUBROUTINE F(I)
   EXTERNAL $POP 
   INTEGER $POP  
   JUNK = $POP(0)       -- REMOVE RETURN ADDRESS
   GO TO I              -- GO TO ASSIGNED LABEL I 
   END

The ASSIGN command puts the address of label 1 into INTEGER I

GO TO I then transfers to that location. Note that we can pass I into the

SUBROUTINE, use $POP to remove a stack level (the return address), then

jump to the variable. Scheme is nicer, which continuations, of course, but

that hadn't been considered yet...

The ASSIGN was deleted as a feature in FORTRAN 95 -- obsolescent by FORTRAN 90.

I guess "very very old-school".

Now, the FORTRAN 66 standard doesn't mention if something like this is allowed... but, since no stack was involved in early implementations, and

recursion was not allowed, I imagine that it would work more widely than

might otherwise be expected.

PaulDavisThe1st wrote at 2021-11-26 20:43:10:

> For example the C++ threads are limited only to the features provided by the POSIX pthreads, even if most operating systems have additional features for multi-threading.

What facilities are you thinking of?

> PL/I on the other hand, already in 1965 had more powerful features than the C++ threads.

Given that the term "threads" originates in the 65/66/67 era, this is an interesting claim.

adrian_b wrote at 2021-11-26 21:05:25:

The term "thread" has become fashionable only after 1990.

Nobody used the terms "thread" or "multi-threading" when PL/I was designed. Everybody used the terms "task" and "multi-tasking", with the same meaning.

So in PL/I you have "tasks" for what are now called "threads".

The most annoying feature of the C++ thread support library, which is inherited from the POSIX pthreads specification, is the pathetic operation _join_ which waits for the termination of a designated thread.

This is almost never what you want. The most frequent case is to wait for the termination of anyone of a group of threads, which cannot be done with _join_.

Already in 1965, PL/I had a _wait_ that could do anything that can be done with _WaitForMultipleEvents/WaitForSingleEvent_, i.e. it could wait for any or all of any combinations of a set of events, e.g. thread terminations.

This is such a basic feature that I cannot understand how it could have been omitted from the POSIX pthreads and in consequence also from the C++ thread support library. The other features of pthreads/C++ that can be used to implement such a functionality, e.g. condition variables, also suck badly in comparison with a good _wait_ for events.

spacechild1 wrote at 2021-11-26 22:28:12:

> This is almost never what you want. The most frequent case is to wait for the termination of anyone of a group of threads, which cannot be done with join.

I also have to say that this doesn't match my experience at all. Typically, my threads run for the whole duration of the program/object and I only need to join them when the program/object is done. In fact, this pattern is so common that C++20 added std::jthread which automatically joins the thread in the destructor.

I literally never had to wait for "the termination of anyone of a group of threads". For you it's apparently the most frequent case. This only teaches us not to make too broad assumptions. The world of programming is large.

adrian_b wrote at 2021-11-27 11:50:32:

Your pattern is normal when you do not need to create a large number of threads.

When you have enough work for a very large number of threads, there are 2 main styles to do it.

Both styles avoid creating an excessive number of active threads in comparison with the number of available cores, because that can reduce the performance a lot.

One style is to have a permanent pool of working threads and to use some means of communication between threads to know when any thread has finished its current job, to give a new piece of work to that thread. This is also done in the simplest way with some kind of waiting for any of multiple events in the dispatching thread, where the event is that some thread has signaled the end of its job.

This style is appropriate for environments where thread creation and destruction is expensive, like Windows.

When the thread creation and destruction is cheap and also the indvidual pieces of work are relatively large, requiring an execution time much greater than the thread creation/deletion time, a second style of programming is much simpler.

You just create one thread for each piece of work, until you reach the limit for the maximum number of active threads. Then you wait until any thread terminates and then you create a new thread for the next piece of work. When there is no more work, you wait until all threads terminate.

In conclusion, any kind of work where you could launch an arbitrarily high number of parallel threads needs to wait for any of a group of events.

For all problems with limited parallelism, where you can create only few threads, you do not need this feature, but my work was always about problems where I could launch much more threads than the available hardware cores, where some kind of wait for multiple events is mandatory.

PaulDavisThe1st wrote at 2021-11-26 21:19:36:

It's clear that you know a lot about this stuff, but I think maybe not quite enough.

The Linux kernel retains the term "task" for "execution context", and the term "thread" is used exclusively in user space purely because of programmer conventions (pthreads, specifically). This makes a limited amount of of sense because "thread" has also come to mean things that are very definitely not full execution tasks (user-space and/or so-called "green" threads). This overloading of the terminology in user-space is not reflected in kernels, but allows for developers to play with alternate implementations without changing their fundamental terminology. After all, there some things where user-space threads are precisely what you want (and these absolutely do not correspond to kernel tasks).

> This is almost never what you want.

I would beg to differ. I've been writing multithreaded code in C++ for nearly 30 years, and I don't remember any time that I wanted to join on a group of threads. My current project (20 years old) is heavily multithreaded (typically 20-60 threads) and there is nowhere that a thread primitive to join on a group of threads would make our lives easier. The only context where I could even think of using it might be to wait on a thread pool, but in general this tends to be unnecessary and if it is, can be trivially handled without a multi-join.

> Already in 1965, PL/I had a wait that could do anything that can be done with WaitForMultipleEvents/WaitForSingleEvent,

WaitForMultipleEvents/WaitForSingleEvent are not part of the C or C++ language, but Windows system calls. While their semantics are very powerful and useful (which obviously would thus apply to PL/1's _wait_), their absence cannot be laid at the feet of C or C++: no Unix-like OS has had this operation, ever (it is almost possible with contemporary Linux).

You could make the argument that it's not part of pthreads because this operation was never a part of posix, and so pthreads could not build on it (you can use those calls on Windows even in a single-threaded task, and that would be true in a POSIX system if the call existed). So it really has almost nothing to do with pthreads at all, other than that one could imagine a pthread wanting to block on "wait-for-something-anything".

adrian_b wrote at 2021-11-26 21:45:48:

> The Linux kernel retains the term "task" for "execution context"

This has historical roots from the first UNIX versions written for DEC computers. Even if the UNIX authors usually preferred the term "process", in the DEC documentation for their computers and operating systems the term "task" was always used for "process". So the term was also used in UNIX in various places.

> I don't remember any time that I wanted to join on a group of threads

This is not surprising, because there are many kinds of multi-threaded applications and many styles of programming them.

I do not doubt that what you say is correct for you applications, but my experience happened to be opposite. I have never encountered a case when I wanted to join a single thread, but I have encountered a lot of cases when I wanted to join any of a group of threads (e.g. for keeping a number of active threads matching the number of available cores) and also a less number of cases when I wanted to join all of a group of threads, typically at the end of an operation. The latter case is less important, because it can be done by repeating a _join_ with a single thread, even if that is much less efficient than a _wait_ that waits for all.

> are not part of the C or C++ language, but Windows system calls

This is precisely what I have already said in my first post, i.e. that standardized languages like C/C++ are forced to specify only the minimal features that are available on all operating systems, so they cannot include _WaitForMultipleEvents_, while PL/I was free of such portability concerns, so it could specify more powerful features.

PaulDavisThe1st wrote at 2021-11-26 22:09:43:

> This is precisely what I have already said in my first post, i.e. that standardized languages like C/C++ are forced to specify only the minimal features that are available on all operating systems, so they cannot include WaitForMultipleEvents, while PL/I was free of such portability concerns,

I think you missed my point. WaitForMultipleEvents is not part of a thread API on any platform. It's a part of the platform API, and is used by single-threaded and multi-threaded code. There's no reason for pthreads (or any other thread API) to represent this system call, because the system call either exists, and can be used directly, or does not exist, and cannot be used.

In essence, you're really just noting that POSIX (not pthreads) never had a wait-for-just-about-anything API. That's a legitimate complaint, just not very relevant for multithreaded programming.

> This is not surprising

Well, given that you said _"This is almost never what you want._", I'd count it as least a little surprising. My point was that multi-join is not "almost never what you want", but has always been "useful in certain contexts". I have never come across a multi-join API that blocks until all threads have completed (they typically return when any of the specified threads completes), and so the difference in efficiency for this version of multi-join is essentially identical to a loop+single-join.

>This has historical roots from the first UNIX versions written for DEC computers.

I don't see much evidence for this claim. task_t exists in early versions of AIX and Mach, and the terminology was already common in Multics (as you know). I don't think that Linux' use of task_t has any relationship to the Ultrix use, but maybe you have some specific insight here?

denton-scratch wrote at 2021-11-27 11:19:14:

> Even if the UNIX authors usually preferred the term "process"

The OS I learned on was called CTOS, which used the term "process" to refer to "the basic unit of code that competes in the scheduler for access to the CPU". A "task" was essentially what we'd now call a program, complete with libraries and sub-processes. We didn't use the term "thread". I think CTOS dates to about 1981.

otabdeveloper4 wrote at 2021-11-27 20:28:15:

> is heavily multithreaded (typically 20-60 threads)

Hm. For me "heavily multithreaded" is 1000 threads and more. That said, I just detach them, there's no real reason to wait for a thread.

PaulDavisThe1st wrote at 2021-11-28 06:28:22:

unless you've got 1000 cores OR are severely i/o bound, 1000 threads seems mostly useless. OTOH, "severely i/o bound" tends to describe client/server computing quite well, so maybe that's just what you need.

my stuff isn't client/server computing, my threads are generally compute bound, and having (lots) more than there are cores would be counter-productive.

otabdeveloper4 wrote at 2021-11-28 13:43:46:

1000 threads blocking on I/O operations is the same as an epoll() over 1000 file descriptors under the hood, except you also get pre-emption guarantees from the kernel.

Yeah, so basically this. Except that "I/O bound" usually implies critical moments of computation too, and pre-emption is really nice here.

monocasa wrote at 2021-11-26 21:35:49:

Linux exports the concept of threads in it's user facing ABI. You can see this in "Thread Group ID" and "Thread ID" values.

ngcc_hk wrote at 2021-11-27 11:03:21:

> Because PL/I was designed by IBM for their computers and operating systems, there were no concerns for portability, so the language definition included a large amount of features that are left unspecified in standards for languages like C/C++, because they are available only for certain operating systems or computers, so the standard must be restricted to specify only the minimum features that are available everywhere.

Reading wiki I am not sure the assertion is right. In fact I am not sure even the first set of statements is right.

It was involved a lot of others from beginning (from users in share/guide) to other universities and European standard committees.

adrian_b wrote at 2021-11-27 15:46:11:

Almost all PL/I features have already been present in an internal technical report about NPL (New Programming Language) from December 1964, which was done by an IBM team, concurrently with the software design for the new IBM System/360 series of computers, for which PL/I was intended to be the main programming language.

The design of the new language has taken into account the feedback from the users of FORTRAN IV and COBOL on the previous 7xxx series of IBM computers and it was also inspired by features from ALGOL 60, but otherwise there was no direct influence of outsiders on the language design.

Around 1966, a few extra features were added to PL/I, the most important being pointers and unions, which were taken from the Hoare and McCarthy proposals for the successor of ALGOL 60.

At that time IBM did not care yet if any of their programming languages will also be used on other computers. They were a hardware vendor, which offered as a bonus operating systems and programming languages with their computers, to convince the customers to buy the hardware. They were not selling software yet.

Compatibility with other hardware was not at all desirable for IBM. Only the other smaller companies were interested in porting IBM languages like FORTRAN or PL/I to their computers, to make them an acceptable alternative to IBM hardware.

So the standardization efforts were done in other places, but based on the de facto standards created by the IBM products.

pjc50 wrote at 2021-11-26 20:47:58:

> the PL/I method was to group the error handlers at the end and pass the labels of the error handlers to the invoked function as alternative return labels.

The idea of an alternative return address - that is, allow the calling convention to pass more than on return instruction pointer - seems both simple to implement and really useful. I guess in the modern gotoless world the trouble is structuring the syntax around the call.

adrian_b wrote at 2021-11-26 21:16:03:

The alternative return addresses were declared in the function prototype as parameters having the type _label_.

Then you just passed them during function call, e.g. "handle = open_file(input_file_name, open_flags, FailedToOpenInputFile);"

The error handlers at the end of the body of the invoking function were just labelled appropriately.

Writing a label like _FailedToOpenInputFile:_ before an error handler is certainly much simpler and more obvious than strange workarounds for exception identification, like inventing a dedicated data type for each possible error, as in C++.

catern wrote at 2021-11-27 12:13:10:

It would be nice if those labeled blocks could be written inline and anonymously - just pass a block as an argument rather than having to actually name the error handling block. Is that supported?

adrian_b wrote at 2021-11-27 12:21:44:

Not in PL/I or in the other languages with this feature (ALGOL 60, FORTRAN IV).

This could be an easy syntax extension, but then the only advantage remains the more efficient hardware implementation of the error handling.

In that case you no longer have the separation in program text between the normal execution path and the error handlers, which removes visual clutter.

gnufx wrote at 2021-11-27 21:05:23:

You can pass a fatal error-handling routine to a Fortran subprogram, to be called with the error code.

drran wrote at 2021-11-26 21:37:15:

It depends on which part of code will clear stack after call. If caller is responsible to clear stack, like in C, to support variable arguments, then called function must return to the original point of call.

If called function is responsible to clear stack, like in Pascal or Forth, then yep, multiple return points are easy to implement and useful to implement something like switch operator. I used it in Pascal, when I was in school. Just pop old return address from stack and push new one.

adrian_b wrote at 2021-11-26 22:00:32:

You are right, but the ancient _cdecl_ function call convention used by C for variable argument lists is really obsolete and it should never be used in any new programming language implementation, even if it has survived until today in various ABI specifications.

Like for the C null-terminated strings, there are many alternative ways to implement variable argument lists and all of them have already been used many years before the first C compilers, e.g. passing a hidden parameter count or implementing transparently for the programmer any _printf_-like functions as _vprintf_-like functions.

With the alternative implementations, the stack should always be freed in the invoked function, which also enables various other important features, e.g. tail-call optimization.

gnufx wrote at 2021-11-26 22:07:55:

Is the PL/I alternate return fundamentally different from Fortran's (or FORTRAN's in those days)?

adrian_b wrote at 2021-11-27 11:58:46:

It was the same as in many FORTRAN IV dialects and also in ALGOL and in many other early languages, but in FORTRAN that was not a standard language feature, even if many compilers implemented it as a language extension.

gnufx wrote at 2021-11-27 14:49:39:

I was responding to "completely absent in all modern languages". Alternate return has been in the standard since '77, and Fortran 2018 seems modern enough.

adrian_b wrote at 2021-11-27 15:27:16:

You are right that alternate return labels have been included in more recent FORTRAN standards.

Nevertheless, this feature is much less useful in FORTRAN than in laguages like PL/I, because the FORTRAN labels are restricted to numbers.

Unless you use a preprocessor, you cannot give descriptive names to the error handlers, so the equivalent FORTRAN code looks much uglier than in PL/I.

gnufx wrote at 2021-11-27 21:06:15:

In which FORTRAN IV dialects was it the same as in PL/I?

I should have said that alternate return is obsolescent in recent Fortran, and I don't remember ever feeling the need to use it, at least for error-handling.

npsimons wrote at 2021-11-26 20:44:19:

Upvoted simply out of controversy and curiosity.

It's been a long time, and I've not delved deeply in to a wide swath of languages (recently), but surely the features he's citing are in other languages? Hell, even the list of "features only Lisp has" keeps shrinking by the decade. Some of these PL/I "features" feel like things you could trivially implement in a library in other languages (ie, not have to implement half of PL/I to get those features). Some of it sounds simply like syntactic sugar, and not actual features.

What am I missing here?

mgraczyk wrote at 2021-11-27 15:43:35:

Generally in modern languages you use libraries rather than language features, which IMO is better and shows that the language is generic enough to allow programmers to build abstractions as needed.

However, some of these (#4 in particular) are exactly supported in other "modern" languages.

1. Assignment by name. 
    python: A = {**A, **B}
    javascript: A = {...A, ...B};

    2. Both binary and decimal data
    For these you use libraries like Decimal in python, boost::multiprecision in C++

    3. Bit strings of arbitrary length
    vector<bool>, boost::dynamic_bitset, or write your own
    in python you would use numpy

    4. A named array is normally passed by reference, as in F(A)
    In C++ you can do this for any type with a copy constructor.

    void f(A_Type& A) { ...
    ... = f(A);   // by ref
    ... = f({A}); // by value

    5. IO by name.
    Not generally done as far as I'm aware, but you could easily build this in C++. Something like
    class StdOut { void operator=(string s) { cout << s; } };
    StdOut{} = "hello\n";

    6. SORT statement
    Why not sort function? c++ std::sort, python sorted() builtin

    7. Complete implicit conversion
    Javascript is the KING here, and we all know how helpful that is....
    Of course C++ let's you define this yourself, but there isn't directly language support.

npsimons wrote at 2021-11-28 19:07:30:

> Generally in modern languages you use libraries rather than language features

This was exactly the feeling I got as I read his list: "these aren't features, they are, at best, nice to have library functions and syntactic sugar."

HelloNurse wrote at 2021-11-28 11:25:18:

I don't think the C++ aggressive implicit conversions are particularly well liked. Saving a little verbosity by omitting an explicit conversion is rarely worth the risk of silently derailing overload resolution; it was probably already a bad trade off in PL/I.

jdougan wrote at 2021-11-27 00:01:04:

My personal favorite feature in PL/1 was the zero overhead matrix transpose. You achieved this by mapping two matrix declarations onto the same storage, one specifying column-major and the other row-major. We used to joke the PL/1 declaration system was turing-complete.

moonchild wrote at 2021-11-27 09:50:06:

The interesting use-case for transposition is not just switching around indices, but changing layout so a given access pattern exhibits greater locality. No getting around the overhead there :/

bigbillheck wrote at 2021-11-26 20:08:49:

7. Astonishingly complete set of implicit data conversions. E.g. if X

is floating-point and S is a string, the assignment X = S works when S

= "2" and raises an exception (not PL/I terminology) when S = "A".

This seems like an extraordinarily bad idea.

jrd259 wrote at 2021-11-26 20:32:14:

Perhaps a bad idea, but at least it was rigorously specified. I note that that Javascript and perl have same ability. See also

https://www.destroyallsoftware.com/talks/wat

masklinn wrote at 2021-11-27 07:26:00:

> I note that that Javascript and perl have same ability.

And it’s widely considered horrible.

layer8 wrote at 2021-11-26 21:00:44:

Javascript and Perl are different in that they allow a variable to change it’s type by reassignment (i.e. they would allow the X = S assignment for S == "A"), which PL/1 apparently doesn’t. Weak/strong typing and dynamic/static typing are different axes.

masklinn wrote at 2021-11-27 07:28:05:

> Javascript and Perl are different in that they allow a variable to change it’s type by reassignment

That is orthogonal to the issue at hand.

> Weak/strong typing and dynamic/static typing are different axes.

And perl and javascript have both, and the weak typing is what gets criticised the most, and what causes the most issues.

layer8 wrote at 2021-11-27 10:58:38:

Yes, the weak typing is what (rightfully) gets criticized, but the PL/1 design here is IMO closer to C with its implicit conversions but typed variables than to Perl and Javascript.

masklinn wrote at 2021-11-27 11:18:11:

> the PL/1 design here is IMO closer to C with its implicit conversions but typed variables than to Perl and Javascript.

So in your opinion, the PL/1 design here is closer to a trainwreck than an other trainwreck?

Furthermore C's implicit conversions come nowhere near parsing a string to get a float, that's squarely on the Perl/JS side of the board.

layer8 wrote at 2021-11-29 02:28:00:

Look, I wasn’t expressing any value judgment, and I’m not advocating for implicit conversions.

My point was that the implicit conversions in PL/1 have a different context in PL/1 than they have in Perl and Javascript, more akin to the one in C. One effect of that is that the type of a variable remains fixed in PL/1 where it doesn’t in Perl and Javascript. This places it at a different point in design space and changes the dynamics to some extent, which is just a nuance that I find interesting. It wasn’t expressed in the original comment I was replying to, so I thought it would be worth pointing it out so that he distinction isn’t being missed.

ptx wrote at 2021-11-26 20:48:05:

It does. It sounds like the Unicode handling in Python 2, where your program would still appear to work if it confused bytes with codepoints (through implicit decoding/encoding), until you get a non-ASCII input somewhere and everything blows up with a UnicodeDecodeError somewhere far from where the problem is.

drfuchs wrote at 2021-11-26 20:40:12:

And all the implicit conversion rules between pairs of types couldn’t fit on a single-page chart; the official IBM manual included an honest-to-goodness fold-out bound right in. Fun times.

layer8 wrote at 2021-11-26 20:55:53:

It’s like implicit numeric conversions in C, just extended to string types.

zokier wrote at 2021-11-26 21:45:54:

Its not like the implicit numeric conversions in C were particularly good idea either

emmelaich wrote at 2021-11-26 22:57:33:

It was a mistake in C++ too, fixed with the _explicit_ keyword.

masklinn wrote at 2021-11-27 07:29:46:

It’s not fixed since it still occurs by default. That you can work around it if you remember to is not a fix.

layer8 wrote at 2021-11-27 00:05:02:

Indeed. My point was that it’s something also found in other widely used languages, and not a PL/1 pecularity.

masklinn wrote at 2021-11-27 11:32:35:

> My point was that it’s something also found in other widely used language

where it is also generally considered

> an extraordinarily bad idea.

so it seems like a rather useless point?

BXLE_1-1-BitIs1 wrote at 2021-11-27 02:36:05:

PL/I was an order of magnitude step from Fortran and Cobol. The earlier languages made direct calls to the operating system while PL/I used subroutine librairies to do the operating system heavy lifting. The library routines in many cases would produce messages of various utility (usually with the number of the failing statement) instead of causing the application to terminate aka ABEND.

Yes, you can use bit strings in PL/I, but the subroutine overhead was ginormous.

This is simply the worst case of PL/I data conversion routines. PUT EDIT and GET EDIT are somewhat less costly.

Structures are a two edged sword. Best to have them STATIC in the owning routine and have them based on pointers for other routines. Otherwise the prolog code can dwarf the code that does useful work.

PICTURE works very well in structures. Assigning between structures with fields of the same name in one PICTURE, and the other in machine format can often be performed without resort to data conversion routines.

Bottom line: Many advertised features are costly while PL/I offers remarkably efficient alternatives.

musicale wrote at 2021-11-27 00:09:48:

PL/I was also memory safe (unlike C/C++), and on Multics the stack grew up rather than down.

Probably the greatest reliability regression from Multics to Unix was implementing the kernel and user apps in an unsafe language.

(Or, more accurately, using an unsafe compiler/ABI, as there have been memory-safe compilers/runtimes for ANSI C such as Fail-Safe C.)

Someone wrote at 2021-11-26 21:43:33:

Another feature: keywords aren’t reserved words. For an example of where that can lead to see

https://multicians.org/proc-proc.html

The thinking behind that was that the set of keywords would grow over time, and that you couldn’t expect any programmer to know all of them.

There likely also are few programmers who do know all of them, as there are hundreds (see

https://www.kednos.com/pli/docs/reference_manual/6291pro_042...

for those of one implementation)

colejohnson66 wrote at 2021-11-27 05:13:15:

> Another feature: keywords aren’t reserved words.

This leads to this confusing block of code being legal (the syntax is almost certainly wrong however):

  IF IF = THEN THEN
        THEN := ELSE
    ELSE ELSE = THEN THEN
        THEN := ENDIF
    ENDIF

curtisf wrote at 2021-11-26 22:06:03:

Java has been using this approach with new reserved identifiers like `var` and `record` in the interest of backwards compatibility.

JavaScript has done something similar with its `await`, `let`, etc

the_only_law wrote at 2021-11-26 21:17:48:

Bit strings of arbitrary length, with bitwise Boolean operations

plus substr and catenation

This one actually piqued my interest in PL/I recently. Closest thing I’ve seen in modern languages is bitstrings in the BEAM.

Someone wrote at 2021-11-27 07:57:00:

Most modern languages have the concept of “libraries” that one can “import” and that can extend the language’s feature set.

For example, I don’t think C++ has such bit strings (the confusingly named _bitset_ (

https://en.cppreference.com/w/cpp/utility/bitset

) comes close, but don’t think it does the substr and concatenation parts. _basic_string<bool>_ has those, but not the Boolean operations), but they can be added when needed.

p_l wrote at 2021-11-26 22:01:57:

I believe both Ada and Common Lisp also share this ability, though Ada has nice support in its type system for doing things like mapping a portion of mmio register as bitstring that gets further defined as new type (whether enum, or integer, or string etc.)

the_only_law wrote at 2021-11-27 20:57:21:

If Ada has something like this, I haven’t seen it yet, but I’d be pleasantly surprised to learn otherwise.

I’ve heard about lisps having bitstrings support, but it was only briefly explained to me years ago and I haven’t seen it since.

throwawayboise wrote at 2021-11-26 21:39:33:

Yes I also thought of Erlang bitstrings when I read that.

moonchild wrote at 2021-11-27 09:51:46:

APL usually uses bitstrings, though that is an implementation detail.

Then, I guess APL is not exactly 'modern' anymore...

stackedinserter wrote at 2021-11-27 15:50:09:

To me it looks like a list of ways to shoot yourself in the foot or get a pagerduty call at 3am.

E.g. what's good about automatic conversion between float and str? It can save you a minute or two if you're making a PoC or fiddling around in a notebook, but it's hiding real issues in your data flow if you're developing something for production use.

throwawayboise wrote at 2021-11-26 20:26:06:

Used PL/1 in one of my first programming classes at university. Never before or since, but I don't recall it being a bad language to work in.

reidacdc wrote at 2021-11-26 21:19:27:

PL/I was the software-engineering language in my undergraduate computer science classes also, although it was beginning to be displaced by Pascal in the introductory courses, and C/C++ in the advanced courses. (Mid-1980s, in case that is not already clear.) It hung on because, we were told, it was still in demand in industry, particularly at telephone companies.

I did a group project with it, and the language was large enough that we found that each member of the group seemed to have learned a slightly different subset of it.

throwawayboise wrote at 2021-11-26 21:35:42:

Similar timeframe. Used PL/1, Pascal, and Modula-2 in three successive semesters IIRC.

jrd259 wrote at 2021-11-26 20:34:51:

PL/1 was the systems programming language for Multics. Certainly not a bad language to work with.

pjmlp wrote at 2021-11-26 21:12:00:

And plenty of other systems as well.

jhallenworld wrote at 2021-11-27 01:33:17:

I think the more relevant language was PL/M (actually PL/M-86) since it was available for 8086 early on. I don't think PL/M had any of these features, and C for the 8086 was better than it so that was the end of it.

Maybe if IBM chose 68K for the PC, we would have gotten the full blown PL/I.

Edit, well actually I found this "X3.74, PL/I General Purpose Subset (Subset G)" from 1983. This version at least has strings.

Look at manual disk here:

https://winworldpc.com/product/digital-research-pl-i-compile...

teddyh wrote at 2021-11-26 20:20:07:

Point 7 simply means that PL/I is very weakly typed. However, weak typing seems to now be unused in all modern languages; strong typing seems to be the current norm. (Not to be confused with static/dynamic typing, which is orthogonal.)

tofflos wrote at 2021-11-27 00:06:07:

1. Assignment by name. If A and B are structs (not official PL/I terminology), then A + B A = B copies similarly named fields of B to corresponding fields in A.

Do you mean something like with-expressions? See

https://docs.microsoft.com/en-us/dotnet/csharp/language-refe...

.

masklinn wrote at 2021-11-27 11:28:03:

The text isn't really clear, but from the wording it reads like the PL/I version will copy between existing instances _of completely unrelated types_.

AFAIK `with` will only "copy with overwrites", similar to e.g. Rust's `..x`.

steeleduncan wrote at 2021-11-27 09:34:29:

I believe golang has a feature like this as well

hungryforcodes wrote at 2021-11-27 15:18:06:

It's not really mentioned in the thread so far, but weirdly... PL/I was one of the inspirations for PL/M, which was the basis for CP/M... which was the basis for DOS... which led us to Windows... which leads us to today for those that don't use other OSs, etc, etc.

timbit42 wrote at 2021-11-27 16:22:12:

PL/I and PL/M are languages. CP/M is an operating system. The '/M' and that CP/M was mostly written in PL/M are the only 'basis' for CP/M from PL/M, but PL/M didn't affect what CP/M was created as.

hungryforcodes wrote at 2021-11-27 16:31:37:

Wait. You're saying that CP/M was created with PL/M but PL/M didn't affect CP/M?! It's been a long day huh?

timbit42 wrote at 2021-11-28 19:03:17:

I'm saying PL/M didn't affect the design of CP/M. You could write CP/M in C or assembly and it would be the same.

hungryforcodes wrote at 2021-11-29 08:11:36:

OK, I see what you mean. I think actually the BIOS was written in assembly, and you had to customize it for each system you made CP/M for -- so fair point. :)

denton-scratch wrote at 2021-11-27 11:07:38:

I used PL/M at one time - PL/M for microprocessors. It was an Intel compiler. PL/M was a subset of PL/1, I think with 64Kb object segments (which suited 8086 architecture). It was a nice, predictable systems programming language.

ptx wrote at 2021-11-26 21:01:33:

_4. A named array is normally passed by reference, as in F(A). But if

the argument is not a bare name, as in F((A)), it is passed by value._

Visual Basic (VB6/VBA) does exactly this, with the same syntax (except without the outer parentheses if the call is a statement).

BenFrantzDale wrote at 2021-11-27 12:49:45:

I believe C++ is getting this (in C++23? I think it was approved…) in the form of if you have `void f(auto&& x)` (taking a universal reference) and call `f(x)` it’s by reference but `f(auto(x))` the `auto(x)` will make a copy so `f` will be passed an rvalue reference.

kergonath wrote at 2021-11-26 21:24:26:

Fortran does it as well, I believe. The reason is that (A) is an expression whose value is that of A, and not a variable. This includes Fortran 2018; I am not arguing whether this is modern or not.

gnufx wrote at 2021-11-26 22:20:54:

Fortran actually passes by value-return, or whatever you call it, not by reference. The call may still be compiled by reference, of course, and it used to be fun to change the value of a number in the days when it wasn't in read-only storage.

bregma wrote at 2021-11-27 12:57:31:

"Fun" in the Dwarf Fortress sense. We once spent days trying to track down an obscure malfunction only to discover that someone had passed a 0 literal as an actual parameter to a certain function that assigned 4 to the formal parameter. For those following along at home, that meant the value of any 0 literal in the program suddenly became 4.

gnufx wrote at 2021-11-27 15:01:39:

I don't know what Dwarf Fortress is, but that's what I meant. Fortran now has INTENT specifiers, and you can expect constants to live in .rodata(?) on ELF systems, so you get a SEGV if you evade any compiler warning.

throwawayboise wrote at 2021-11-26 21:47:27:

It seems quite reasonable and intuitive to me.

TheCoreh wrote at 2021-11-27 06:59:37:

You can achieve this in JS via `f([...a])` instead of `f(a)`.

ptx wrote at 2021-11-27 09:53:50:

It's not quite the same, as JS doesn't have what's known as pass by reference in VB.

JS and Python always pass object references (pointers) by value (the value being the memory address), but in VB you can pass references by reference, references by value, values by reference or values by value, which lets you do things like this:

    Dim x as Integer
  x = 1
  SomeFunction x
  Debug.Assert x = 2    ' our local variable was reassigned from elsewhere

manbart wrote at 2021-11-26 19:56:00:

I think 2,3,5 and 7 apply to Rexx as well, another IBM created language. I wouldn’t call it modern however

npsimons wrote at 2021-11-26 20:48:11:

> I wouldn’t call it modern however

I wonder how the claim stands up if we remove that "modern" adjective. Or perhaps consider that "modern" languages don't have some of these features as they turned out to be "harmful" (see Dijkstra's "GOTO considered harmful") - at least one reply in that thread calls out one of these "missing features" as very dangerous, sort of like how some C++ projects will forbid features (private inheritance, etc) for ease of maintenance.

kgeist wrote at 2021-11-27 08:01:43:

If A and B are structs (not official PL/I terminology), then A + B A = B copies similarly named fields of B to corresponding fields in A.

In Go, you can copy a variable of type A to a variable of type B if the types have similarly named fields of same types, although an explicit cast is required.