home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
The C Users' Group Library 1994 August
/
wc-cdrom-cusersgrouplibrary-1994-08.iso
/
vol_300
/
357_01
/
cstar1.exe
/
PL68K.DOC
< prev
next >
Wrap
Text File
|
1991-06-20
|
34KB
|
718 lines
PL/68K.DOC: The Dr. Dobb's Journal Article
June 25, 1991
The following is a slight revision of an article that appeared in the
January, 1986 issue of Dr. Dobb's Journal. Alas, the figures are no
longer available.
This article gives the original vision behind the language. It does *not*
describe the current language, which is CSTAR. (The PL/68K language
evolved in the years 1986 and 1987 to become the CSTAR language.)
See the files CSTAR.DOC and DESIGN.DOC for a discussion of how
PL/68K became CSTAR.
C BECOMES 68000 ASSEMBLY LANGUAGE
One day not long ago, I became embroiled in an old debate with another
programmer named Charlie...
"The programming team I manage is about to start a big project," I said,
"and I must decide which language to use."
"Really? Which languages are you considering?"
"C and 68000 assembly language. The product will have strong
competition, and great performance is crucial, so it's reasonable to
consider assembly language. On the other hand , C is so much easier to
use.
"Why don't you program in C and recode in assembly language as
needed?" Charlie asked.
"Of course I've considered that. It might work as far as execution speed
is concerned, although I'm not sure. C doesn't let you allocate registers
globally, and that's a big handicap. Speed is not the only problem,
though. The code must be compact, but our C compiler produces code
that is 50 percent larger than assembly language. No, there's no doubt
about it--eventually the program will have to be written in assembly."
"Do the initial prototyping in C. That's the right way," Charlie persisted.
"When the program is finished recoding in assembly language will be
much easier."
"Hmmm. I'm not convinced. Recoding is going to be expensive; we'll
end up debugging the whole program twice. There might even be
pressure from higher management not to recode and come out with an
inferior product."
Charlie just snorted and walked away, muttering something about
assembly language being a throw back to the dark ages.
WRITING IN BOTH C AND ASSEMBLY LANGUAGE
Fortunately, my friend John overheard this conversation. John and I have
worked together for 15 years, and we enjoy discussing problems that
come up on the job. John laughed, "Charlie is more interested in being
right about C than in solving your problem."
"You sound more sympathetic."
"Well, your choice is crucial. Which language you use determines, to a
large extent, how your project will turn out."
"Yes. What bothers me most is that I've got to choose now, but I won't
know until the project is almost over whether the choice was correct."
"I think I know a way around this dilemma--it's a language I invented
called PL/68K."
"John, my only options are C and 68000 assembly language."
"Don't be fooled by the name. PL/68K isn't really an independent
language but a way of using C to do assembly-language programming."
"John, you are not making sense!"
"Let me explain. You can think of PL/68K as being either C or assembly
language--either/or. But in fact, you can run a program written in
PL/68K through both the PL/68K assembler and any standard C
compiler. PL/68K is both C and assembly language at the same time."
"Wait a minute. You are going much too fast," I said. "First of all, you
can't possibly compile an assembly-language program with a C compiler!
Assembly language doesn't look anything like C--the C compiler will
spit out a thousand error messages!"
"PL/68K doesn't resemble 'traditional' assembly language. Forget what
assembly language usually looks like and ask yourself, 'What are the
characteristics of assembly language?' "
"Go on," I replied. "You tell me."
"First, assembly language allows full access to all machine resources--all
registers, all locations in memory (including the run-time stack), all I/O
ports, all privilege modes, and all machine instructions. Second, there is
a one-for-one correspondence between the source code you write and and
the object code produced by the assembler. You always know what code
a particular assembly-language construct generates; assemblers neither
rearrange code nor 'optimize' code away nor add anything extraneous.
Assemblers are very literal-minded. Thus, assembly language ensures
zero time and space overhead."
"You're saying that assembly language gives you complete control over
the machine, without a compiler getting in the way."
"Exactly. Now, suppose we say assembly language is any language that
(1) allows complete access to all machine resources, (2) provides a clear
correspondence between source code and object code, and (3) imposes
zero time or space overhead."
SEMANTIC IDENTITY
"Hmmm," I mused. "This definition doesn't say what assembly language
looks like. It could even look like C. But I still don't understand. If you
run a PL/68K program through an assembler, you will get one program.
If you run the same source through a C compiler, you will get a second
program. The two programs are not going to do the same things--similar
things, maybe, but not the same things. The fact that the source code is
the same doesn't matter. To put it another way, given a result desired
from a specific PL/68K program, we would still have to choose between
assembling the program with the PL/68K assembler or compiling it with a
C compiler."
"You've stated the problem very well," John said, "but I have discovered
that it's possible to design PL/68K so that the program produced by the
PL/68K assembler will work in the same way as the program produced
by the C compiler."
"That sounds impossible!"
"I don't think so. Let's turn the problem around. Suppose we design
PL/68K according to what might be called 'the principle of semantic
equivalence.' This principle states that a program, when assembled by
the PL/68K assembler, must work in the same way as when it is compiled
by a standard C compiler. Now let's ask ourselves, 'What needs to be
eliminated from PL/68K to guarantee semantic equivalence?' " (See
Figure 1.)
"Tell me," I said, "how much of C is left after the principle of semantic
equivalence takes its toll?"
"Just about all of it. The preprocessor is identical to the C preprocessor.
All declarations and structure statements are present. Function are
unchanged, as are Boolean and relational operators and expressions.
PL/68K is a little more fussy about types and type conversions than C is
and there are some slight restrictions are placed on how complicated
arithmetic expressions can be.
"You keep talking about PL/68K being assembly language," I said.
"How is it possible to produce code the quality of assembly language
from a language that is a subset of C?"
"I haven't shown you the whole language yet. Two other rules guide the
design of PL/68K. These rules, together with the principle of semantic
equivalence, determine the form and content of PL/68K. The two rules
are 'the code selection rule'--the assembler for PL/68K does no code
selection, and all arithmetic operations in PL/68K correspond to unique
68000 machine instructions; and 'the register allocation rule'--the
assembler for PL/68K does no register allocation. The programmer must
specify how registers will be allocated.
"In short, these rules say that an assembler for PL/68K never has to make
any significant decisions. Because the PL/68K assembler knows how to
select code and allocate register, it will never need any of the fancy
techniques used by optimizing compilers, but it will be able to produce
code that is just as good as assembly language.
"I like to think of the assembler for PL/68K as a simple compiler,
consisting of a parser, a straightforward code generator and a peephole
optimizer. The whole job should take about a year to complete rather than
the 10 to 20 programmer years for a typical optimizing compiler. Using
compiler technology to write an assembler was the initial idea that started
me thinking about PL/68K."
Now that I've presented the general ideas behind PL/68K, I'll drop this
dialogue format and these fictional characters and look at the details of the
language.
SPECIFYING REGISTERS
Because PL/68K is both assembly language and C, some way must be
found to deal with assembly-language constructs such as registers,
address modes, and individual machine instructions, while at the same
time remaining compatible with C. These assembly-language constructs
are represented by reserved words, shown in Table 1.
As for the registers of the 68000, d0 through d7 stand for the data
registers, a0 through a7 for the address registers, pc for the program
counter, ssr for the status register, and ccr for the condition code register,
which is the lower byte of the ssr.
Standard aliases are also defined. Register a7 can also be called sp, ssp,
or usp to denote the stack pointer (or system stack pointer or user stack
pointer). The reserved words r0 through r7 are synonyms for d0 through
d7, and the names r8 through r15 are synonyms for registers a0 through
a7.
All registers on the 68000 are 32 bits long (except the status registers),
but not every instruction uses all 32 bits of a register. Besides long (32-
bit) operations, byte-length (8-bit) and word-length (16-bit) operations are
permitted on data registers, and word-length operations are permitted on
address registers. To represent the length of an operation, the name of
any data register can be followed by a 'b' to denote to denote byte length
or w to denote word length. Thus, d0 stands for the long register d0,
d0w stands for the word-length register d0, and d0b stands for the byte-
length register d0. Address registers are treated in a similar manner,
except that byte-length operations are not permitted.
ADDRESS MODES
The 68000 has 12 different address modes, or means of accessing
operands. (See Table 2) The address modes are represented in PL/68K
by five of C's operators, namely &, *, ++, P P, and P>.
Let's look, for example, at the Address Register Indirect with
Postincrement address mode. (It's a lot easier to use than to say.) This
mode uses the contents of an address register as the address of an
operand. After the operation is performed, the address register is
incremented by 1, 2, or 4, depending on the size of the operation. In
traditional assembly language, that mode applied to address register a0
would be written as (a0)+. In PL/68K, that address modes is represented
by *a0++. for example, you would write d0b = *a0++; in PL/68K
instead of MOVE.B (a0)+, d0b.
The word primitive denotes what is called an effective address in
machine-language terms. A primitive describes an operand, which may
be in a register, on the run-time stack, or in static memory. In PL/68K,
the valid forms of primitives are determined by the address modes I've
just discussed.
DECLARATIONS
In effect, declarations produce DC (define constant) and DS (define
storage) pseudo-operations. (See Table 3) Although declarations produce
no executable code they determine what code gets produced by arithmetic
operators. For instance, if a is an integer, the assignment statement a *=
b, which multiplies a by b, generates a MULS (signed multiply) machine
instruction, but if a is an unsigned integer or pointer, the assignment
statement generates a MULU (unsigned multiply) instruction.
As another example, the assignment a += b. which adds b to a, generates
and ADD.B (byte length add) instruction if a is a char, but it generates an
ADD.W (word length add) instruction if a is a long word or pointer.
ASSEMBLY-LANGUAGE INSTRUCTIONS AND PSEUDO-OPERATIONS
Reserved identifiers also stand for 68000 machine-language instructions
and pseudo-operations. A library of pseudofunctions must be linked with
a PL/68K program when it is translated with a C compiler. This library,
called the ops library, contains declarations and functions that allow C
programs to simulate the effect of 68000 machine instructions and
pseudo-operations. (See Table 4)
The pseudofunction btst ( ), for example, simulates the BTST (bit test)
machine instruction. In PL/68K, you would write btst(1,d0b); in those
places where you would write btst.b#11,d0 in traditional 68000 assembly
language.
Other pseudofunctions allow PL/68K programs to refer to assembly-
language pseudo-operations. The PL/68K assembler translates the org( ),
even( ), bss( ), text(), and data( ) pseudofunctions to the ORG, EVEN
BSS, TEXT, and DATA pseudo-operations. Similarly, the PL/68K
assembler translates the dcb( ), dcw( ), dcl( ), dsb( ), dsw( ), and dsl( )
pseudofunctions to the DC.B, DC.W, DC.L, DS.B, DS.W, and DS.L
pseudo-operations. None of these pseudofunctions has any effect when a
C compiler translates a PL/68K program. In other words, the
corresponding pseudofunctions in the ops library do nothing.
You may be wondering why I keep calling these routines
pseudofunctions. After all, they are perfectly good functions when
compiling a PL/68K program with C compiler. When you turn the
program through the PL/68K assembler, though, it translates
pseudofunctions directly in 68000 machine instructions or pseudo-
operations.
EXPRESSIONS AND ASSIGNMENT STATEMENTS
I've covered the components of low-level assembly language--register,
address modes and effective addresses, machine instructions, and
pseudo-ops. Now let's see how you put these components together to
make expressions and assignment statements.
Expressions are just like C expressions, but they may not be arbitrarily
complex. As the code selection rule requires, PL/68K defines what code
arithmetic expressions will generate.
(See Table 5.) The += and + operators generate the ADD instruction, the
P= and P operators generate the SUB instruction, and so on. Many
machine instructions on the 68000 have several variants--for example,
ADDA adds address registers, ADDI adds literal data, and plain ADD
adds to data registers and memory locations. The assembler has to do
some code selection, but the choice is easy; even traditional assemblers do
that much.
BOOLEAN EXPRESSIONS
Boolean expressions in PL/68K are similar to Boolean expressions in C.
All the boolean operators !, | |, and && and all the relational operators ==,
!=, <, <=, >, and >= are allowed.
Boolean expressions can only appear in the appropriate part of if, do,
while, and for statements. The code fragment a ==b; is not valid in
PL/68K outside a structure statement. This restriction eliminates a whole
class of hard-to-find errors (the programmer almost certainly meant to say
a = b).
Because Boolean expressions appear in limited contexts, less work is
required to evaluate them. (See Tables 6 and 7) Boolean expressions
generate the TST (test operand) or CMP (compare) instructions followed
by some form of the Bxx (conditional branch) instruction. The Bxx
instruction chosen depends on the relop and the declared type of the
operand being tested. The NOT (!) operator generates no code at all but
instead simply reverses the "polarity" of one or more BXX instructions.
For example, the statement
if(a == 0)
generates
TST A
BNE
while the statement
if(!(a == 0))
generates
TST A
BEQ
Similarly, the | | and && operator generate no extra code. Incidentally,
you can use parentheses in Boolean expressions to affect the order of
binding of Boolean operators. For instance, the Boolean expression
if(a && !(b<5 | |b>20))
is valid and generates
TST A
BEQ
CMPI 5, B
BLT
CMPI 20, B
BGT
You can specify condition code values directly. (See Table 8) For
instance, the statement
if (ccr=Z)
tests the current value of the zero bit in the condition code
register and generates the BNZ (branch not zero) instruction. Z is a
macro in C, defined in the ops library, which expands to a call to the
pseudofunction cc_z( ).
STRUCTURE STATEMENTS
I said earlier that PL/68K has all C's structure statements--if, do, while,
for, and switch. They look exactly like C code, but curly braces are
required surrounding statement lists in structure statements. In other
words, structure statements have the form
if(...) {statement list}
if(...) {statement list} else {statement list}
while(...) {statement list}
do {statement list} while(...);
for(...) {statement list}
switch(...) {statement list}
In my opinion, allowing curly braces to be optional is a big flaw in C. In
this example:
if(abc<5)
xyz=5;
if(abc<6)
xyz=6;
the indentation is misleading and will probably cause a bug. This kind of
error can be extremely difficult to find.
Let's see what code PL/68K's structure statements produce. In the
accompanying tables, the dollar sign denotes code that corresponds to
some language construct. for example,
$ statement list $
stands for whatever code is generated for the statement list. The statement
list could be arbitrarily complicated--for instance, it could contain nested
structure statements. the notation
$ Evaluate boolean. If false, jump to label1 $
indicates that code is generated for the Boolean expression such that a
jump to label1 is taken if the Boolean expression is false, Otherwise,
control falls through to the following code. Labels are indicated in the
usual way, by identifiers followed by colons. All generated labels are, of
course, unique, even though they may have identical names in the tables.
Table 9 shows the if statement. When an if statement contains no else
clause, the Boolean expression is evaluated, and control either falls
through to the then clause or jump is make to the end of the statement.
Similar code is generated when the if statement contains an else clause.
the Boolean expression is evaluated, and control either falls through to the
then clause or a jump is made to the else clause. A BRA (branch always)
instruction following the then clause skips around the else clause.
The code generated for the while statement, shown in Table 10 might be a
little controversial. the first instruction is a branch to the end of the loop
so that the loop test occurs at the bottom. This produces the fastest code
unless the while loop is executed less than once on average. In the rare
cases in which this jump is unwanted, the programmer must simulate the
loop in some way.
Notice the labels called continue_label and break_label. These are used as
target labels for the break and continue statements. In other words,
within a while, do, or for statement, the effect of a continue instruction is
to generate a branch to the continue_label defined for that statement.
Similarly, a break statement generates a jump to the appropriate
break_label. As in C, you can also use the break statement inside a
switch statement.
The while statement generates different code if the Boolean expression is
a nonzero constant. this is a common idiom in C, and the definition of
PL/68K ensures that there is no time penalty for using it.
The code for the do statement, shown in Table 11 is similar to the code
produced by the while statement. The code for the for statement (See
Table 12) is more interesting. If the loop test in a for statement is
nontrivial, the code for it appears at the bottom of the loop. Note also that
the syntax of the for statement is more restricted than in C.
The switch statement, shown in Table 13 generates a jump table--i.e., a
table of addresses. code is generated that jumps through that table to the
proper case statement, based on the contents of a register. Note that the
switch statement destroys this register.
It is sometimes better to generate a sequence of tests rather than a table
jump, but the case statement always generates a table jump. Remember,
each language construct in PL/68K stands for a particular sequence of
code--if you want a sequence of tests, use a sequence of if statements; if
you want a table jump, use a switch statement.
Many compilers generate jumps to jumps when they translate structure
statement, but the definition of PL/68K requires that all jumps to jumps
(and jumps to return statements) be eliminated. The assembler can do this
in several ways. For instance, if the assembler creates a parse tree for an
entire function before any code is generated, it's easy for the code
generator to look at the target of any jump to see if it is another jump or a
return instruction. Alternatively, the assembler can use a standard
peephole optimizer.
FUNCTION CALLS
Functions in PL/68K can have formal parameters and local arguments,
just as in C. The code shown in Table 14 is generated by function calls.
Code is generated to push all arguments on the stack, a JSR (jump to
subroutine) instruction is generated to pop arguments off the stack. One
long word is always reserved on the stack for the first argument, which
shortens the calling sequence when there are less than two arguments.
The ADD instruction can be eliminated by having the called program,
instead of the calling program, pop the arguments off the stack, but the
sequence shown in Table 14 is the fastest. If you eliminated the ADD
instruction and pushed the arguments in the same way (that is, above the
return address), the called program would need to do much more work to
pop off its arguments. You could also push the actual arguments below
the return address, but that way actually increases the length of the calling
sequence. In order to push arguments below the return address, you
would have to use an instruction such as move arg,m(sp), which is 2
bytes longer than move arg,P(sp).
Unlike standard C, PL/68K does specify the order in which arguments
are pushed, namely in reverse order. thus, a function that takes a variable
number of arguments--printf( ) for instance--will find its first argument
on the top of the stack.
Of course, it is often best to pass arguments in registers, but PL/68K
doesn't need a separate mechanism to do this. Suppose you have a
function called g( ) whose two arguments are passed on the stack. to
change g( ) so that it will take its arguments in registers, you must define
the following macro
#define g(a,b) d0=a; d1=b; g1()
and change g's name to g1. Notice that a statement such as
if(...) g(x,y);
cannot cause problems in PL/68K because you must write
if(...) {g(x,y);}
instead. (If braces were omitted, after macro expansion, the code would
be
if(...)d0=a; d1=b;g1();
and only the assignment d0=a would be part of the if statement.)
This macro might generate redundant code. Suppose it were called with
d0 as the first argument, for instance. The macro would expand to d0=d0
and generate the instruction move d0,d0. to handle that problem, the
PL/68K assembler eliminates redundant moves. If you must generate
such a redundant move for some reason, use a pseudofunction.
Pseudofunctions are never second-guessed by the assembler.
ACCESSING VARIABLES WITHIN FUNCTIONS
Now that I've covered the passing of arguments to a function, I'll explain
how the function gets hold of those arguments. This is a complicated
subject, so before getting involved in some messy details, let's handle the
easy cases.
First, PL/68K keeps its hands off all registers declared outside any
function. These global registers can be accessed from within functions,
but PL/68K never generates code to save or restore them. Consequently,
you can prevent PL/68K from interfering with any register simply by
declaring that register outside a function. By the way, the register
keyword is not valid outside functions, but that does not prevent you
from declaring register variables anywhere you wish, either in C or in
PL/68K. For instance, you can declare a0 to be global simply by saying
char *a0;
outside any function.
Second, except for these global registers, all registers used in a function
are saved on entry and restored on exit from the function. the assembler
uses the MOVEM.L (move multiply register. long) instruction for this
purpose. Accessing register variables declared in a function is, of course,
easy.
Third, if a local variable is declared to be static, memory is allocated to
that variable in static memory, not on the stack. No extra code is needed
to access that variable, either on function entry or exit.
Static internal variables are not very useful; because the values of static
internals are retained between invocations of a function, static internals are
destroyed by recursive function calls. Also, on many machines
(including the 68000) accessing a variable in static memory is more
expensive than accessing a "local auto" variable--that is, a local stack
variable. At any rate, generating code for static internal variables is
straightforward and is not affected by the following complications.
Functions need to access two kinds of nonregister variables: formal
parameters and local auto variables. Both types are allocated on the stack.
You have three choices for how PL/68K generates code for these stack
variables. You make your choice using one of three pseudofunctions:
base(An), base(sp), or nobase( ). if none of these appears in a function,
the default is base(sp).
First, you can have PL/68K access stack variables via an address register
that is different from the stack pointer, as shown in Table 15. If the
base(An) pseudofunction appears anywhere in the function (where An is
any address register except the stack pointer, a7), PL/68K generates a
LINK (link and allocate) instruction on function entry and UNLK
instruction on function exit. All stack variables are accessed via the
address register named in the base(An) pseudofunction.
Second, you can have PL/68K access stack variables via the stack
pointer, as shown in Table 16. The assembler generates this kind of code
if the base(a7) or base(sp) pseudofunction appears anywhere in the
function. The code generated for sp-based functions is more complicated,
but more efficient, than that for An-based functions. If an sp-based
function contains no function calls, the stack pointer is not incremented on
function entry and local auto variables are accessed using positive offsets
from the stack pointer. If the function does contain other function call,
however, space is reserved for local auto variables by incrementing the
stack pointer on function entry, and local auto variables are accessed using
negative offsets from the stack pointer.
Third, you can access stack variables by hand. If the no_base( )
pseudofunction occurs anywhere in the function, no code is generated on
function entry or exit, declarations of stack variables are ignored, and no
explicit references to stack variables are permitted. the no_base( )
pseudofunction is useful when you must play some kind of game with the
stack.
Sp-based functions are somewhat dangerous. If the stack pointer is
changed using a pseudofunction, the offsets used to access stack variables
are going to get out of sync. to handle this problem, several
pseudofunctions, shown in Table 17 allow you to indicate that the offsets
should be changed.
The push(arg) and pop(arg) pseudofunctions are equivalent to the
move(arg,*P Psp) and move(*sp++,arg) pseudofunctions, but in
addition, they tell the PL/68K assembler to adjust the offsets used to
access stack variables in sp-based functions. The addsp(n) and subsp(n)
pseudofunctions are equivalent to the addi(n,sp) and subi(n,sp)
pseudofunctions, but again they tell the assembler to adjust offsets.
Finally, the adjsp(n) pseudofunction generates no code but tells the
assembler to adjust the offsets. These pseudofunctions have no effect on
An-based functions.
SUMMARY
To summarize the strengths and weaknesses of PL/68K, the syntactical
restrictions that make PL/68K a strict subset of C are listed as follows:
o All operand/operator combinations correspond to an instruction in the
68000 instruction set.
o Assignment statements are slightly restricted.
o Statement lists must be surrounded by curly brackets.
o There are no floating point or double constants or operators.
There are no additions or changes to C syntax. PL/68K and assembly
language are semantically identical; the advantages of PL/68K over
assembly language are all syntactical:
o C syntax makes programs easier to read.
o C syntax eliminates many common coding errors.
o Far fewer visible labels are required.
PL/68K is not the successor to C nor is it superior to C for most
programming projects. The advantages of PL/68K over C apply only in
limited circumstances, but when performance is paramount, PL/68K
stands out:
o All machine resources and instructions are available.
o Global allocation of registers is possible.
o Total control over generated code is possible.
o Generated code is smaller and faster.
Figure 2 shows the relationship between PL/68K and its two parent
languages. PL/68K is only a subset of C; not all of C is included. On the
other hand, C must be augmented by the ops library in order to simulate
all the machine resources of assembly language.
Listing One shows an example of a PL/68K function. This function finds
a name in a symbol table and returns information about that name in
registers. Listing Two shows the code generated by the PL/68K
assembler for that function.
CONCLUSION
PL/68K started life as a C-like assembly language for the 68000 chip.
High-level assemblers are not new--a paper by Niklaus Wirth (Journal
ACM 15, January 1, 1968) described a high-level assembly language for
IBM 360 machines.
My early thinking about PL/68K was influenced by Wirth's paper and by
the register allocation and code selection principles. At that stage, I was
interested in using compiler technology to create better assemblers. I saw
no particular reason to make PL/68K a subset of C.
I felt dissatisfied with the initial version of PL/68K; it was doomed to be
"just another computer language." Not wanting to add another minor
dialect to the Babel of computer languages, I decided to make PL/68K's
syntax identical to C's. This was a lucky decision.
From the first, PL/68K had to be able to name all machine resources,
including machine instructions and assembly-language pseudo-ops. Early
versions of the language allowed "raw" lines of assembly code to be
interspersed with C-like lines of code. Although this approach had some
merit, my decision to make PL/68K's syntax compatible with C
convinced me to use functional notation to represent assembly-language
features. This change was also fortunate.
I began to speculate about what would happen if a PL/68K program were
compiled with a C compiler. I asked myself, suppose the program
produced by the C compiler and the program produced by the PL/68K
assembler were semantically equivalent? Suddenly, PL/68K became not
just another assembly language but a new way of using C.
The principle of semantic equivalence guided a redesign of the language;
any construct that violated this principle had to go. In addition, I invented
the ops library so that C programs could simulate 68000 machine
instructions. Along with the ops library, the concept of pseudofunctions
was born, and the language was complete.
PL/68K allows me to sidestep a question that has been haunting me ever
since I started programming--whether to program in assembly language
and accept the resulting inconveniences or to program in a high-level
language and accept a final product that is larger and slower than it could
be. PL/68K solves that dilemma.
You can design and write a program in C, keeping in mind the possibility
that you will convert it to PL/68K eventually. You can write difficult parts
of the program, or parts of the program, you can produce a PL/68K
version of the program, if desired, by rewriting or excluding those parts
of the program that use full C. You then test the PL/68K version and
improve its performance as much as you require.
PL/68K hits precisely the right level of abstraction for systems
programming. All the features of C allow you to design and code
programs easily, but when you need to do low-level work, nothing stands
in your way.
A final thought--you could transfer most of the syntax and all the design
rules of PL/68K to a similar language for other machines. In this limited
sense, PL/68K is a machine-independent assembly language--PL/68K
programs are much more portable than programs written in traditional
assembly language.