Discussion:
transpiling to low level C
Add Reply
Thiago Adams
2024-12-15 03:05:13 UTC
Reply
Permalink
I am working on a C backend that generates simple C code.

You can test it here:
http://thradams.com/cake/playground.html

Objective
- Shift all the complexity of the C compiler to the frontend.
Why? This approach simplifies having one frontend and multiple
backends.
- Use C as an intermediate language to feed a backend (any C compiler
can act as the backend).
The backend can then focus solely on code generation.
- Code is not portable

Removed C89 Features
- Preprocessor
- sizeof
- typedef
- enum
- Constant expressions (the final result is precomputed during earlier
stages)
- const
- I may also remove switch.
- struct are declared only at external scope

Most features from C99, C11, and C23 are not included.
However, features directly related to code generation may be supported,
in the future such as:
- restrict
- inline
- atomic
- _NoReturn

Current Status
This project is very new and still contains bugs.


When generating a constant like '(unsigned char) 1', the cast is
removed. The rationale is that type-specific representations aren't
necessary, as values are promoted automatically.
Similarly, 'nullptr' is replaced by '0' instead of '(void*)0'.
Constructs dependent on type (like 'sizeof', '_Generic', etc.) have
already been removed in earlier stages.

Example:

unsigned char c = u8'~';

Generated Code:

unsigned char c = 126;

Instead of:

unsigned char c = ((unsigned char)126);

For larger types like 'unsigned long long', a suffix is still included.

Initialization Strategy

Currently, I use 'memset' to initialize structures to zero. For
instance:

struct X x = {};
memset(&x, 0, 8 /*bytes*/);


Boolean Conversion
The behavior of converting integers to 'bool' is still incomplete.
For example:

bool b = 123;

Here, 'b' should have the value '1' but today it is 123.
Lawrence D'Oliveiro
2024-12-15 04:31:07 UTC
Reply
Permalink
Post by Thiago Adams
- Use C as an intermediate language to feed a backend (any C compiler
can act as the backend).
The backend can then focus solely on code generation.
Would this be for architectures not already covered by LLVM?
Thiago Adams
2024-12-15 10:44:34 UTC
Reply
Permalink
Post by Lawrence D'Oliveiro
Post by Thiago Adams
- Use C as an intermediate language to feed a backend (any C compiler
can act as the backend).
The backend can then focus solely on code generation.
Would this be for architectures not already covered by LLVM?
Yes.
Another objective is learning how everything works so I am planning to
write the backend ( a simple C compiler that generates code).
I also think LLVM is too big.
Lawrence D'Oliveiro
2024-12-15 22:22:09 UTC
Reply
Permalink
Post by Thiago Adams
I also think LLVM is too big.
How about QBE, then <https://c9x.me/compile/>.
Thiago Adams
2024-12-15 23:22:55 UTC
Reply
Permalink
Post by Lawrence D'Oliveiro
Post by Thiago Adams
I also think LLVM is too big.
How about QBE, then <https://c9x.me/compile/>.
QBE is just for linux and it does not generate code directly. C is
everywhere.
But I am reading QBE docs to see what is necessary in a IL.
BGB
2024-12-16 07:02:41 UTC
Reply
Permalink
Post by Thiago Adams
Post by Lawrence D'Oliveiro
Post by Thiago Adams
I also think LLVM is too big.
How about QBE, then <https://c9x.me/compile/>.
QBE is just for linux and it does not generate code directly. C is
everywhere.
But I am reading QBE docs to see what is necessary in a IL.
I agree that both LLVM and GCC are too big.

I am not sure the minimum size of a usable C compiler, my current
estimate is probably somewhere in the area of 50-100 kLOC.

My current compiler is a bit bigger than this, and my own past attempt
to implement a C compiler in under 30k lines had failed.

My current compiler use to be smaller, but multiple backends and similar
did make it bigger:
SH-2, SH-4, BJX1 (older)
BJX2, RISC-V (current)



If I were to estimate a feature set for a 3AC IL:
Basic UNARY and BINARY operators;
Also COMPARE and similar;
Load and Store Index operators;
With support for an immediate index;
GOTO
IF-GOTO
CALL (with a list of argument variables and a target variable).

One doesn't need any higher-level control flow constructs.

Any built-in support for "switch()" can be optional (the main reason to
have so is that it can make switch a little faster, but isn't required).

Earlier on, my compiler didn't have any backend support for this, and
would instead decompose the switch into "if/else/goto" logic. You can
then use recursive binary subdivision to implement the list of case
labels (or linear probes if N is small).

BGBCC still uses binary subdivide if the case-labels don't satisfy the
constraints for a branch-table (Say, needs to be 16..256 case labels
with a density over 75%).

As can be noted though, BGBCC uses a stack IL, with the 3AC IR currently
only existing in the backend. But, there are possible merits to a 3AC
IR, and I had almost considered jumping over a few times, but was
largely held back by inertia.

As noted though, not too long ago did relicense it to MIT-NA.

...





Oh well, have been distracted recently:
New filesystem for my project;
Have started work on trying to build a printable-electronics printer.

Essentially, the printer will be a very non-standard form of inkjet
printer with a construction more like that of a CNC mill mixed with a 3D
printer, but much lighter-duty. Partly realizing that a paper printer
wouldn't really work (though, TBD if basically "the cheapest drill-press
x/y table I could find" will be sufficient for the X/Y base). Resembles
a mill table, but made out of aluminum and uses allthread and hex-nuts
for the X and Y axes. CNC converting it mostly with parts I am making
out of 3D printed plastic.

Will hold off on the PEDOT:PSS ink for now, this is likely to be the
most expensive part of the project (then again, did burn a good chunk of
the cost of this ink on getting a 3D printer and filament, but at least
these will have other uses; whereas there is a big dependency ladder to
climb, and until all dependencies are met, having a bottle of PEDOT:PSS
ink would be kinda useless).

Even then, it would be limited to PMOS logic (both NMOS and CMOS would
require materials currently outside of my price range).

So, likely test case is to build the printer and use it for CMYK
printing and similar (can verify operation, and CMYK inks are at least
affordable).

A difficult part of the process will be converting netlist output from a
Verilog compiler into actual logic. Current plans are to tell the tools
it is targeting a funky version of an ECP5 (one pretty much entirely
lacking in block-memory or hard multipliers). Pretty much everything
will need to be LUT4's, FFs, and some amount of distributed RAM (though,
not yet sure how this will work; will effectively need to implement
something with 2-ports, 4-bit address, and 1-bit wide).


Some of this (such as trying to figure out how to best construct a
LUTRAM) seems too complicated to try throwing at a genetic algorithm.
Though, could simplify the task (for a GA) if its view of everything is
as operating on a subset of logic gates (AND, OR, NAND, NOR, NOT, ...;
would exclude XOR and XNOR as these can't easily be fit into the same
footprint).

Granted, another possible strategy being to rewrite the netlist to
decompose all of the higher-level logic into basic logic gates, and then
run the place-and-route logic purely in terms of gates
(AND/OR/NAND/NOR). So, say, if one requests a CARRY, it is first
decomposed into the more basic logic gates, which can be more easily
mapped to a uniform grid.

Granted, this sort of thing is likely to be overly niche.
Would be slower than an FPGA, and non reprogrammable, but per-unit could
be cheaper than using FPGAs, and still viable for running off individual
and small numbers of devices (unlike ASIC) and could be made within the
realm of affordability for a hobbyist (more so if the requisite machines
could be mass-produced, similar to the situation with 3D printers;
nevermind the cost of the needed inks).

Dunno, possibly (besides Verilog), a lower-level possibility would be to
expose the gates more directly. Could almost express it in a similar
form to an early form of Minecraft's "redstone logic", except slightly
less limited.

Some similar strategies would likely apply, say, for example, if one
created "master clock" by building a big ring of NOT gates (with an odd
number of NOT gates), or BUF gates (with a single NOT gate), and
whatever speed the ring of not-gates goes, is ones clock-pulse.




OTOH, Filesystem thing, as noted:
Structure partly resembles a hybrid of NTFS and EXT2;
Uses an inode table where the inode table is itself an inode;
The inodes consist of multiple tagged structures (like NTFS);
Uses fixed-size directory entries (sorta like FAT);
Directories use a variant of AVL trees.
Has file compression, currently applies to larger virtual blocks;
Has symlinks and the ability to inline small items into the inodes.
Size limit for inline storage is 128 bytes with 256 byte inodes.

Currently, I have an implementation in around 3.5 kLOC, which is
slightly less than my FAT32/VFAT driver.

Still pretty experimental, and pretty much no other OS could access it.

It exists mostly because none of the other filesystems seemingly both
have the feature-set I wanted, while also not being horridly
over-complicated.


Granted, AVL trees are possibly more complicated than ideal, and trying
to debug it, dealing with this had been a significant source of bugs.

B-Trees are more popular here, but B-Trees have more up-front complexity
cost.

Granted, linear search or hash chaining would have been simpler; but
doesn't scale as well O(n) vs O(log n).

...
Thiago Adams
2024-12-16 11:17:45 UTC
Reply
Permalink
Post by BGB
Post by Thiago Adams
Post by Lawrence D'Oliveiro
Post by Thiago Adams
I also think LLVM is too big.
How about QBE, then <https://c9x.me/compile/>.
QBE is just for linux and it does not generate code directly. C is
everywhere.
But I am reading QBE docs to see what is necessary in a IL.
I agree that both LLVM and GCC are too big.
I am not sure the minimum size of a usable C compiler, my current
estimate is probably somewhere in the area of 50-100 kLOC.
My hope is 10-30K lines. But remember this is a simpler version of C.
This is the objective. It also may work in one pass, without building a
lot of data structures for the AST.
Post by BGB
My current compiler is a bit bigger than this, and my own past attempt
to implement a C compiler in under 30k lines had failed.
My current compiler use to be smaller, but multiple backends and similar
  SH-2, SH-4, BJX1 (older)
  BJX2, RISC-V (current)
  Basic UNARY and BINARY operators;
    Also COMPARE and similar;
  Load and Store Index operators;
    With support for an immediate index;
  GOTO
  IF-GOTO
  CALL (with a list of argument variables and a target variable).
One doesn't need any higher-level control flow constructs.
Any built-in support for "switch()" can be optional (the main reason to
have so is that it can make switch a little faster, but isn't required).
Earlier on, my compiler didn't have any backend support for this, and
would instead decompose the switch into "if/else/goto" logic. You can
then use recursive binary subdivision to implement the list of case
labels (or linear probes if N is small).
BGBCC still uses binary subdivide if the case-labels don't satisfy the
constraints for a branch-table (Say, needs to be 16..256 case labels
with a density over 75%).
Yes, switch is something I can remove.
C2Y is adding range in case.
(https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3370.htm)
bart
2024-12-16 11:46:16 UTC
Reply
Permalink
Post by BGB
Post by Thiago Adams
Post by Lawrence D'Oliveiro
Post by Thiago Adams
I also think LLVM is too big.
How about QBE, then <https://c9x.me/compile/>.
QBE is just for linux and it does not generate code directly. C is
everywhere.
But I am reading QBE docs to see what is necessary in a IL.
I agree that both LLVM and GCC are too big.
I've just been working on a new backend which can be shared between
different projects. It can be built into a standalone library, which has
a size from 70KB to 180KB (x64 target), depending on output options.
(30K of that is my language's std library.)

The 70KB product interprets only. The full one can also generate
EXE/DLL/OBJ files, my private MX format, ASM source, IL source (when
code has been generated via its API), or it can run programs directly as
native code (when not interpreting the IL!).

It's several magnitudes smaller in scale than LLVM, and can be compiled
from source in 0.06 seconds:

c:\px>tm mm6 -opt pcl -dll
Compiling pcl.m to pcl.dll
TM: 0.06

c:\px>dir pcl.dll
16/12/2024 11:38 175,616 pcl.dll
Post by BGB
I am not sure the minimum size of a usable C compiler, my current
estimate is probably somewhere in the area of 50-100 kLOC.
My current compiler is a bit bigger than this, and my own past attempt
to implement a C compiler in under 30k lines had failed.
Mine, for quite a large subset (that can compile Lua and sqlite3 for
example), is between 180KB and 290KB. That's roughly 20 to 30Kloc,
assuming that one line of code maps to approx 10 bytes of x64 code.

The 18KB version is to interpret only; the 290K is 'fully loaded' (see
above). But usually my C standard headers are embedded, adding 40KB. (I
no longer embed windows.h; that's a massive 600KB.

But, people write all sorts of much smaller C compilers, obviously with
some compromises. The smallest I remember is one fitting into a 512-byte
boot-loader.
Post by BGB
  Basic UNARY and BINARY operators;
    Also COMPARE and similar;
  Load and Store Index operators;
    With support for an immediate index;
  GOTO
  IF-GOTO
  CALL (with a list of argument variables and a target variable).
The IL I mentioned above isn't that small. There are about 150
instructions. Probably half could be dispensed with (eg. 'MIN/MAX' ops,
or in-place MUL), but then the functionality needs to be provided by
other means. And as I showed, the resulting backend isn't that huge.

It uses a stack-model, and IL code looks ugly. I'd also developed a 3AC
IL that looked gorgeous, like a mini-HLL, but that was just too hard to
work with, even though such a model is similar to SSA which is very popular.
Lawrence D'Oliveiro
2024-12-16 19:44:59 UTC
Reply
Permalink
Post by BGB
I agree that both LLVM and GCC are too big.
And yet LLVM is used heavily in many specialist applications.
Keith Thompson
2024-12-16 21:59:33 UTC
Reply
Permalink
BGB <***@gmail.com> writes:
[...]
Post by BGB
Basic UNARY and BINARY operators;
Also COMPARE and similar;
Load and Store Index operators;
With support for an immediate index;
GOTO
IF-GOTO
CALL (with a list of argument variables and a target variable).
One doesn't need any higher-level control flow constructs.
[...]

Have you looked at C-- (cminusminus)?

https://en.wikipedia.org/wiki/C--
--
Keith Thompson (The_Other_Keith) Keith.S.Thompson+***@gmail.com
void Void(void) { Void(); } /* The recursive call of the void */
bart
2024-12-16 23:36:03 UTC
Reply
Permalink
Post by Keith Thompson
[...]
Post by BGB
Basic UNARY and BINARY operators;
Also COMPARE and similar;
Load and Store Index operators;
With support for an immediate index;
GOTO
IF-GOTO
CALL (with a list of argument variables and a target variable).
One doesn't need any higher-level control flow constructs.
[...]
Have you looked at C-- (cminusminus)?
https://en.wikipedia.org/wiki/C--
I looked at it perhaps 15 years ago. I got the impression it was dead.

According to your link (which might be broken, the "--" could be missing
when clicked), it was discontinued in 2013, and the only form of it
exists inside GHC, which uses various special backends specific to that
product:

"GHC backends are responsible for further transforming C-- into
executable code, via LLVM IR, slow C, or directly through the built-in
native backend"

I can't see that is available as a general purpose ready-to-use backend
for other languages, and for the targets that people might want (eg. x64
for Win64).

Or if there was, how practical it might be, in terms of compile-speed,
code quality, output options etc. If it has to be fed to LLVM, then you
might as well use LLVM directly.
Chris M. Thomasson
2024-12-15 04:39:39 UTC
Reply
Permalink
Post by Thiago Adams
I am working on a C backend that generates simple C code.
http://thradams.com/cake/playground.html
[...]

Wrt to C11, it is missing <stdatomic.h>:
________________________
#include <stdio.h>
#include <stdatomic.h>

struct ct_bar
{
int a;
int b;
atomic_int m_atomic;
};

struct ct_foo
{
char* a;
struct ct_bar bar;
};

int main()
{
struct ct_foo foo = { "Hello", { 1, 2, 0 } };

atomic_exchange(&foo.bar.m_atomic, 42);

printf("%s\n%d\n%d\n%d\n",
foo.a, foo.bar.a, foo.bar.b, foo.bar.m_atomic);

return 0;
}
________________________
Thiago Adams
2024-12-15 10:49:23 UTC
Reply
Permalink
Post by Chris M. Thomasson
Post by Thiago Adams
I am working on a C backend that generates simple C code.
http://thradams.com/cake/playground.html
[...]
________________________
#include <stdio.h>
#include <stdatomic.h>
struct ct_bar
{
    int a;
    int b;
    atomic_int m_atomic;
};
struct ct_foo
{
    char* a;
    struct ct_bar bar;
};
int main()
{
    struct ct_foo foo = { "Hello", { 1, 2, 0 } };
    atomic_exchange(&foo.bar.m_atomic, 42);
    printf("%s\n%d\n%d\n%d\n",
        foo.a, foo.bar.a, foo.bar.b, foo.bar.m_atomic);
    return 0;
}
________________________
Yes this conversion is not implemented yet.

Is
atomic_exchange(&foo.bar.m_atomic, 42);

The generated code for
foo.bar.m_atomic = 42;
?
I may look this at future.
Chris M. Thomasson
2024-12-15 21:01:52 UTC
Reply
Permalink
Post by Thiago Adams
Post by Chris M. Thomasson
Post by Thiago Adams
I am working on a C backend that generates simple C code.
http://thradams.com/cake/playground.html
[...]
________________________
#include <stdio.h>
#include <stdatomic.h>
struct ct_bar
{
     int a;
     int b;
     atomic_int m_atomic;
};
struct ct_foo
{
     char* a;
     struct ct_bar bar;
};
int main()
{
     struct ct_foo foo = { "Hello", { 1, 2, 0 } };
     atomic_exchange(&foo.bar.m_atomic, 42);
     printf("%s\n%d\n%d\n%d\n",
         foo.a, foo.bar.a, foo.bar.b, foo.bar.m_atomic);
     return 0;
}
________________________
Yes this conversion is not implemented yet.
Is
atomic_exchange(&foo.bar.m_atomic, 42);
The generated code for
foo.bar.m_atomic =  42;
?
Yes. atomic_exchange is an atomic RMW. Iirc, it defaults to seq_cst
memory_order. atomic_exchange_explicit allows us to define a different
memory_order.
Post by Thiago Adams
I may look this at future.
Cool. :^)
bart
2024-12-15 11:28:45 UTC
Reply
Permalink
Post by Thiago Adams
I am working on a C backend that generates simple C code.
http://thradams.com/cake/playground.html
Objective
- Shift all the complexity of the C compiler to the frontend.
  Why? This approach simplifies having one frontend and multiple backends.
- Use C as an intermediate language to feed a backend (any C compiler
can act as the backend).
  The backend can then focus solely on code generation.
- Code is not portable
Removed C89 Features
- Preprocessor
- sizeof
If I try:

printf("%zu\n", sizeof(void*));

it turns into:

printf("%zu\n", 4U);

Presumably this translator will only target 32-bit systems even if run
on a 64-bit one?

(My own C transpiler goes the other way and generates 'C64', requiring a
64-bit compiler. Earlier versions made it optional, but the output was
then either C32 or C64; if someone else compiled it, they'd have to
choose the right compiler option.)

I don't know if that "%zu" format will be an issue.
Post by Thiago Adams
- typedef
- enum
- Constant expressions (the final result is precomputed during earlier
stages)
- const
- I may also remove switch.
You can do that. But it can also slow down certain programs. (TCC 0.9.26
generated seqential tests for switch, but on one benchmark that relied
on it heavily, its code was slower than my dynamic interpreter.)

If you think this might be too hard to compile later, you can choose to
do the same. (As for parsing switch, you'd need to do that anyway,
either in the transpiler, or the backend compiler.)
Thiago Adams
2024-12-15 11:46:05 UTC
Reply
Permalink
Post by Thiago Adams
I am working on a C backend that generates simple C code.
http://thradams.com/cake/playground.html
Objective
- Shift all the complexity of the C compiler to the frontend.
   Why? This approach simplifies having one frontend and multiple
backends.
- Use C as an intermediate language to feed a backend (any C compiler
can act as the backend).
   The backend can then focus solely on code generation.
- Code is not portable
Removed C89 Features
- Preprocessor
- sizeof
    printf("%zu\n", sizeof(void*));
    printf("%zu\n", 4U);
Presumably this translator will only target 32-bit systems even if run
on a 64-bit one?
It will have to be configured to x86 or x64. It has to match the
target/platform compiler settings. The online versions is emulating
something. I think x86 gcc on linux.
(My own C transpiler goes the other way and generates 'C64', requiring a
64-bit compiler. Earlier versions made it optional, but the output was
then either C32 or C64; if someone else compiled it, they'd have to
choose the right compiler option.)
I don't know if that "%zu" format will be an issue.
It has to generate code as the target compiler wants to see. I am
removing sizeof for instance, then sizeof computed must match the target
compiler.
Post by Thiago Adams
- typedef
- enum
- Constant expressions (the final result is precomputed during earlier
stages)
- const
- I may also remove switch.
You can do that. But it can also slow down certain programs. (TCC 0.9.26
generated seqential tests for switch, but on one benchmark that relied
on it heavily, its code was slower than my dynamic interpreter.)
I want to move this to the front end.
If you think this might be too hard to compile later, you can choose to
do the same. (As for parsing switch, you'd need to do that anyway,
either in the transpiler, or the backend compiler.)
Initialization also needs a strategy. I want to move these to front end.
The problem is if that strategy cannot be implemented in C89.
Thiago Adams
2024-12-15 12:13:41 UTC
Reply
Permalink
Post by Thiago Adams
I am working on a C backend that generates simple C code.
http://thradams.com/cake/playground.html
Objective
- Shift all the complexity of the C compiler to the frontend.
   Why? This approach simplifies having one frontend and multiple
backends.
- Use C as an intermediate language to feed a backend (any C compiler
can act as the backend).
   The backend can then focus solely on code generation.
- Code is not portable
Removed C89 Features
- Preprocessor
- sizeof
     printf("%zu\n", sizeof(void*));
     printf("%zu\n", 4U);
Presumably this translator will only target 32-bit systems even if run
on a 64-bit one?
It will have to be configured to x86 or x64. It has to match the target/
platform compiler settings. The online versions is emulating something.
I think x86 gcc on linux.
Th
Obs:
The platform windows\linux makes difference in the size of wchar_t L'a'
also on sizes of long etc.. In practice the result is assuming fixed
sizes. I may add at the end of generated code the assumptions . It can
be in a static_assert form.
Bonita Montero
2024-12-15 19:08:53 UTC
Reply
Permalink
C++ is more readable because is is magnitudes more expressive than C.
You can easily write a C++-statement that would hunddres of lines in
C (imagines specializing a unordered_map by hand). Making a language
less expressive makes it even less readable, and that's also true for
your reduced C.
bart
2024-12-15 21:32:11 UTC
Reply
Permalink
Post by Bonita Montero
C++ is more readable because is is magnitudes more expressive than C.
You can easily write a C++-statement that would hunddres of lines in
C (imagines specializing a unordered_map by hand). Making a language
less expressive makes it even less readable, and that's also true for
your reduced C.
That's not really the point of it. This reduced C is used as an
intermediate language for a compiler target. It will not usually be
read, or maintained.

An intermediate language needs to at a lower level than the source language.

And for this project, it needs to be compilable by any C89 compiler.

Generating C++ would be quite useless.
BGB
2024-12-15 23:53:30 UTC
Reply
Permalink
Post by bart
Post by Bonita Montero
C++ is more readable because is is magnitudes more expressive than C.
You can easily write a C++-statement that would hunddres of lines in
C (imagines specializing a unordered_map by hand). Making a language
less expressive makes it even less readable, and that's also true for
your reduced C.
That's not really the point of it. This reduced C is used as an
intermediate language for a compiler target. It will not usually be
read, or maintained.
An intermediate language needs to at a lower level than the source language.
And for this project, it needs to be compilable by any C89 compiler.
Generating C++ would be quite useless.
As an IL, even C is a little overkill, unless turned into a restricted
subset (say, along similar lines to GCC's GIMPLE).

Say:
Only function-scope variables allowed;
No high-level control structures;
...

Say:
int foo(int x)
{
int i, v;
for(i=x, v=0; i>0; i--)
v=v*i;
return(v);
}

Becoming, say:
int foo(int x)
{
int i;
int v;
i=x;
v=0;
if(i<=0)goto L1;
L0:
v=v*i;
i=i-1;
if(i>0)goto L0;
L1:
return v;
}

...


Though, this still requires the backend to have a full parser.



Would still be simpler still for a backend to use a plain binary
serialization, and/or maybe a syntax more like BASIC or similar.

Say:
SUB foo ( x as Int ) as Int
DIM i as Int
DIM v as Int
i=x
v=0
IF i <= 0 THEN GOTO L1
L0:
v = v * i
i = i - 1
IF i > 0 THEN GOTO L0
L1:
RETURN v
END SUB

Where one can have a parser that reads one line at a time, breaks it
into tokens, and does simple pattern matching.

Pretty much all higher level control flow can be expressed via goto.

Variables within sub-blocks can be promoted to function scope, possibly
renamed:
int i;
if() {
long i;
...
}
Remaps:
int i$0;
long i$1;

"switch()" can be decomposed:
switch(i)
{
case 1:
A;
break;
case 2:
B;
case 3:
C;
}
To:
if(i==1)goto CL1;
if(i==2)goto CL2;
if(i==3)goto CL3;
goto CDFL;
CL1:
A;
goto CEND;
CL2:
B;
CL3:
C;
CDFL:
CEND:

...
David Brown
2024-12-16 09:36:04 UTC
Reply
Permalink
Post by BGB
Post by bart
Post by Bonita Montero
C++ is more readable because is is magnitudes more expressive than C.
You can easily write a C++-statement that would hunddres of lines in
C (imagines specializing a unordered_map by hand). Making a language
less expressive makes it even less readable, and that's also true for
your reduced C.
That's not really the point of it. This reduced C is used as an
intermediate language for a compiler target. It will not usually be
read, or maintained.
An intermediate language needs to at a lower level than the source language.
And for this project, it needs to be compilable by any C89 compiler.
Generating C++ would be quite useless.
As an IL, even C is a little overkill, unless turned into a restricted
subset (say, along similar lines to GCC's GIMPLE).
Have either you or Thiago looked at C-- as an alternative? I don't know
how practical it would be.

<https://en.wikipedia.org/wiki/C-->
Thiago Adams
2024-12-16 11:21:07 UTC
Reply
Permalink
Post by BGB
Post by bart
Post by Bonita Montero
C++ is more readable because is is magnitudes more expressive than C.
You can easily write a C++-statement that would hunddres of lines in
C (imagines specializing a unordered_map by hand). Making a language
less expressive makes it even less readable, and that's also true for
your reduced C.
That's not really the point of it. This reduced C is used as an
intermediate language for a compiler target. It will not usually be
read, or maintained.
An intermediate language needs to at a lower level than the source language.
And for this project, it needs to be compilable by any C89 compiler.
Generating C++ would be quite useless.
As an IL, even C is a little overkill, unless turned into a restricted
subset (say, along similar lines to GCC's GIMPLE).
  Only function-scope variables allowed;
  No high-level control structures;
  ...
  int foo(int x)
  {
    int i, v;
    for(i=x, v=0; i>0; i--)
      v=v*i;
    return(v);
  }
  int foo(int x)
  {
    int i;
    int v;
    i=x;
    v=0;
    if(i<=0)goto L1;
    v=v*i;
    i=i-1;
    if(i>0)goto L0;
    return v;
  }
...
I have considered to remove loops and keep only goto.
But I think this is not bring too much simplification.
BGB
2024-12-17 07:03:20 UTC
Reply
Permalink
Post by Thiago Adams
Post by BGB
Post by bart
Post by Bonita Montero
C++ is more readable because is is magnitudes more expressive than C.
You can easily write a C++-statement that would hunddres of lines in
C (imagines specializing a unordered_map by hand). Making a language
less expressive makes it even less readable, and that's also true for
your reduced C.
That's not really the point of it. This reduced C is used as an
intermediate language for a compiler target. It will not usually be
read, or maintained.
An intermediate language needs to at a lower level than the source language.
And for this project, it needs to be compilable by any C89 compiler.
Generating C++ would be quite useless.
As an IL, even C is a little overkill, unless turned into a restricted
subset (say, along similar lines to GCC's GIMPLE).
   Only function-scope variables allowed;
   No high-level control structures;
   ...
   int foo(int x)
   {
     int i, v;
     for(i=x, v=0; i>0; i--)
       v=v*i;
     return(v);
   }
   int foo(int x)
   {
     int i;
     int v;
     i=x;
     v=0;
     if(i<=0)goto L1;
     v=v*i;
     i=i-1;
     if(i>0)goto L0;
     return v;
   }
...
I have considered to remove loops and keep only goto.
But I think this is not bring too much simplification.
It depends.

If the compiler works like an actual C compiler, with a full parser and
AST stage, yeah, it may not save much.


If the parser is a thin wrapper over 3AC operations (only allowing
statements that map 1:1 with a 3AC IR operation), it may save a bit more...



As for whether or not it makes sense to use a C like syntax here, this
is more up for debate (for practical use within a compiler, I would
assume a binary serialization rather than an ASCII syntax, though ASCII
may be better in terms of inter-operation or human readability).


But, as can be noted, I would assume a binary serialization that is
oriented around operators; and *not* about serializing the structures
used to implement those operators. Also I would assume that the IR need
not be in SSA form (conversion to full SSA could be done when reading in
the IR operations).


Ny argument is that not using SSA form means fewer issues for both the
serialization format and compiler front-end to need to deal with (and is
comparably easy to regenerate for the backend, with the backend
operating with its internal IR in SSA form).

Well, contrast to LLVM assuming everything is always in SSA form.

...
Thiago Adams
2024-12-17 17:55:34 UTC
Reply
Permalink
Post by BGB
Post by Thiago Adams
Post by BGB
Post by bart
Post by Bonita Montero
C++ is more readable because is is magnitudes more expressive than C.
You can easily write a C++-statement that would hunddres of lines in
C (imagines specializing a unordered_map by hand). Making a language
less expressive makes it even less readable, and that's also true for
your reduced C.
That's not really the point of it. This reduced C is used as an
intermediate language for a compiler target. It will not usually be
read, or maintained.
An intermediate language needs to at a lower level than the source language.
And for this project, it needs to be compilable by any C89 compiler.
Generating C++ would be quite useless.
As an IL, even C is a little overkill, unless turned into a
restricted subset (say, along similar lines to GCC's GIMPLE).
   Only function-scope variables allowed;
   No high-level control structures;
   ...
   int foo(int x)
   {
     int i, v;
     for(i=x, v=0; i>0; i--)
       v=v*i;
     return(v);
   }
   int foo(int x)
   {
     int i;
     int v;
     i=x;
     v=0;
     if(i<=0)goto L1;
     v=v*i;
     i=i-1;
     if(i>0)goto L0;
     return v;
   }
...
I have considered to remove loops and keep only goto.
But I think this is not bring too much simplification.
It depends.
If the compiler works like an actual C compiler, with a full parser and
AST stage, yeah, it may not save much.
If the parser is a thin wrapper over 3AC operations (only allowing
statements that map 1:1 with a 3AC IR operation), it may save a bit more...
As for whether or not it makes sense to use a C like syntax here, this
is more up for debate (for practical use within a compiler, I would
assume a binary serialization rather than an ASCII syntax, though ASCII
may be better in terms of inter-operation or human readability).
But, as can be noted, I would assume a binary serialization that is
oriented around operators; and *not* about serializing the structures
used to implement those operators. Also I would assume that the IR need
not be in SSA form (conversion to full SSA could be done when reading in
the IR operations).
Ny argument is that not using SSA form means fewer issues for both the
serialization format and compiler front-end to need to deal with (and is
comparably easy to regenerate for the backend, with the backend
operating with its internal IR in SSA form).
Well, contrast to LLVM assuming everything is always in SSA form.
...
I also have considered split expressions.

For instance

if (a*b+c) {}

into

register int r1 = a * b;
register int r2 = r1 + c;
if (r2) {}

This would make easier to add overflow checks in runtime (if desired)
and implement things like _complex

Is this what you mean by 3AC or SSA?

This would definitely simplify expressions grammar.
Thiago Adams
2024-12-17 17:59:49 UTC
Reply
Permalink
Post by Thiago Adams
Post by BGB
Post by Thiago Adams
Post by BGB
Post by bart
Post by Bonita Montero
C++ is more readable because is is magnitudes more expressive than C.
You can easily write a C++-statement that would hunddres of lines in
C (imagines specializing a unordered_map by hand). Making a language
less expressive makes it even less readable, and that's also true for
your reduced C.
That's not really the point of it. This reduced C is used as an
intermediate language for a compiler target. It will not usually be
read, or maintained.
An intermediate language needs to at a lower level than the source language.
And for this project, it needs to be compilable by any C89 compiler.
Generating C++ would be quite useless.
As an IL, even C is a little overkill, unless turned into a
restricted subset (say, along similar lines to GCC's GIMPLE).
   Only function-scope variables allowed;
   No high-level control structures;
   ...
   int foo(int x)
   {
     int i, v;
     for(i=x, v=0; i>0; i--)
       v=v*i;
     return(v);
   }
   int foo(int x)
   {
     int i;
     int v;
     i=x;
     v=0;
     if(i<=0)goto L1;
     v=v*i;
     i=i-1;
     if(i>0)goto L0;
     return v;
   }
...
I have considered to remove loops and keep only goto.
But I think this is not bring too much simplification.
It depends.
If the compiler works like an actual C compiler, with a full parser
and AST stage, yeah, it may not save much.
If the parser is a thin wrapper over 3AC operations (only allowing
statements that map 1:1 with a 3AC IR operation), it may save a bit more...
As for whether or not it makes sense to use a C like syntax here, this
is more up for debate (for practical use within a compiler, I would
assume a binary serialization rather than an ASCII syntax, though
ASCII may be better in terms of inter-operation or human readability).
But, as can be noted, I would assume a binary serialization that is
oriented around operators; and *not* about serializing the structures
used to implement those operators. Also I would assume that the IR
need not be in SSA form (conversion to full SSA could be done when
reading in the IR operations).
Ny argument is that not using SSA form means fewer issues for both the
serialization format and compiler front-end to need to deal with (and
is comparably easy to regenerate for the backend, with the backend
operating with its internal IR in SSA form).
Well, contrast to LLVM assuming everything is always in SSA form.
...
I also have considered split expressions.
For instance
if (a*b+c) {}
into
register int r1 = a * b;
register int r2 = r1 + c;
if (r2) {}
This would make easier to add overflow checks in runtime (if desired)
and implement things like _complex
Is this what you mean by 3AC or SSA?
This would definitely simplify expressions grammar.
I also have consider remove local scopes. But I think local scopes may
be useful to better use stack reusing the same addresses when variables
goes out of scope.
For instance

{
int i =1;
{
int a = 2;
}
{
int b = 3;
}
}
I think scope makes easier to use the same stack position of a and b
because it is easier to see a does not exist any more.
Thiago Adams
2024-12-17 18:16:48 UTC
Reply
Permalink
Post by Thiago Adams
Post by Thiago Adams
Post by BGB
Post by Thiago Adams
Post by BGB
Post by bart
Post by Bonita Montero
C++ is more readable because is is magnitudes more expressive than C.
You can easily write a C++-statement that would hunddres of lines in
C (imagines specializing a unordered_map by hand). Making a language
less expressive makes it even less readable, and that's also true for
your reduced C.
That's not really the point of it. This reduced C is used as an
intermediate language for a compiler target. It will not usually
be read, or maintained.
An intermediate language needs to at a lower level than the source language.
And for this project, it needs to be compilable by any C89 compiler.
Generating C++ would be quite useless.
As an IL, even C is a little overkill, unless turned into a
restricted subset (say, along similar lines to GCC's GIMPLE).
   Only function-scope variables allowed;
   No high-level control structures;
   ...
   int foo(int x)
   {
     int i, v;
     for(i=x, v=0; i>0; i--)
       v=v*i;
     return(v);
   }
   int foo(int x)
   {
     int i;
     int v;
     i=x;
     v=0;
     if(i<=0)goto L1;
     v=v*i;
     i=i-1;
     if(i>0)goto L0;
     return v;
   }
...
I have considered to remove loops and keep only goto.
But I think this is not bring too much simplification.
It depends.
If the compiler works like an actual C compiler, with a full parser
and AST stage, yeah, it may not save much.
If the parser is a thin wrapper over 3AC operations (only allowing
statements that map 1:1 with a 3AC IR operation), it may save a bit more...
As for whether or not it makes sense to use a C like syntax here,
this is more up for debate (for practical use within a compiler, I
would assume a binary serialization rather than an ASCII syntax,
though ASCII may be better in terms of inter-operation or human
readability).
But, as can be noted, I would assume a binary serialization that is
oriented around operators; and *not* about serializing the structures
used to implement those operators. Also I would assume that the IR
need not be in SSA form (conversion to full SSA could be done when
reading in the IR operations).
Ny argument is that not using SSA form means fewer issues for both
the serialization format and compiler front-end to need to deal with
(and is comparably easy to regenerate for the backend, with the
backend operating with its internal IR in SSA form).
Well, contrast to LLVM assuming everything is always in SSA form.
...
I also have considered split expressions.
For instance
if (a*b+c) {}
into
register int r1 = a * b;
register int r2 = r1 + c;
if (r2) {}
This would make easier to add overflow checks in runtime (if desired)
and implement things like _complex
Is this what you mean by 3AC or SSA?
This would definitely simplify expressions grammar.
I also have consider remove local scopes. But I think local scopes may
be useful to better use stack reusing the same addresses when variables
goes out of scope.
For instance
{
 int i =1;
 {
  int a  = 2;
 }
 {
  int b  = 3;
 }
}
I think scope makes easier to use the same stack position of a and b
because it is easier to see a does not exist any more.
also remove structs changing by unsigned char [] and cast parts of it to
access members.

I think this the lower level possible in c.
bart
2024-12-17 18:37:47 UTC
Reply
Permalink
Post by Thiago Adams
also remove structs changing by unsigned char [] and cast parts of it to
access members.
I think this the lower level possible in c.
This is what I do in my IL, where structs are just fixed blocks of so
many bytes.

But there are some things to consider:

* A struct may still need alignment corresponding to the strictest
alignment among the members. (Any padding between members and at the end
should already be taken care of.)

I use an alignment based on overall size, so a 40-byte struct is assumed
to have an 64-bit max alignment, but it may only need 16-bit alignment.
That is harmless, but it can be fixed with some extra metadata.

With a C char[], you can choose to use a short[] array for example
(obviously of half the length) to signal that it needs 16-bit alignment.


* Some machine ABIs, like SYS V for 64 bits, may need to know the
internal layout of structs when they are passed 'by value'.

If reduced down to char[], this info will be missing.

I ignore this because I only target Win64 ABI. It only comes up in SYS
V, when calling functions across an FFI, and when the API uses value
structs, which is uncommon. And also makes I can't make head or tail of
the rules.
Thiago Adams
2024-12-17 19:07:45 UTC
Reply
Permalink
Post by bart
Post by Thiago Adams
also remove structs changing by unsigned char [] and cast parts of it
to access members.
I think this the lower level possible in c.
This is what I do in my IL, where structs are just fixed blocks of so
many bytes.
How do you do with struct parameters?
bart
2024-12-17 19:42:51 UTC
Reply
Permalink
Post by Thiago Adams
Post by bart
Post by Thiago Adams
also remove structs changing by unsigned char [] and cast parts of it
to access members.
I think this the lower level possible in c.
This is what I do in my IL, where structs are just fixed blocks of so
many bytes.
How do you do with struct parameters?
In the IL they are always passed notionally by value. This side of the
IL (that is, the frontend compile that generates IL), knows nothing
about the target, such as ABI details.

(In practice, some things are known, like the word size of the target,
since that can change characteristics of the source language, like the
size of 'int' or of 'void*'. It also needs to assume, or request from
the backend, argument evaluation order, although my IL can reverse order
if necessary.)

It is the backend, on the other size of the IL, that needs to deal with
those details.

That can include making copies of structs that the ABI says are passed
by value. But when targeting SYS V ABI (which I haven't attempted yet),
it may need to know the internal layout of a struct.

You can however do experiments with using SYS V on Linux (must be 64 bits):

* Create test structs with, say, int32 or int64 elements

* Write a test function where such a struct is passed by value, and
then return a modified copy

* Rerun the test using a version of the function where a char[] version
of the struct is passed and returned, and which contains the member
access casts you suggested

* See if it gives the same results.

You might need a union of the two structs, or use memcpy to transfer
contents, before and after calling the test function.
BGB
2024-12-18 18:51:04 UTC
Reply
Permalink
Post by bart
Post by Thiago Adams
Post by bart
Post by Thiago Adams
also remove structs changing by unsigned char [] and cast parts of
it to access members.
I think this the lower level possible in c.
This is what I do in my IL, where structs are just fixed blocks of so
many bytes.
How do you do with struct parameters?
In the IL they are always passed notionally by value. This side of the
IL (that is, the frontend compile that generates IL), knows nothing
about the target, such as ABI details.
(In practice, some things are known, like the word size of the target,
since that can change characteristics of the source language, like the
size of 'int' or of 'void*'. It also needs to assume, or request from
the backend, argument evaluation order, although my IL can reverse order
if necessary.)
It is the backend, on the other size of the IL, that needs to deal with
those details.
That can include making copies of structs that the ABI says are passed
by value. But when targeting SYS V ABI (which I haven't attempted yet),
it may need to know the internal layout of a struct.
* Create test structs with, say, int32 or int64 elements
* Write a test function where such a struct is passed by value, and
  then return a modified copy
* Rerun the test using a version of the function where a char[] version
of the struct is passed and returned, and which contains the member
access casts you suggested
* See if it gives the same results.
You might need a union of the two structs, or use memcpy to transfer
contents, before and after calling the test function.
I took a different approach:
In the backend IR stage, structs are essentially treated as references
to the structure.

A local structure may be "initialized" via an IR operation, in which
point it will be assigned storage in the stack frame, and the reference
will be initialized to the storage area for the structure.

Most operations will pass them by reference.

Assigning a struct will essentially be turned into a struct-copy
operation (using the same mechanism as inline memcpy).


Type model could be seen as multiple levels:
I: integer types of 'int' and smaller;
L: integer types of 64 bits or less that are not I.
D: 'double' and smaller floating-point types.
A: Address (pointers, arrays, structs, ...)
X: 128-bit types.
int128, 'long double', SIMD vectors, ...

I:
char, signed char, unsigned char
short, unsigned short
int, unsigned int
_Bool, wchar_t, ...
L:
long, long long, unsigned long, unsigned long long
64-bit SIMD vectors
variant (sorta)
D: double, float, short float
A:
pointers
arrays
structs
class instances
...
X:
grab bag of pretty much everything that is 128 bits.

The toplevel types all basically have similar storage and behavior, so
in many cases one can rely on this rather than the actual type.



...
Thiago Adams
2024-12-18 19:43:59 UTC
Reply
Permalink
Post by BGB
In the backend IR stage, structs are essentially treated as references
to the structure.
A local structure may be "initialized" via an IR operation, in which
point it will be assigned storage in the stack frame, and the reference
will be initialized to the storage area for the structure.
Most operations will pass them by reference.
Assigning a struct will essentially be turned into a struct-copy
operation (using the same mechanism as inline memcpy).
But what happens with calling a external C function that has a struct X
as parameter? (not pointer to struct)
BGB
2024-12-19 00:27:26 UTC
Reply
Permalink
Post by Thiago Adams
Post by BGB
In the backend IR stage, structs are essentially treated as references
to the structure.
A local structure may be "initialized" via an IR operation, in which
point it will be assigned storage in the stack frame, and the
reference will be initialized to the storage area for the structure.
Most operations will pass them by reference.
Assigning a struct will essentially be turned into a struct-copy
operation (using the same mechanism as inline memcpy).
But what happens with calling a external C function that has a struct X
as parameter? (not pointer to struct)
In my ABI, if larger than 16 bytes, it is passed by reference (as a
pointer in a register or on the stack), callee is responsible for
copying it somewhere else if needed.

For struct return, a pointer to return the struct into is provided by
the caller, and the callee copies the returned struct into this address.

If the caller ignores the return value, the caller provides a dummy
buffer for the return value.

If no prototype is provided... well, most likely the program crashes or
similar.

So, in effect, the by-value semantics are mostly faked by the compiler.


It is roughly similar to the handling of C array types, which in this
case are also seen as a combination of a hidden pointer to the data, and
the backing data (the array's contents). The code-generator mostly
operates in terms of this hidden pointer.


By-Value Structs smaller than 16 bytes are passed as-if they were a 64
or 128 bit integer type (as a single register or as a register pair,
with a layout matching their in-memory representation).

...


But, yeah, at the IL level, one could potentially eliminate structs and
arrays as a separate construct, and instead have bare pointers and a
generic "reserve a blob of bytes in the frame and initialize this
pointer to point to it" operator (with the business end of this operator
happening in the function prolog).

...
bart
2024-12-19 00:35:41 UTC
Reply
Permalink
Post by BGB
Post by Thiago Adams
Post by BGB
In the backend IR stage, structs are essentially treated as
references to the structure.
A local structure may be "initialized" via an IR operation, in which
point it will be assigned storage in the stack frame, and the
reference will be initialized to the storage area for the structure.
Most operations will pass them by reference.
Assigning a struct will essentially be turned into a struct-copy
operation (using the same mechanism as inline memcpy).
But what happens with calling a external C function that has a struct
X as parameter? (not pointer to struct)
In my ABI, if larger than 16 bytes, it is passed by reference (as a
pointer in a register or on the stack), callee is responsible for
copying it somewhere else if needed.
For struct return, a pointer to return the struct into is provided by
the caller, and the callee copies the returned struct into this address.
If the caller ignores the return value, the caller provides a dummy
buffer for the return value.
If no prototype is provided... well, most likely the program crashes or
similar.
So, in effect, the by-value semantics are mostly faked by the compiler.
It is roughly similar to the handling of C array types, which in this
case are also seen as a combination of a hidden pointer to the data, and
the backing data (the array's contents). The code-generator mostly
operates in terms of this hidden pointer.
By-Value Structs smaller than 16 bytes are passed as-if they were a 64
or 128 bit integer type (as a single register or as a register pair,
with a layout matching their in-memory representation).
...
But, yeah, at the IL level, one could potentially eliminate structs and
arrays as a separate construct, and instead have bare pointers and a
generic "reserve a blob of bytes in the frame and initialize this
pointer to point to it" operator (with the business end of this operator
happening in the function prolog).
The problem with this, that I mentioned elsewhere, is how well it would
work with SYS V ABI, since the rules for structs are complex, and
apparently recursive.

Having just a block of bytes might not be enough.
BGB
2024-12-19 05:46:21 UTC
Reply
Permalink
Post by bart
Post by BGB
Post by Thiago Adams
Post by BGB
In the backend IR stage, structs are essentially treated as
references to the structure.
A local structure may be "initialized" via an IR operation, in which
point it will be assigned storage in the stack frame, and the
reference will be initialized to the storage area for the structure.
Most operations will pass them by reference.
Assigning a struct will essentially be turned into a struct-copy
operation (using the same mechanism as inline memcpy).
But what happens with calling a external C function that has a struct
X as parameter? (not pointer to struct)
In my ABI, if larger than 16 bytes, it is passed by reference (as a
pointer in a register or on the stack), callee is responsible for
copying it somewhere else if needed.
For struct return, a pointer to return the struct into is provided by
the caller, and the callee copies the returned struct into this address.
If the caller ignores the return value, the caller provides a dummy
buffer for the return value.
If no prototype is provided... well, most likely the program crashes
or similar.
So, in effect, the by-value semantics are mostly faked by the compiler.
It is roughly similar to the handling of C array types, which in this
case are also seen as a combination of a hidden pointer to the data,
and the backing data (the array's contents). The code-generator mostly
operates in terms of this hidden pointer.
By-Value Structs smaller than 16 bytes are passed as-if they were a 64
or 128 bit integer type (as a single register or as a register pair,
with a layout matching their in-memory representation).
...
But, yeah, at the IL level, one could potentially eliminate structs
and arrays as a separate construct, and instead have bare pointers and
a generic "reserve a blob of bytes in the frame and initialize this
pointer to point to it" operator (with the business end of this
operator happening in the function prolog).
The problem with this, that I mentioned elsewhere, is how well it would
work with SYS V ABI, since the rules for structs are complex, and
apparently recursive.
Having just a block of bytes might not be enough.
In my case, I am not bothering with the SysV style ABI's (well, along
with there not being any x86 or x86-64 target...).


For my ISA, it is a custom ABI, but follows mostly similar rules to some
of the other "Microsoft style" ABIs (where, I have noted that across
multiple targets, MS tools have tended to use similar ABI designs).

For my compiler targeting RISC-V, it uses a variation of RV's ABI rules.
Argument passing is basically similar, but struct pass/return is
different; and it passes floating-point values in GPRs (and, in my own
ISA, all floating-point values use GPRs, as there are no FPU registers;
though FPU registers do exist for RISC-V).

Not likely a huge issue as one is unlikely to use ELF and PE/COFF in the
same program.


For the "OS" that runs on my CPU core, it is natively using PE/COFF, but
ELF is supported for RISC-V (currently PIE only). It generally needs to
use my own C library as I still haven't gotten glibc or musl libc to
work on it (and they work in a different way from my own C library).

Seemingly, something is going terribly wrong in the "dynamic linking"
process, but too hard to figure out in the absence of any real debugging
interface (what debug mechanisms I have, effectively lack any symbols
for things inside "ld-linux.so"'s domain).

Theoretically, could make porting usermode software easier, as then I
could compile stuff as-if it were running on an RV64 port of Linux.

But, easier said than done.

...
bart
2024-12-19 11:27:03 UTC
Reply
Permalink
Post by BGB
Post by bart
Post by BGB
By-Value Structs smaller than 16 bytes are passed as-if they were a
64 or 128 bit integer type (as a single register or as a register
pair, with a layout matching their in-memory representation).
...
But, yeah, at the IL level, one could potentially eliminate structs
and arrays as a separate construct, and instead have bare pointers
and a generic "reserve a blob of bytes in the frame and initialize
this pointer to point to it" operator (with the business end of this
operator happening in the function prolog).
The problem with this, that I mentioned elsewhere, is how well it
would work with SYS V ABI, since the rules for structs are complex,
and apparently recursive.
Having just a block of bytes might not be enough.
In my case, I am not bothering with the SysV style ABI's (well, along
with there not being any x86 or x86-64 target...).
I'd imagine it's worse with ARM targets as there are so many more
registers to try and deconstruct structs into.
Post by BGB
For my ISA, it is a custom ABI, but follows mostly similar rules to some
of the other "Microsoft style" ABIs (where, I have noted that across
multiple targets, MS tools have tended to use similar ABI designs).
When you do your own thing, it's easy.

In the 1980s, I didn't need to worry about call conventions used for
other software, since there /was/ no other software! I had to write
everything, save for the odd calls to DOS which used some form of SYSCALL.

Then, arrays and structs were actually passed and returned by value (not
via hidden references), by copying the data to and from the stack.

However, I don't recall ever using the feature, as I considered it
efficient. I always used explicit references in my code.
Post by BGB
For my compiler targeting RISC-V, it uses a variation of RV's ABI rules.
Argument passing is basically similar, but struct pass/return is
different; and it passes floating-point values in GPRs (and, in my own
ISA, all floating-point values use GPRs, as there are no FPU registers;
though FPU registers do exist for RISC-V).
Supporting C's variadic functions, which is needed for many languages
when calling C across an FFI, usually requires different rules. On Win64
ABI for example, by passing low variadic arguments in both GPRs and FPU
registers.

/Implementing/ variadic functions (which only occurs if implementing C)
is another headache if it has to work with the ABI (which can be assumed
for a non-static function).

I barely have a working solution for Win64 ABI, which needs to be done
via stdarg.h, but wouldn't have a clue how to do it for SYS V.

(Even Win64 has problems, as it assumes a downward-growing stack; in my
IL interpreter, the stack grows upwards!)
Post by BGB
Not likely a huge issue as one is unlikely to use ELF and PE/COFF in the
same program.
For the "OS" that runs on my CPU core, it is natively using PE/COFF, but
That's interesting: you deliberately used one of the most complex file
formats around, when you could have devised your own?

I did exactly that at a period when my generated DLLs were buggy for
some reason (it turned out to be two reasons). I created a simple
dynamic library format of my own. Then I found the same format worked
also for executables.

But I needed a loader program to run them, as Windows obviously didn't
understand the format. Such a program can be written in 800 lines of C,
and can dynamically libraries in both my format, and proper DLLs (not
the buggy ones I generated!).

A hello-world program is under 300 bytes compared with 2 or
2.5KB of EXE. And the format is portable to Linux, so no need to
generate ELF (but I haven't tried). Plus the format might be transparent
to AV software (haven't tried that either).
BGB
2024-12-19 20:36:37 UTC
Reply
Permalink
Post by bart
Post by BGB
Post by bart
Post by BGB
By-Value Structs smaller than 16 bytes are passed as-if they were a
64 or 128 bit integer type (as a single register or as a register
pair, with a layout matching their in-memory representation).
...
But, yeah, at the IL level, one could potentially eliminate structs
and arrays as a separate construct, and instead have bare pointers
and a generic "reserve a blob of bytes in the frame and initialize
this pointer to point to it" operator (with the business end of this
operator happening in the function prolog).
The problem with this, that I mentioned elsewhere, is how well it
would work with SYS V ABI, since the rules for structs are complex,
and apparently recursive.
Having just a block of bytes might not be enough.
In my case, I am not bothering with the SysV style ABI's (well, along
with there not being any x86 or x86-64 target...).
I'd imagine it's worse with ARM targets as there are so many more
registers to try and deconstruct structs into.
Not messed much with the ARM64 ABI or similar, but I will draw the line
in the sand somewhere.

Struct passing/return is enough of an edge case that one can just sort
of declare it "no go" between compilers with "mostly but not strictly
compatible" ABIs.
Post by bart
Post by BGB
For my ISA, it is a custom ABI, but follows mostly similar rules to
some of the other "Microsoft style" ABIs (where, I have noted that
across multiple targets, MS tools have tended to use similar ABI
designs).
When you do your own thing, it's easy.
In the 1980s, I didn't need to worry about call conventions used for
other software, since there /was/ no other software! I had to write
everything, save for the odd calls to DOS which used some form of SYSCALL.
Then, arrays and structs were actually passed and returned by value (not
via hidden references), by copying the data to and from the stack.
However, I don't recall ever using the feature, as I considered it
efficient. I always used explicit references in my code.
Most of the time, one is passing/returning structures as pointers, and
not by value.

By value structures are usually small.


When a structure is not small, it is both simpler to implement, and
usually faster, to internally pass it by reference.

If you pass a large structure to a function by value, via an on-stack
copy, and the function assigns it to another location (say, a global
variable):
Pass by reference: Only a single copy operation is needed;
Pass by value on-stack: At least two copy operations are needed.

One also needs to reserve enough space in the function arguments list to
hold any structures passed, which could be bad if they are potentially
large.



But, on my ISA, ABI is sort of like:
R4 ..R7 : Arg0 ..Arg3
R20..R23: Arg4 ..Arg7
R36..R39: Arg8 ..Arg11 (optional)
R52..R55: Arg12..Arg15 (optional)
Return Value:
R2, R3:R2 (128 bit)
R2 is also used to pass in the return value pointer.

'this':
Generally passed in either R3 or R18, depending on ABI variant.

Where, callee-save:
R8 ..R14, R24..R31,
R40..R47, R56..R63
R15=SP

Non-saved scratch:
R2 ..R7 , R16..R23,
R32..R39, R48..R55


Arguments beyond the first 8/16 register arguments are passed on stack.
In this case, a spill space for the first 8/16 arguments (64 or 128
bytes) is provided on stack before the first non-register argument.

If the function accepts a fixed number of arguments and the number of
argument registers is 8 or less, spill space need only be provided for
the first 8 arguments (calling vararg functions will always reserve
space for 16 registers in the 16-register ABI). This spill space
effectively belongs to the callee rather than the caller.


Structures (by value):
1.. 8 bytes: Passed in a single register
9..16 bytes: Passed in a pair, padded to the next even pair
17+: Pass as a reference.

Things like 128-bit types are also passed/returned in register pairs.



Contrast, RV ABI:
X10..X17 are used for arguments;
No spill space is provided;
...

My variant uses similar rules to my own ABI for passing/returning
structures, with:
X28, structure return pointer
X29, 'this'
Normal return values go into X10 or X11:X10.



Note that in both ABI's, passing 'this' in a register would mean that
class instances and COM objects are not equivalent (COM object methods
always pass 'this' as the first argument).

The 'this' register is implicitly also used by lambdas to pass in the
pointer to the captured bindings area (which mostly resembles a
structure containing each variable captured by the lambda).

Can note though that in this case, capturing a binding by reference
means the lambda is limited to automatic lifetime (non-automatic lambdas
may only capture by value). In this case, capture by value is the default.
Post by bart
Post by BGB
For my compiler targeting RISC-V, it uses a variation of RV's ABI rules.
Argument passing is basically similar, but struct pass/return is
different; and it passes floating-point values in GPRs (and, in my own
ISA, all floating-point values use GPRs, as there are no FPU
registers; though FPU registers do exist for RISC-V).
Supporting C's variadic functions, which is needed for many languages
when calling C across an FFI, usually requires different rules. On Win64
ABI for example, by passing low variadic arguments in both GPRs and FPU
registers.
I simplified things by assuming only GPRs are used.
Post by bart
/Implementing/ variadic functions (which only occurs if implementing C)
is another headache if it has to work with the ABI (which can be assumed
for a non-static function).
I barely have a working solution for Win64 ABI, which needs to be done
via stdarg.h, but wouldn't have a clue how to do it for SYS V.
(Even Win64 has problems, as it assumes a downward-growing stack; in my
IL interpreter, the stack grows upwards!)
Most targets use a downward growing stack.
Mine is no exception here...
Post by bart
Post by BGB
Not likely a huge issue as one is unlikely to use ELF and PE/COFF in
the same program.
For the "OS" that runs on my CPU core, it is natively using PE/COFF, but
That's interesting: you deliberately used one of the most complex file
formats around, when you could have devised your own?
For what I wanted, I would have mostly needed to recreate most of the
same functionality as PE/COFF anyways.


When one considers the entire loading process (including DLLs/SOs), then
PE/COFF loading is actually simpler than ELF loading (ELF subjects the
loader to needing to deal with symbol and relocation tables), similar to
PIE loading.


Things like the MZ stub are optional in my case, and mostly ignored if
present (in my LZ compressed PE variants, the MZ stub is omitted entirely).


I had at one point considered doing a custom format resembling LZ
compressed MachO, but ended up not bothering, as it wouldn't have really
saved anything over LZ compressed PE/COFF.


Some "unneeded cruft" like the Resource Section was discarded, mostly
replaced by an embedded WAD2 image. The header was modified some to
allow for backwards compatibility with the Windows format (mostly
creating a dummy header in the original format that points to the WAD2
directory).


Idea is that icons, bitmaps, and other things, would mostly be held in
WAD lumps. Though, resources which may be accessed via symbols in the
EXE/DLL need to be stored uncompressed (where "__rsrc_lumpname" may be
used to access the contents of resource-section lumps as an extern symbol).

Say, for example:
extern byte __rsrc_mybitmap[]; //resolves to a DIB/BMP or similar

For now, resource formats:
Images:
BMP (various settings)
4, 8, and 16 bpp typical
Supports a non-standard 16-bpp alpha-blended mode (*1).
Supports non-standard 16 color and 256 color with transparent.
Supports CRAM BMP as well (2 bpp)
QOI (assumes RGBA32, nominally lossless)
QOI is a semi-simplistic non-entropy-coded format.
Can give PNG-like compression in some cases.
Reasonably fast/cheap to decode.
LCIF, custom lossy format, color-cell compression.
OK Q/bpp but mostly only on the low-end.
Resembles a QOI+CRAM hybrid.
UPIC, lossy or lossless, JPEG-like (*2)

*1:
0rrrrrgggggbbbbb Normal/Opaque
1rrrraggggabbbba With 3 bit alpha (4b/ch RGB).

For 16 and 256 color, a variant is supported with a transparent color.
Generally the high intensity magenta is reused as the transparent color.
This is encoded in the color palette (if all colors apart from one have
the alpha bits set to FF, and one color has 00, then that color is
assumed to be a transparent color).

CRAM bpp: Uses a limited form of the 8-bit CRAM format:
16 bits, 4x4 pixels, 1 bit per pixel
2x 8 bits: Color Endpoints
The rest of the format being unsupported, so it can simply assume a
fixed 32-bits per 4x4 pixel cell.



*2: The UPIC format is structurally similar to JPEG, but:
Uses TLV packaging (vs FF-escape tagging);
Uses Rice coding (vs Huffman)
Uses Z3.V5 VLC, vs Z4.V4
Uses Block-Haar and RCT
Vs DCT and YCbCr.
Supports an alpha channel.
Y 1 (*2A)
YA 1:1 (*2A)
YUV 4:2:0
YUV 4:4:4 (*2A)
YUVA 4:2:0:4
YUVA 4:4:4:4 (*2A)
*2A: May be used in the lossless modes, depending on image.


VLC coding resembles Deflate's natch distance encoding, with sign-folded
values. Runs of zero coefficients have a shorter limit, but similar.
Like with JPEG, an 0x00 symbol encodes an early EOB.

In tests, on my main PC:
Vs JPEG: It is a little faster
Q/bpp is similar, better/worse depends on image.
Slightly worse on photos, but "similar".
Generally somewhat better on artificial images.
Vs PNG:
Faster to decode (with less memory overhead);
Better compression on many images (particularly photo-like).

Note that UPIC was designed to not require any large intermediate
buffers, so will decode directly to an RGB555 or RGBA32 output buffer
(decoding happens in terms of individual 16x16 pixel macroblocks).

It was designed to be moderately fast and to try to minimize memory
overhead for decoding (vs either PNG or JPEG, which need a more
significant chunk of working memory to decode).


Block-Haar is a Haar transform made to fit the same 8x8 pixel blocks as
DCT, where Haar maps (A,B)->(C,D):
C=(A+B)/2 (*: X/2 here being defined as (X>>1))
D=A-B
But, can be reversed exactly, IIRC:
B=C-(D/2)
A=B+D
By doing multiple stages of Haar transform, one can build an 8-pixel
version, and then use horizontal and vertical transforms for an 8x8
block. It is computationally fairly cheap, and lossless.

The Walsh-Hadamard transform can give similar properties, but generally
involves a few extra steps that make it more computationally expensive.

It is possible to use a lifting transform to make a Reversible DCT, but
it is slow...


BGBCC accepts JPEG and PNG for input and can convert them to
BMP/QOI/UPIC as needed.


For audio storage, generally using the RIFF WAV format. For bulk audio,
both A-Law and IMA ADPCM work OK. Granted, IMA ADPCM is not space
efficient for stereo, but mostly OK for mono (most common use-case for
sound effects).
Post by bart
I did exactly that at a period when my generated DLLs were buggy for
some reason (it turned out to be two reasons). I created a simple
dynamic library format of my own. Then I found the same format worked
also for executables.
But I needed a loader program to run them, as Windows obviously didn't
understand the format. Such a program can be written in 800 lines of C,
and can dynamically libraries in both my format, and proper DLLs (not
the buggy ones I generated!).
A hello-world program is under 300 bytes compared with 2 or
2.5KB of EXE. And the format is portable to Linux, so no need to
generate ELF (but I haven't tried). Plus the format might be transparent
to AV software (haven't tried that either).
OK.

By design, my PEL format (PE+LZ) isn't going to get under 2K (1K for
headers, 1K for LZ'ed sections).

But, usually this is not a problem.
BGB
2024-12-20 11:10:44 UTC
Reply
Permalink
Post by BGB
Post by bart
Post by BGB
Post by bart
Post by BGB
By-Value Structs smaller than 16 bytes are passed as-if they were a
64 or 128 bit integer type (as a single register or as a register
pair, with a layout matching their in-memory representation).
...
But, yeah, at the IL level, one could potentially eliminate structs
and arrays as a separate construct, and instead have bare pointers
and a generic "reserve a blob of bytes in the frame and initialize
this pointer to point to it" operator (with the business end of
this operator happening in the function prolog).
The problem with this, that I mentioned elsewhere, is how well it
would work with SYS V ABI, since the rules for structs are complex,
and apparently recursive.
Having just a block of bytes might not be enough.
In my case, I am not bothering with the SysV style ABI's (well, along
with there not being any x86 or x86-64 target...).
I'd imagine it's worse with ARM targets as there are so many more
registers to try and deconstruct structs into.
Not messed much with the ARM64 ABI or similar, but I will draw the line
in the sand somewhere.
Struct passing/return is enough of an edge case that one can just sort
of declare it "no go" between compilers with "mostly but not strictly
compatible" ABIs.
Post by bart
Post by BGB
For my ISA, it is a custom ABI, but follows mostly similar rules to
some of the other "Microsoft style" ABIs (where, I have noted that
across multiple targets, MS tools have tended to use similar ABI
designs).
When you do your own thing, it's easy.
In the 1980s, I didn't need to worry about call conventions used for
other software, since there /was/ no other software! I had to write
everything, save for the odd calls to DOS which used some form of SYSCALL.
Then, arrays and structs were actually passed and returned by value
(not via hidden references), by copying the data to and from the stack.
However, I don't recall ever using the feature, as I considered it
efficient. I always used explicit references in my code.
Most of the time, one is passing/returning structures as pointers, and
not by value.
By value structures are usually small.
When a structure is not small, it is both simpler to implement, and
usually faster, to internally pass it by reference.
If you pass a large structure to a function by value, via an on-stack
copy, and the function assigns it to another location (say, a global
  Pass by reference: Only a single copy operation is needed;
  Pass by value on-stack: At least two copy operations are needed.
One also needs to reserve enough space in the function arguments list to
hold any structures passed, which could be bad if they are potentially
large.
  R4 ..R7 : Arg0 ..Arg3
  R20..R23: Arg4 ..Arg7
  R36..R39: Arg8 ..Arg11 (optional)
  R52..R55: Arg12..Arg15 (optional)
  R2, R3:R2 (128 bit)
  R2 is also used to pass in the return value pointer.
  Generally passed in either R3 or R18, depending on ABI variant.
  R8 ..R14,  R24..R31,
  R40..R47,  R56..R63
  R15=SP
  R2 ..R7 ,  R16..R23,
  R32..R39,  R48..R55
Arguments beyond the first 8/16 register arguments are passed on stack.
In this case, a spill space for the first 8/16 arguments (64 or 128
bytes) is provided on stack before the first non-register argument.
If the function accepts a fixed number of arguments and the number of
argument registers is 8 or less, spill space need only be provided for
the first 8 arguments (calling vararg functions will always reserve
space for 16 registers in the 16-register ABI). This spill space
effectively belongs to the callee rather than the caller.
  1.. 8 bytes: Passed in a single register
  9..16 bytes: Passed in a pair, padded to the next even pair
  17+: Pass as a reference.
Things like 128-bit types are also passed/returned in register pairs.
  X10..X17 are used for arguments;
  No spill space is provided;
  ...
My variant uses similar rules to my own ABI for passing/returning
  X28, structure return pointer
  X29, 'this'
Normal return values go into X10 or X11:X10.
Note that in both ABI's, passing 'this' in a register would mean that
class instances and COM objects are not equivalent (COM object methods
always pass 'this' as the first argument).
The 'this' register is implicitly also used by lambdas to pass in the
pointer to the captured bindings area (which mostly resembles a
structure containing each variable captured by the lambda).
Can note though that in this case, capturing a binding by reference
means the lambda is limited to automatic lifetime (non-automatic lambdas
may only capture by value). In this case, capture by value is the default.
Post by bart
Post by BGB
For my compiler targeting RISC-V, it uses a variation of RV's ABI rules.
Argument passing is basically similar, but struct pass/return is
different; and it passes floating-point values in GPRs (and, in my
own ISA, all floating-point values use GPRs, as there are no FPU
registers; though FPU registers do exist for RISC-V).
Supporting C's variadic functions, which is needed for many languages
when calling C across an FFI, usually requires different rules. On
Win64 ABI for example, by passing low variadic arguments in both GPRs
and FPU registers.
I simplified things by assuming only GPRs are used.
Post by bart
/Implementing/ variadic functions (which only occurs if implementing
C) is another headache if it has to work with the ABI (which can be
assumed for a non-static function).
I barely have a working solution for Win64 ABI, which needs to be done
via stdarg.h, but wouldn't have a clue how to do it for SYS V.
(Even Win64 has problems, as it assumes a downward-growing stack; in
my IL interpreter, the stack grows upwards!)
Most targets use a downward growing stack.
Mine is no exception here...
Post by bart
Post by BGB
Not likely a huge issue as one is unlikely to use ELF and PE/COFF in
the same program.
For the "OS" that runs on my CPU core, it is natively using PE/COFF, but
That's interesting: you deliberately used one of the most complex file
formats around, when you could have devised your own?
For what I wanted, I would have mostly needed to recreate most of the
same functionality as PE/COFF anyways.
When one considers the entire loading process (including DLLs/SOs), then
PE/COFF loading is actually simpler than ELF loading (ELF subjects the
loader to needing to deal with symbol and relocation tables), similar to
PIE loading.
My wording there sucked...

PIE loading is the same as the case for ELF shared object loading, so is
fairly complex.

For normal loading, they try to make it simpler for the kernel loader by
having a special "interpreter" program deal with it. The process it then
uses to bootstrap itself is rather convoluted.
Post by BGB
Things like the MZ stub are optional in my case, and mostly ignored if
present (in my LZ compressed PE variants, the MZ stub is omitted entirely).
My loader will accept multiple sub-variants:
With MZ stub (original format);
Without MZ stub (but uncompressed);
With LZ4 compression (no MZ stub allowed).


The format for the no-stub case is basically the same as the with-stub
case, except that the stub is absent and thus the 'PE' sig is still present.

Note that in my variants, omitting the MZ stub does cause it to change
to a different checksum algorithm (the original PE/COFF checksum being
unacceptably weak).
Post by BGB
I had at one point considered doing a custom format resembling LZ
compressed MachO, but ended up not bothering, as it wouldn't have really
saved anything over LZ compressed PE/COFF.
The core process is still:
Read stuff into memory;
Apply post-load fixups.

This part of the process was essentially unavoidable.
Post by BGB
Some "unneeded cruft" like the Resource Section was discarded, mostly
replaced by an embedded WAD2 image. The header was modified some to
allow for backwards compatibility with the Windows format (mostly
creating a dummy header in the original format that points to the WAD2
directory).
Note that the change of resource section format was more because the
original approach to the resource section made little sense to me.

Identifying things with short names made a lot more sense than magic
numbers.

The WAD approach Worked for Doom and similar, probably sufficient for
things like inline bitmap images and icons.
Post by BGB
Idea is that icons, bitmaps, and other things, would mostly be held in
WAD lumps. Though, resources which may be accessed via symbols in the
EXE/DLL need to be stored uncompressed (where "__rsrc_lumpname" may be
used to access the contents of resource-section lumps as an extern symbol).
Note that it can also load blobs of text or binary data.
Though, BGBCC provides less in terms of format converters for arbitrary
data.

A special text format is used both to define files to pull into the
resource section (and what lump name to use), as well as format
conversions to apply.
Post by BGB
  extern byte __rsrc_mybitmap[];  //resolves to a DIB/BMP or similar
    BMP (various settings)
      4, 8, and 16 bpp typical
      Supports a non-standard 16-bpp alpha-blended mode (*1).
      Supports non-standard 16 color and 256 color with transparent.
      Supports CRAM BMP as well (2 bpp)
    QOI (assumes RGBA32, nominally lossless)
      QOI is a semi-simplistic non-entropy-coded format.
      Can give PNG-like compression in some cases.
      Reasonably fast/cheap to decode.
    LCIF, custom lossy format, color-cell compression.
      OK Q/bpp but mostly only on the low-end.
      Resembles a QOI+CRAM hybrid.
    UPIC, lossy or lossless, JPEG-like (*2)
  0rrrrrgggggbbbbb  Normal/Opaque
  1rrrraggggabbbba  With 3 bit alpha (4b/ch RGB).
For 16 and 256 color, a variant is supported with a transparent color.
Generally the high intensity magenta is reused as the transparent color.
This is encoded in the color palette (if all colors apart from one have
the alpha bits set to FF, and one color has 00, then that color is
assumed to be a transparent color).
  16 bits, 4x4 pixels, 1 bit per pixel
  2x 8 bits: Color Endpoints
The rest of the format being unsupported, so it can simply assume a
fixed 32-bits per 4x4 pixel cell.
There being cases where one may want this...
If an image doesn't have more than 2 colors per 4x4 cell, it may give an
acceptable image (and is often less space than 16-color).

Though, for small images, 16 color may use less space due to a smaller
color palette (but, in theory, could add a special case to allow
omitting the color palette when it is the default palette).

Say:
biBitCount=8, biClrUsed=0, biClrImportant=256
Encoding a special "palette is absent, use fixed OS palette" case.
As the BMP format burns 1K just to encode a 256-color palette.
Post by BGB
  Uses TLV packaging (vs FF-escape tagging);
  Uses Rice coding (vs Huffman)
  Uses Z3.V5 VLC, vs Z4.V4
  Uses Block-Haar and RCT
    Vs DCT and YCbCr.
  Supports an alpha channel.
    Y    1       (*2A)
    YA   1:1     (*2A)
    YUV  4:2:0
    YUV  4:4:4   (*2A)
    YUVA 4:2:0:4
    YUVA 4:4:4:4 (*2A)
  *2A: May be used in the lossless modes, depending on image.
VLC coding resembles Deflate's natch distance encoding, with sign-folded
values. Runs of zero coefficients have a shorter limit, but similar.
Like with JPEG, an 0x00 symbol encodes an early EOB.
^ match. Also, UPIC is a custom format.

Add context:
Actually, it is using an entropy coding scheme I call STF+AdRice:
Swap towards front, with Adaptive Rice Coding.

The Rice coding parameter (k) is adapted based on Q:
0: k--;
1: no change;
2..7: k++
8: k++; Symbol index encoded as a raw 8 bits.

Symbols are encoded as indices into a table. Whenever an index is
encoded, the symbol swaps places with the symbol at (I*15)/16, causing
more commonly used symbols to migrate towards 0.

Theoretically, the decoding process is more complex than a table-driven
static Huffman decoder (as well as worse compression), but:
Less memory is needed;
Faster to initialize;
On average, it is speed competitive.
Lookup table initialization for static Huffman is expensive;
Decode speed hindered by high L1 miss rates.

With a 15-bit symbol-length limit, Huffman has a very high L1 miss rate.
Generally, to be fast, one needs to impose a 12 or 13 bit symbol length
limit, reducing compression, but greatly reducing the number of L1
misses. Though, 12 bits is a lower limit in practice (going much less
than this, and Huffman coding becomes ineffective).
Post by BGB
  Vs JPEG: It is a little faster
    Q/bpp is similar, better/worse depends on image.
      Slightly worse on photos, but "similar".
      Generally somewhat better on artificial images.
    Faster to decode (with less memory overhead);
    Better compression on many images (particularly photo-like).
Note that UPIC was designed to not require any large intermediate
buffers, so will decode directly to an RGB555 or RGBA32 output buffer
(decoding happens in terms of individual 16x16 pixel macroblocks).
It was designed to be moderately fast and to try to minimize memory
overhead for decoding (vs either PNG or JPEG, which need a more
significant chunk of working memory to decode).
Block-Haar is a Haar transform made to fit the same 8x8 pixel blocks as
  C=(A+B)/2  (*: X/2 here being defined as (X>>1))
  D=A-B
  B=C-(D/2)
  A=B+D
By doing multiple stages of Haar transform, one can build an 8-pixel
version, and then use horizontal and vertical transforms for an 8x8
block. It is computationally fairly cheap, and lossless.
The Walsh-Hadamard transform can give similar properties, but generally
involves a few extra steps that make it more computationally expensive.
It is possible to use a lifting transform to make a Reversible DCT, but
it is slow...
Also, the code-size footprint for UPIC is smaller than a JPEG decoder.
Post by BGB
BGBCC accepts JPEG and PNG for input and can convert them to BMP/QOI/
UPIC as needed.
For audio storage, generally using the RIFF WAV format. For bulk audio,
both A-Law and IMA ADPCM work OK. Granted, IMA ADPCM is not space
efficient for stereo, but mostly OK for mono (most common use-case for
sound effects).
This isn't used much yet in this project.

In general, for other cases where I use audio, 16kHz is a typical default.

Where:
8 and 11 kHz sound poor.
Also 8-bit linear PCM sounds poor.

I am less a fan of MP3:
Very complex decoder;
Much under 96 or 128 kbps, has very obvious audio distortions...
At lower bitrates, the audio quality is decidedly unpleasant.
IMHO: 16 kHz ADPCM sounds better than 64 kbps MP3.

Not sure why it is so possible, when, as noted, at lower bitrates it
sounds pretty broken (but, then again, it mostly sounds much fine at 128
kbps or beyond, so dunno).

ADPCM's property of sounding tinny is still preferable to sounding like
one is rattling a steel can full of broken glass, IMHO.


Did experimentally create an MP3-like audio codec (but much simpler),
also using Block-Haar (rather than MDCT), and reused some amount of code
from UPIC, which seems to avoid some of MP3's more obvious artifacts.
But, the design did have a few of its own issues (might need to revisit
later).

Mostly, it uses a half-cubic spline to approximate the low-frequency
components (and try to reduce blocking artifacts; the spline is
subtracted out so only higher frequency components use the Block-Haar),
but seemingly the spline was too coarse (one sample per block), and I
would likely need a higher effective sampling rate for the spline to
avoid blocking artifacts in some cases (mostly, with sounds at roughly
the same frequency as the block size effectively resulting in square
waves, which sound bad).
Post by BGB
Post by bart
I did exactly that at a period when my generated DLLs were buggy for
some reason (it turned out to be two reasons). I created a simple
dynamic library format of my own. Then I found the same format worked
also for executables.
But I needed a loader program to run them, as Windows obviously didn't
understand the format. Such a program can be written in 800 lines of
C, and can dynamically libraries in both my format, and proper DLLs
(not the buggy ones I generated!).
A hello-world program is under 300 bytes compared with 2 or
2.5KB of EXE. And the format is portable to Linux, so no need to
generate ELF (but I haven't tried). Plus the format might be
transparent to AV software (haven't tried that either).
OK.
By design, my PEL format (PE+LZ) isn't going to get under 2K (1K for
headers, 1K for LZ'ed sections).
But, usually this is not a problem.
Lawrence D'Oliveiro
2024-12-23 02:08:46 UTC
Reply
Permalink
... (what debug mechanisms I have, effectively lack any symbols
for things inside "ld-linux.so"'s domain).
nm -D /lib/ld-linux.so.2

BGB
2024-12-17 19:07:44 UTC
Reply
Permalink
Post by Thiago Adams
Post by BGB
Post by Thiago Adams
Post by BGB
Post by bart
Post by Bonita Montero
C++ is more readable because is is magnitudes more expressive than C.
You can easily write a C++-statement that would hunddres of lines in
C (imagines specializing a unordered_map by hand). Making a language
less expressive makes it even less readable, and that's also true for
your reduced C.
That's not really the point of it. This reduced C is used as an
intermediate language for a compiler target. It will not usually be
read, or maintained.
An intermediate language needs to at a lower level than the source language.
And for this project, it needs to be compilable by any C89 compiler.
Generating C++ would be quite useless.
As an IL, even C is a little overkill, unless turned into a
restricted subset (say, along similar lines to GCC's GIMPLE).
   Only function-scope variables allowed;
   No high-level control structures;
   ...
   int foo(int x)
   {
     int i, v;
     for(i=x, v=0; i>0; i--)
       v=v*i;
     return(v);
   }
   int foo(int x)
   {
     int i;
     int v;
     i=x;
     v=0;
     if(i<=0)goto L1;
     v=v*i;
     i=i-1;
     if(i>0)goto L0;
     return v;
   }
...
I have considered to remove loops and keep only goto.
But I think this is not bring too much simplification.
It depends.
If the compiler works like an actual C compiler, with a full parser
and AST stage, yeah, it may not save much.
If the parser is a thin wrapper over 3AC operations (only allowing
statements that map 1:1 with a 3AC IR operation), it may save a bit more...
As for whether or not it makes sense to use a C like syntax here, this
is more up for debate (for practical use within a compiler, I would
assume a binary serialization rather than an ASCII syntax, though
ASCII may be better in terms of inter-operation or human readability).
But, as can be noted, I would assume a binary serialization that is
oriented around operators; and *not* about serializing the structures
used to implement those operators. Also I would assume that the IR
need not be in SSA form (conversion to full SSA could be done when
reading in the IR operations).
Ny argument is that not using SSA form means fewer issues for both the
serialization format and compiler front-end to need to deal with (and
is comparably easy to regenerate for the backend, with the backend
operating with its internal IR in SSA form).
Well, contrast to LLVM assuming everything is always in SSA form.
...
I also have considered split expressions.
For instance
if (a*b+c) {}
into
register int r1 = a * b;
register int r2 = r1 + c;
if (r2) {}
This would make easier to add overflow checks in runtime (if desired)
and implement things like _complex
Is this what you mean by 3AC or SSA?
3AC means that IR expressed 3 (or sometimes more) operands per IR op.

So:
MUL r1, a, b
Rather than, say, stack:
LOAD a
LOAD b
MUL
STORE r1


SSA:
Static Single Assignment

Generally:
Every variable may only be assigned once (more like in a functional
programming language);
Generally, variables are "merged" in the control-flow via PHI operators
(which variable merges in depending on which path control came from).

IMHO, while SSA is preferable for backend analysis, optimization, and
code generation; it is undesirable pretty much everywhere else as it
adds too much complexity.

Better IMO for the frontend compiler and main IL stage to assume that
local variables are freely mutable.

Typically, global variables are excluded in most variants, and remain
fully mutable; but may be handled as designated LOAD/STORE operations.


In BGBCC though, full SSA only applies to temporaries. Normal local
variables are merely flagged by "version", and all versions of the same
local variable implicitly merge back together at each branch/label.

This allows some similar advantages (for analysis and optimization)
while limiting some of the complexities. Though, this differs from
temporaries which are assumed to essentially fully disappear once they
go outside of the span in which they exist (albeit with an awkward case
to deal with temporaries that cross basic-block boundaries, which need
to actually "exist" in some semi-concrete form, more like local variables).

Note that unless the address is taken of a local variable, it need not
have any backing in memory. Temporaries can never have their address
taken, so generally exist exclusively in CPU registers.
Post by Thiago Adams
This would definitely simplify expressions grammar.
Thiago Adams
2024-12-17 19:33:09 UTC
Reply
Permalink
Post by BGB
Post by Thiago Adams
Post by BGB
Post by Thiago Adams
Post by BGB
Post by bart
Post by Bonita Montero
C++ is more readable because is is magnitudes more expressive than C.
You can easily write a C++-statement that would hunddres of lines in
C (imagines specializing a unordered_map by hand). Making a language
less expressive makes it even less readable, and that's also true for
your reduced C.
That's not really the point of it. This reduced C is used as an
intermediate language for a compiler target. It will not usually
be read, or maintained.
An intermediate language needs to at a lower level than the source language.
And for this project, it needs to be compilable by any C89 compiler.
Generating C++ would be quite useless.
As an IL, even C is a little overkill, unless turned into a
restricted subset (say, along similar lines to GCC's GIMPLE).
   Only function-scope variables allowed;
   No high-level control structures;
   ...
   int foo(int x)
   {
     int i, v;
     for(i=x, v=0; i>0; i--)
       v=v*i;
     return(v);
   }
   int foo(int x)
   {
     int i;
     int v;
     i=x;
     v=0;
     if(i<=0)goto L1;
     v=v*i;
     i=i-1;
     if(i>0)goto L0;
     return v;
   }
...
I have considered to remove loops and keep only goto.
But I think this is not bring too much simplification.
It depends.
If the compiler works like an actual C compiler, with a full parser
and AST stage, yeah, it may not save much.
If the parser is a thin wrapper over 3AC operations (only allowing
statements that map 1:1 with a 3AC IR operation), it may save a bit more...
As for whether or not it makes sense to use a C like syntax here,
this is more up for debate (for practical use within a compiler, I
would assume a binary serialization rather than an ASCII syntax,
though ASCII may be better in terms of inter-operation or human
readability).
But, as can be noted, I would assume a binary serialization that is
oriented around operators; and *not* about serializing the structures
used to implement those operators. Also I would assume that the IR
need not be in SSA form (conversion to full SSA could be done when
reading in the IR operations).
Ny argument is that not using SSA form means fewer issues for both
the serialization format and compiler front-end to need to deal with
(and is comparably easy to regenerate for the backend, with the
backend operating with its internal IR in SSA form).
Well, contrast to LLVM assuming everything is always in SSA form.
...
I also have considered split expressions.
For instance
if (a*b+c) {}
into
register int r1 = a * b;
register int r2 = r1 + c;
if (r2) {}
This would make easier to add overflow checks in runtime (if desired)
and implement things like _complex
Is this what you mean by 3AC or SSA?
3AC means that IR expressed 3 (or sometimes more) operands per IR op.
  MUL r1, a, b
  LOAD a
  LOAD b
  MUL
  STORE r1
  Static Single Assignment
Oh sorry .. I knew what SSA is.
Post by BGB
Every variable may only be assigned once (more like in a functional
programming language);
Generally, variables are "merged" in the control-flow via PHI operators
(which variable merges in depending on which path control came from).
I do similar merge in my flow analysis but without the concept of SSA.
Post by BGB
IMHO, while SSA is preferable for backend analysis, optimization, and
code generation; it is undesirable pretty much everywhere else as it
adds too much complexity.
Better IMO for the frontend compiler and main IL stage to assume that
local variables are freely mutable.
Typically, global variables are excluded in most variants, and remain
fully mutable; but may be handled as designated LOAD/STORE operations.
In BGBCC though, full SSA only applies to temporaries. Normal local
variables are merely flagged by "version", and all versions of the same
local variable implicitly merge back together at each branch/label.
Sorry what is BGBCC ? (C compiler?)
Post by BGB
This allows some similar advantages (for analysis and optimization)
while limiting some of the complexities. Though, this differs from
temporaries which are assumed to essentially fully disappear once they
go outside of the span in which they exist (albeit with an awkward case
to deal with temporaries that cross basic-block boundaries, which need
to actually "exist" in some semi-concrete form, more like local variables).
Note that unless the address is taken of a local variable, it need not
have any backing in memory. Temporaries can never have their address
taken, so generally exist exclusively in CPU registers.
Post by Thiago Adams
This would definitely simplify expressions grammar.
It can be added in the future.
BGB
2024-12-18 18:51:23 UTC
Reply
Permalink
Post by Thiago Adams
Post by BGB
Post by Thiago Adams
Post by BGB
Post by Thiago Adams
Post by BGB
Post by bart
Post by Bonita Montero
C++ is more readable because is is magnitudes more expressive than C.
You can easily write a C++-statement that would hunddres of lines in
C (imagines specializing a unordered_map by hand). Making a language
less expressive makes it even less readable, and that's also true for
your reduced C.
That's not really the point of it. This reduced C is used as an
intermediate language for a compiler target. It will not usually
be read, or maintained.
An intermediate language needs to at a lower level than the source language.
And for this project, it needs to be compilable by any C89 compiler.
Generating C++ would be quite useless.
As an IL, even C is a little overkill, unless turned into a
restricted subset (say, along similar lines to GCC's GIMPLE).
   Only function-scope variables allowed;
   No high-level control structures;
   ...
   int foo(int x)
   {
     int i, v;
     for(i=x, v=0; i>0; i--)
       v=v*i;
     return(v);
   }
   int foo(int x)
   {
     int i;
     int v;
     i=x;
     v=0;
     if(i<=0)goto L1;
     v=v*i;
     i=i-1;
     if(i>0)goto L0;
     return v;
   }
...
I have considered to remove loops and keep only goto.
But I think this is not bring too much simplification.
It depends.
If the compiler works like an actual C compiler, with a full parser
and AST stage, yeah, it may not save much.
If the parser is a thin wrapper over 3AC operations (only allowing
statements that map 1:1 with a 3AC IR operation), it may save a bit more...
As for whether or not it makes sense to use a C like syntax here,
this is more up for debate (for practical use within a compiler, I
would assume a binary serialization rather than an ASCII syntax,
though ASCII may be better in terms of inter-operation or human
readability).
But, as can be noted, I would assume a binary serialization that is
oriented around operators; and *not* about serializing the
structures used to implement those operators. Also I would assume
that the IR need not be in SSA form (conversion to full SSA could be
done when reading in the IR operations).
Ny argument is that not using SSA form means fewer issues for both
the serialization format and compiler front-end to need to deal with
(and is comparably easy to regenerate for the backend, with the
backend operating with its internal IR in SSA form).
Well, contrast to LLVM assuming everything is always in SSA form.
...
I also have considered split expressions.
For instance
if (a*b+c) {}
into
register int r1 = a * b;
register int r2 = r1 + c;
if (r2) {}
This would make easier to add overflow checks in runtime (if desired)
and implement things like _complex
Is this what you mean by 3AC or SSA?
3AC means that IR expressed 3 (or sometimes more) operands per IR op.
   MUL r1, a, b
   LOAD a
   LOAD b
   MUL
   STORE r1
   Static Single Assignment
Oh sorry .. I knew what SSA is.
Post by BGB
Every variable may only be assigned once (more like in a functional
programming language);
Generally, variables are "merged" in the control-flow via PHI
operators (which variable merges in depending on which path control
came from).
I do similar merge in my flow analysis but without the concept of SSA.
Post by BGB
IMHO, while SSA is preferable for backend analysis, optimization, and
code generation; it is undesirable pretty much everywhere else as it
adds too much complexity.
Better IMO for the frontend compiler and main IL stage to assume that
local variables are freely mutable.
Typically, global variables are excluded in most variants, and remain
fully mutable; but may be handled as designated LOAD/STORE operations.
In BGBCC though, full SSA only applies to temporaries. Normal local
variables are merely flagged by "version", and all versions of the
same local variable implicitly merge back together at each branch/label.
Sorry what is BGBCC ? (C compiler?)
It is my C compiler.

Can be found within my current main project:
https://github.com/cr88192/bgbtech_btsr1arch/tree/master/bgbcc22


It started out, long ago, as a fork off my scripting language, which was
originally a JavaScript clone.

First stage:
Originally written as a C interpreter of sorts.

Original idea was to use dynamically compiled C as an application
scripting language, but C wasn't a great language for this task (vs a JS
clone), and the compiler was a lot harder to debug.

Then, for a while, it was turned over to mining metadata from headers to
generate an FFI for the script language.


Its use as a C compiler was revived when I started my CPU ISA project,
as I needed a compiler for it, and other options (Clang, GCC, and LCC)
were unattractive in various ways.

Though, in all, a lot more effort in the project has gone into the C
compiler than into much of anything else, and it is still a bit of a
pain finding and fixing bugs (and avoiding causing new bugs).


It targets both BJX2 (my own ISA) or RISC-V, albeit using PE/COFF for
the latter (rather than ELF).
Post by Thiago Adams
Post by BGB
This allows some similar advantages (for analysis and optimization)
while limiting some of the complexities. Though, this differs from
temporaries which are assumed to essentially fully disappear once they
go outside of the span in which they exist (albeit with an awkward
case to deal with temporaries that cross basic-block boundaries, which
need to actually "exist" in some semi-concrete form, more like local
variables).
Note that unless the address is taken of a local variable, it need not
have any backing in memory. Temporaries can never have their address
taken, so generally exist exclusively in CPU registers.
Post by Thiago Adams
This would definitely simplify expressions grammar.
It can be added in the future.
Lawrence D'Oliveiro
2024-12-21 05:34:07 UTC
Reply
Permalink
Every variable may only be assigned once ...
Note this only applies to registers.
Janis Papanagnou
2024-12-16 17:12:40 UTC
Reply
Permalink
Post by BGB
[...]
Pretty much all higher level control flow can be expressed via goto.
A 'goto' may be used but it isn't strictly *necessary*. What *is*
necessary, though, that is an 'if' (some conditional branch), and
either 'goto' or recursive functions.

Janis
Post by BGB
[...]
bart
2024-12-16 18:37:08 UTC
Reply
Permalink
Post by Janis Papanagnou
Post by BGB
[...]
Pretty much all higher level control flow can be expressed via goto.
A 'goto' may be used but it isn't strictly *necessary*. What *is*
necessary, though, that is an 'if' (some conditional branch), and
either 'goto' or recursive functions.
You would do away with just about the simplest feature of any language,
and insist users emulate intra-function branching via function calls?

I'd like to see your design of IL that doesn't have branching! I guess
it would also need closures and/or continuations. I suspect you either
don't quite understand what an IL is for, or forgot that's what this is
about.

An IL is typically lower level than the source language, while higher
level than a native code target which makes it easier to generate code
for. A functional IL would make it harder than assembly.
Janis Papanagnou
2024-12-16 20:39:54 UTC
Reply
Permalink
Post by bart
Post by Janis Papanagnou
Post by BGB
[...]
Pretty much all higher level control flow can be expressed via goto.
A 'goto' may be used but it isn't strictly *necessary*. What *is*
necessary, though, that is an 'if' (some conditional branch), and
either 'goto' or recursive functions.
You would do away with just about the simplest feature of any language,
and insist users emulate intra-function branching via function calls?
Why are you, yet again, and still, making up nonsensical suppositions!
Post by bart
I'd like to see your design of IL that doesn't have branching! I guess
it would also need closures and/or continuations. I suspect you either
don't quite understand what an IL is for, or forgot that's what this is
about.
Why are you, yet again, and still, making up nonsensical suppositions!

I wasn't commenting on any "IL", nor on any "emulation". I also didn't
restrict my view to any specific machine model (as you have obviously
been doing).

You can still see what I was commenting on in the single sentence that
I quoted in my post. (No more and no less.) - If you have anything to
say (without any nonsensical suppositions), concerning the quote and
my response, feel free to make another try.
Post by bart
An IL is typically lower level than the source language, while higher
level than a native code target which makes it easier to generate code
for. A functional IL would make it harder than assembly.
If you want to talk about "IL" or "assembly" search another target for
discussing your ideas.

Janis
bart
2024-12-16 23:26:22 UTC
Reply
Permalink
Post by Janis Papanagnou
Post by bart
Post by Janis Papanagnou
Post by BGB
[...]
Pretty much all higher level control flow can be expressed via goto.
A 'goto' may be used but it isn't strictly *necessary*. What *is*
necessary, though, that is an 'if' (some conditional branch), and
either 'goto' or recursive functions.
You would do away with just about the simplest feature of any language,
and insist users emulate intra-function branching via function calls?
Why are you, yet again, and still, making up nonsensical suppositions!
Post by bart
I'd like to see your design of IL that doesn't have branching! I guess
it would also need closures and/or continuations. I suspect you either
don't quite understand what an IL is for, or forgot that's what this is
about.
Why are you, yet again, and still, making up nonsensical suppositions!
I wasn't commenting on any "IL",
The subthread was about ILs.
Post by Janis Papanagnou
nor on any "emulation". I also didn't
restrict my view to any specific machine model (as you have obviously
been doing).
You can still see what I was commenting on in the single sentence that
I quoted in my post. (No more and no less.) - If you have anything to
say (without any nonsensical suppositions), concerning the quote and
my response, feel free to make another try.
In that case I've no idea what you were trying to say.

When somebody says that 'goto' can emulate any control structure, then
clearly some of them need to be conditional; that is implied.

Your reply suggested they you can do away with 'goto', and use recursive
functions, in a scenario where no other control structures need exist.

OK, if this is not for an IL, then it's not a language I would care for
either. Why tie one hand behind your back for no good reason?
Post by Janis Papanagnou
If you want to talk about "IL" or "assembly" search another target for
discussing your ideas.
So what was your proposal about?
Keith Thompson
2024-12-17 01:19:21 UTC
Reply
Permalink
bart <***@freeuk.com> writes:
[SNIP]
Post by bart
In that case I've no idea what you were trying to say.
When somebody says that 'goto' can emulate any control structure, then
clearly some of them need to be conditional; that is implied.
Your reply suggested they you can do away with 'goto', and use
recursive functions, in a scenario where no other control structures
need exist.
OK, if this is not for an IL, then it's not a language I would care
for either. Why tie one hand behind your back for no good reason?
I read Janis's post. I saw a suggestion that certain constructs are
*theoretically* unnecessary. I saw no suggestion of any advocacy for
such an approach.

"""
A 'goto' may be used but it isn't strictly *necessary*. What *is*
necessary, though, that is an 'if' (some conditional branch), and
either 'goto' or recursive functions.
"""

[...]
Post by bart
So what was your proposal about?
I saw no proposal.
--
Keith Thompson (The_Other_Keith) Keith.S.Thompson+***@gmail.com
void Void(void) { Void(); } /* The recursive call of the void */
BGB
2024-12-17 06:40:54 UTC
Reply
Permalink
Post by Keith Thompson
[SNIP]
Post by bart
In that case I've no idea what you were trying to say.
When somebody says that 'goto' can emulate any control structure, then
clearly some of them need to be conditional; that is implied.
Your reply suggested they you can do away with 'goto', and use
recursive functions, in a scenario where no other control structures
need exist.
OK, if this is not for an IL, then it's not a language I would care
for either. Why tie one hand behind your back for no good reason?
I read Janis's post. I saw a suggestion that certain constructs are
*theoretically* unnecessary. I saw no suggestion of any advocacy for
such an approach.
"""
A 'goto' may be used but it isn't strictly *necessary*. What *is*
necessary, though, that is an 'if' (some conditional branch), and
either 'goto' or recursive functions.
"""
While if-call is technically possible, it is likely to result in a
significantly higher performance overhead if compared with if-goto.


Say:
if-goto:
can be a single CPU instruction on some targets.
Both RISC-V and BJX2 can do single-instruction compare-and-branch.
if-call:
A whole lot more than 1 instruction...
One may have call/return, stack frame creation, ...
Or, a backend that is very clever about inlining.


Also: if-goto can readily express pretty much every other intra-function
control flow construct:
for, while, switch, break, continue, ...
And, is less awkward for a compiler than trying to map control flow to
any of the other constructs. Also it is generally the fastest construct
for implementing these higher level constructs.

Meanwhile, if-call, good luck... You are also going to need something
beyond normal C style variable scoping to make this work acceptably. At
best, it is likely to be very awkward and likely to perform poorly.


Though, it is possible that one could make a case for a 3-way branch
operator in the IR, say:
if3(x,y) A, B, C
Which does the equivalent of, say:
if(x<y)goto A;
else if(x>y)goto B;
else goto C;

This would be a bit niche, but could be useful for things like
implementing "switch()" via binary subdivision.

Though, functionally could decay into normal if-goto in various cases.


...
Post by Keith Thompson
[...]
Post by bart
So what was your proposal about?
I saw no proposal.
bart
2024-12-17 16:17:37 UTC
Reply
Permalink
Post by Keith Thompson
[SNIP]
Post by bart
In that case I've no idea what you were trying to say.
When somebody says that 'goto' can emulate any control structure, then
clearly some of them need to be conditional; that is implied.
Your reply suggested they you can do away with 'goto', and use
recursive functions, in a scenario where no other control structures
need exist.
OK, if this is not for an IL, then it's not a language I would care
for either. Why tie one hand behind your back for no good reason?
I read Janis's post. I saw a suggestion that certain constructs are
*theoretically* unnecessary. I saw no suggestion of any advocacy for
such an approach.
"""
A 'goto' may be used but it isn't strictly *necessary*. What *is*
necessary, though, that is an 'if' (some conditional branch), and
either 'goto' or recursive functions.
"""
This doesn't actually make much sense. So 'goto' is necessary, but
'goto' *is*?

If you try to extract any meaning, it is that any control flow can be
expressed either with 'goto' or with 'recursive functions'.

This is what I picked up on. Who on earth would eschew 'goto' and use
such a disproportionately more complex and inefficient method like
recursive functions?

How would you even express an arbitrary goto from random point X in a
function to random point Y, which may be inside differently nested
blocks, via a recursive function?

If this is suppposed to be theoretically possible, then neither do you
need a compiler OR a computer to run any program! Pencil and paper will
work.
Janis Papanagnou
2024-12-17 17:18:27 UTC
Reply
Permalink
Post by bart
[...]
"""
[...] but it isn't strictly *necessary*. [...]
"""
This doesn't actually make much sense. So 'goto' is necessary, but
'goto' *is*?
Have you issues with reading? ("isn't" is not "is", and Keith's
"[*theoretically*] unnecessary" is not "necessary".)
Post by bart
If you try to extract any meaning, it is that any control flow can be
expressed either with 'goto' or with 'recursive functions'.
It's actually the other way round; you can specify functionality
using Recursive Functions, only a subset of these functions can
be expressed with simple loops by algorithmic transformations.[*]

(Of course you *could* thus also take an approach the other way
round, i.e. from an imperative 'while' to a recursive function,
but yet, beyond your [wrong] suppositions, no one was suggesting
that.)
Post by bart
[...]
(I snipped the irrelevant rest that you made up in your confusion
of not knowing.)

Janis

[*] I had started to write a longer post to explain that in detail
to you, though when I saw Keith's terse post I thought that should
suffice. - Alas, no. So all I want to suggest is to read up things
yourself. I'd suggest books from F.L.Bauer ("Algorithmic Language
and Program Development", for example), or from H.Partsch on that
topic; e.g. "Specification and Transformation of Programs".
(You may come back after reading, in case you are still puzzled.)
Waldek Hebisch
2024-12-17 18:46:00 UTC
Reply
Permalink
Post by bart
If you try to extract any meaning, it is that any control flow can be
expressed either with 'goto' or with 'recursive functions'.
This is what I picked up on. Who on earth would eschew 'goto' and use
such a disproportionately more complex and inefficient method like
recursive functions?
Due to silly conding standard? Or in language that does not have
'goto'.
Post by bart
How would you even express an arbitrary goto from random point X in a
function to random point Y, which may be inside differently nested
blocks, via a recursive function?
AFAICS in C main limitation is that you either pass all variables
as parameters (ugly and verbose) or use only global variables
(much worse than 'goto'). The following silly example shows
that 'if' can be simulated using array of function pointers and
indirect calls:

static int bar(int a) {
return a + 1;
}

static int baz(int a) {
return 2*a;
}

int
silly(int a) {
int (*t[2])(int) = {bar, baz};
return (*t[!!(a > 3)])(a);
}

If you compile it with 'gcc -S -O2' you can see that actually there
are no function calls in generated code (but generated code is clearly
crappy). However, needed optimication is really simple, so
in principle any compiler could do better. OTOH code like
this is rare in practice, so probably compiler writers did not
bother.

In similar way one can simulate dense C 'switch'.

Main point is that function call at the end of say 'F' to function
'G' which retruns in the same way as 'F' can be compiled to some
stack shuffling + goto (this is called 'tail call optimization').

IIUC at least some Scheme and ML compilers keep calls in intermediate
level representation (both have no 'goto') and convert them to
jumps only when emiting machine code.

Similar thing was used by one disassembly system: it generated "high
level code" by converting all jumps in machine code to function
calls. Later the result was cleaned up by transformations, in
particular recursion elimination.

Of course, for orginal purpose of this thread replacing 'if' by
indirect calls is useless
--
Waldek Hebisch
bart
2024-12-17 22:45:14 UTC
Reply
Permalink
Post by Waldek Hebisch
Post by bart
If you try to extract any meaning, it is that any control flow can be
expressed either with 'goto' or with 'recursive functions'.
This is what I picked up on. Who on earth would eschew 'goto' and use
such a disproportionately more complex and inefficient method like
recursive functions?
Due to silly conding standard? Or in language that does not have
'goto'.
It was suggested that 'theoretically', 'goto' could be replaced by
recursive function calls.

Whether still within the context of a language with no other control
flow instructions, is not known. The suggester also chose not to share
examples of how it would work.
Waldek Hebisch
2024-12-18 00:23:12 UTC
Reply
Permalink
Post by bart
Post by Waldek Hebisch
Post by bart
If you try to extract any meaning, it is that any control flow can be
expressed either with 'goto' or with 'recursive functions'.
This is what I picked up on. Who on earth would eschew 'goto' and use
such a disproportionately more complex and inefficient method like
recursive functions?
Due to silly conding standard? Or in language that does not have
'goto'.
It was suggested that 'theoretically', 'goto' could be replaced by
recursive function calls.
Whether still within the context of a language with no other control
flow instructions, is not known. The suggester also chose not to share
examples of how it would work.
The example I gave (and you snipped) was supposed to explain how
the technique works, but it seems that it is not enough. So
let us look at another example. Start from ordinary C code that
only uses global variables (this is not strictly necessary, but
let as make such assumption for simplicity):

int n;
int * a;
int b;
int i;

...
/* Simple search loop */
for(i = 0; i < n; i++) {
if (a[i] == b) {
break;
}
}

First, express flow control using only conditional and unconditional
jump:

l0:
i = 0;
goto l3;
l1:
int c1 = a[i] == b;
if (c1) {
goto l4;
} else {
goto l2;
}
l2:
i++;
l3:
int c2 = i < n;
if (c2) {
goto l1;
} else {
goto l4;
}
l4:
;

Note, I introduced more jumps than strictly necessary, so that
hunks between labels end either in conditional or unconditional
jump.

Next, replace each hunk staring in a label, up to (but not
including) next label, by a new function. Replace final jumps
by function calls, for conditional jumps using the same trick
as in previous 'silly' example:

int n;
int * a;
int b;
int i;

void l2(void);
void l3(void);
void l4(void);

void l0(void) {
i = 0;
l3();
}

void l1(void) {
void (*(t[2]))(void) = {l4, l2};
int c1 = a[i] == b;
(*(t[c1]))();
}

void l2(void) {
i++;
l3();
}

void l3(void) {
void (*(t[]))(void) = {l1, l4};
int c2 = i < n;
(*(t[c2]))();
}

void l4(void) {
}

Note: 'l4' is different than other functions, intead of calling
something it returns, ensuring that the sequence of calls
eventually terminate.

I hope that principles are clear now. If you compile this
with gcc at -O2 you will see that there are no calls
in generated code, only jumps. Slightly better code is
generated by clang. Note that generated code uses stack
only for final return.

BTW: you can see that currently tcc do not support this
coding style, that is code generated by tcc dully performs
all calls leading possibly to stack overflow and to
lower performance. Code generated by tcc from "jumpy"
version looks slightly worse than code generated by
clang from version using calls.
--
Waldek Hebisch
bart
2024-12-18 01:24:42 UTC
Reply
Permalink
Post by Waldek Hebisch
Post by bart
Post by Waldek Hebisch
Post by bart
If you try to extract any meaning, it is that any control flow can be
expressed either with 'goto' or with 'recursive functions'.
This is what I picked up on. Who on earth would eschew 'goto' and use
such a disproportionately more complex and inefficient method like
recursive functions?
Due to silly conding standard? Or in language that does not have
'goto'.
It was suggested that 'theoretically', 'goto' could be replaced by
recursive function calls.
Whether still within the context of a language with no other control
flow instructions, is not known. The suggester also chose not to share
examples of how it would work.
The example I gave (and you snipped) was supposed to explain how
the technique works, but it seems that it is not enough.
It showed how to do conditional code without explicit branching. It
didn't seem to me to cover arbitrary gotos, or where recursion comes
into it.

(Actually I implemented it in my two languages to compare performance to
'straight' versions, however my test called silly() lots of times so it
wasn't a good test.)
Post by Waldek Hebisch
So
let us look at another example. Start from ordinary C code that
only uses global variables (this is not strictly necessary, but
int n;
int * a;
int b;
int i;
...
/* Simple search loop */
for(i = 0; i < n; i++) {
if (a[i] == b) {
break;
}
}
First, express flow control using only conditional and unconditional
i = 0;
goto l3;
int c1 = a[i] == b;
if (c1) {
goto l4;
} else {
goto l2;
}
i++;
int c2 = i < n;
if (c2) {
goto l1;
} else {
goto l4;
}
;
Note, I introduced more jumps than strictly necessary, so that
hunks between labels end either in conditional or unconditional
jump.
Next, replace each hunk staring in a label, up to (but not
including) next label, by a new function. Replace final jumps
by function calls, for conditional jumps using the same trick
int n;
int * a;
int b;
int i;
void l2(void);
void l3(void);
void l4(void);
void l0(void) {
i = 0;
l3();
}
void l1(void) {
void (*(t[2]))(void) = {l4, l2};
int c1 = a[i] == b;
(*(t[c1]))();
}
void l2(void) {
i++;
l3();
}
void l3(void) {
void (*(t[]))(void) = {l1, l4};
int c2 = i < n;
(*(t[c2]))();
}
void l4(void) {
}
Note: 'l4' is different than other functions, intead of calling
something it returns, ensuring that the sequence of calls
eventually terminate.
OK thanks for this. I tried to duplicate it based on this starting point:

#include <stdio.h>

int n=6;
int a[]={10,20,30,40,50,60};
int b=30;
int i;

int main(void) {
for(i = 0; i < n; i++) {
printf("%d\n",a[i]);
if (a[i] == b) {
break;
}
}
}

This prints 10 20 30 as it is. But the version with the function calls
showed only '10'. If I swapped '{l1, l4}' in l3(), then I got '10 10 20'.

I didn't spend too long to debug it further. I will take your word that
this works. (I tried 3 compilers all with the same results, including TCC.)

I don't fully understand it; what I got was that you first produce
linear code with labels. Each span between labels is turned into a
function. To 'step into' label L, or jump to L, I have to do L().

There would still be lots of questions (even ignoring the problems of
accessing locals), like what the return path is, or how an early return
would work (also returning a value). Or what kind of pressure the stack
would be under.

It looks like a crude form of threaded code (which, when I use that,
never returns, and it doesn't use a stack either).

I've seen enough to know that it would be last kind of IL I would choose
(unless it was the last IL left in the world - then maybe).

There is also the oddity that eliminating a simple kind of branching
relies on more elaborate branching: call and return mechanisms.

More interesting and more practical would be replacing call/return by
'goto'! (It would need to support label pointers or indirect jumps,
unless runtime code modification was allowed.)


(my test)
--------------------------
#include <stdio.h>

int n=6;
int a[]={10,20,30,40,50,60};
int b=30;
int i;

void k2(void);
void k3(void);
void k4(void);

void k0(void) {
i = 0;
k3();
}

void k1(void) {
void (*(t[2]))(void) = {k4, k2};
printf("%d\n",a[i]);
int c1 = a[i] == b;
(*(t[c1]))();
}

void k2(void) {
i++;
// k3();
}

void k3(void) {
void (*(t[]))(void) = {k4, k1};
int c2 = i < n;
(*(t[c2]))();
}

void k4(void) {
}

int main(void) {
k0();
k1();
k2();
k3();
k4();
}
Waldek Hebisch
2024-12-18 03:51:17 UTC
Reply
Permalink
Post by bart
Post by Waldek Hebisch
Post by bart
Post by Waldek Hebisch
Post by bart
If you try to extract any meaning, it is that any control flow can be
expressed either with 'goto' or with 'recursive functions'.
This is what I picked up on. Who on earth would eschew 'goto' and use
such a disproportionately more complex and inefficient method like
recursive functions?
Due to silly conding standard? Or in language that does not have
'goto'.
It was suggested that 'theoretically', 'goto' could be replaced by
recursive function calls.
Whether still within the context of a language with no other control
flow instructions, is not known. The suggester also chose not to share
examples of how it would work.
The example I gave (and you snipped) was supposed to explain how
the technique works, but it seems that it is not enough.
It showed how to do conditional code without explicit branching. It
didn't seem to me to cover arbitrary gotos, or where recursion comes
into it.
(Actually I implemented it in my two languages to compare performance to
'straight' versions, however my test called silly() lots of times so it
wasn't a good test.)
Post by Waldek Hebisch
So
let us look at another example. Start from ordinary C code that
only uses global variables (this is not strictly necessary, but
int n;
int * a;
int b;
int i;
...
/* Simple search loop */
for(i = 0; i < n; i++) {
if (a[i] == b) {
break;
}
}
First, express flow control using only conditional and unconditional
i = 0;
goto l3;
int c1 = a[i] == b;
if (c1) {
goto l4;
} else {
goto l2;
}
i++;
int c2 = i < n;
if (c2) {
goto l1;
} else {
goto l4;
}
;
Note, I introduced more jumps than strictly necessary, so that
hunks between labels end either in conditional or unconditional
jump.
Next, replace each hunk staring in a label, up to (but not
including) next label, by a new function. Replace final jumps
by function calls, for conditional jumps using the same trick
int n;
int * a;
int b;
int i;
void l2(void);
void l3(void);
void l4(void);
void l0(void) {
i = 0;
l3();
}
void l1(void) {
void (*(t[2]))(void) = {l4, l2};
^^^^^^^
Should be
l2, l4
Post by bart
Post by Waldek Hebisch
int c1 = a[i] == b;
(*(t[c1]))();
}
void l2(void) {
i++;
l3();
}
void l3(void) {
void (*(t[]))(void) = {l1, l4};
^^^^^^
l4, l2
Post by bart
Post by Waldek Hebisch
int c2 = i < n;
(*(t[c2]))();
}
void l4(void) {
}
Note: 'l4' is different than other functions, intead of calling
something it returns, ensuring that the sequence of calls
eventually terminate.
#include <stdio.h>
int n=6;
int a[]={10,20,30,40,50,60};
int b=30;
int i;
int main(void) {
for(i = 0; i < n; i++) {
printf("%d\n",a[i]);
if (a[i] == b) {
break;
}
}
}
This prints 10 20 30 as it is. But the version with the function calls
showed only '10'. If I swapped '{l1, l4}' in l3(), then I got '10 10 20'.
Sorry, there was a thinko: 1 is true and this is the second element
of the array, while I was thinking that the first one is true branch
and second is false branch.
Post by bart
I didn't spend too long to debug it further. I will take your word that
this works. (I tried 3 compilers all with the same results, including TCC.)
I don't fully understand it; what I got was that you first produce
linear code with labels. Each span between labels is turned into a
function. To 'step into' label L, or jump to L, I have to do L().
Yes.
Post by bart
There would still be lots of questions (even ignoring the problems of
accessing locals), like what the return path is, or how an early return
would work (also returning a value). Or what kind of pressure the stack
would be under.
OK, you take a function F, it has some arguments and local variables.
And some retrun type. You create "entry function" to take the
same arguments as F and has the same return type as F. You tranform
body as above, but now each new function has the same return type
as F and arguments are arguments of original function + extra arguments,
one for each local variable of F. In "entry function" you call
function corresponding to first label passing it arguments and
initial values of local variables of F. In each subseqent call
you pass argument and values around so that they are available
in each new function. And the call is an argument to return
statement. When you want to return you simply return value,
without performing a call.

Stack use depend on optimizations in your compiler. With small
effort compiler can recognize that it will return value (possibly
void) from a call and replace such call by stack+register
shuffling + jump. Actually when there is return value, you
have something like

return lx(a0, a1, ..., ak);

which is easy to recognize due to 'return' keyword. One also
need to check that types agree (C automatically applies integer
convertions, but such convertions may produce real code, so in
such case one needs normal call). In void case one need to
check that there the call is textually last thing or that
it is followed by return statement. Stack+register
shuffling may require some code before control transfer, but
call can be replaced by jump.

So, if compiler has tail call optimization, then there is no
more stack use than maximum needed by any of the functions.

Note: I described general transformation, partially to show
that 'if' is _not_ needed. But similar style is used to
write code by hand. In hand written code people do not
bother with transforming 'if', which makes tail call
optimization a bit more complicated. OTOH, unlike rather
ugly code produced by mechanical transformation, hand
written code depending on tail call optimization may be quite
nice and readible. There is potential trouble: sometimes
author thinks that a call is a tail call, but compiler
disagrees, leading to lower efficiency.

Of course, when compiler do not have tail call optimization,
then stack use may be quite high.
Post by bart
It looks like a crude form of threaded code (which, when I use that,
never returns, and it doesn't use a stack either).
IMO it is quite different than what I know as threaded code.
Post by bart
I've seen enough to know that it would be last kind of IL I would choose
(unless it was the last IL left in the world - then maybe).
There is also the oddity that eliminating a simple kind of branching
relies on more elaborate branching: call and return mechanisms.
One motivation for eliminating 'goto' is that it is not easy to
say what effect 'goto' has on variables. I mean, variables keep
ther values, but when you may arrive to given point from several
places than values of variables depend on place that control came
from, and this may be hard to analyze. In a sense functions have
the same problem, but there is well-developed technique to reason
about function calls. So both jumps and function calls are
hard to analyze, but eliminating jumps allows re-use of work
done for functions.
Post by bart
More interesting and more practical would be replacing call/return by
'goto'! (It would need to support label pointers or indirect jumps,
unless runtime code modification was allowed.)
The point is that calls are strictly more powerful than jumps
(you get parameter passing and local variables).
--
Waldek Hebisch
Janis Papanagnou
2024-12-18 16:26:49 UTC
Reply
Permalink
Post by Waldek Hebisch
[...]
[ ponderings about where recursive functions might be used ]
Post by Waldek Hebisch
Due to silly conding standard? Or in language that does not have
'goto'.
(I'd rule out the "coding standards" hypothesis.)

Languages without 'goto', I suppose, would either have other control
constructs ('while', etc.) to formulate in an imperative style, or
be of the Functional Programming Languages type.

Janis
Keith Thompson
2024-12-17 20:13:15 UTC
Reply
Permalink
Post by bart
Post by Keith Thompson
[SNIP]
Post by bart
In that case I've no idea what you were trying to say.
When somebody says that 'goto' can emulate any control structure, then
clearly some of them need to be conditional; that is implied.
Your reply suggested they you can do away with 'goto', and use
recursive functions, in a scenario where no other control structures
need exist.
OK, if this is not for an IL, then it's not a language I would care
for either. Why tie one hand behind your back for no good reason?
I read Janis's post. I saw a suggestion that certain constructs are
*theoretically* unnecessary. I saw no suggestion of any advocacy for
such an approach.
"""
A 'goto' may be used but it isn't strictly *necessary*. What *is*
necessary, though, that is an 'if' (some conditional branch), and
either 'goto' or recursive functions.
"""
This doesn't actually make much sense. So 'goto' is necessary, but
'goto' *is*?
I presume you didn't write what you intended to write. Responding to
what I *think* you meant :

Either
"if" and "goto"
or
"if" and recursive functions
are theoretically sufficient to express certain kinds of algorithms
(I'm handwaving a bit). Which implies that "goto" is not strictly
necessary. It also implies that recursive functions are not strictly
necessary if you have "goto".

Since this is comp.lang.c, not comp.theory (or what comp.theory was
intended to be), I'm not going to go into the details, nor am I going to
take the time to express the concept in mathematically rigorous terms.
Post by bart
If you try to extract any meaning, it is that any control flow can be
expressed either with 'goto' or with 'recursive functions'.
Yes, either of those plus "if". It appears you understand the point.
Post by bart
This is what I picked up on. Who on earth would eschew 'goto' and use
such a disproportionately more complex and inefficient method like
recursive functions?
Perhaps it wasn't clear initially, but it should be by now,
that Janis was talking about what's theoretically sufficient to
express general algorithms. You seized on the silly idea that
Janis was *advocating* the use of one of the two minimal methods in
an intermediate language for a compiler. The idea Janis brought
up (briefly, in passing) is about theoretical computer science,
not practical software engineering. (Janis, please correct me if
I'm mistaken.)

Repeatedly asking why anyone would do such a thing misses the point.

[...]
--
Keith Thompson (The_Other_Keith) Keith.S.Thompson+***@gmail.com
void Void(void) { Void(); } /* The recursive call of the void */
Janis Papanagnou
2024-12-18 16:19:01 UTC
Reply
Permalink
[...]
[...]
(Janis, please correct me if I'm mistaken.)
I think it couldn't have been explained clearer. - Thanks.

Janis
Janis Papanagnou
2024-12-17 17:29:37 UTC
Reply
Permalink
Post by bart
Post by Janis Papanagnou
I wasn't commenting on any "IL",
The subthread was about ILs.
You obviously missed that I wasn't quoting anything else from this
subthread but the one isolated statement I replied to (which also
wasn't a part of a larger paragraph but standing alone on its own).

(That's why I think it's good style to strip to the essentials one
intends to reply to and don't assume (or refer) to any unspoken or
unquoted parts of a thread; keep posts self-contained! YMMV. That
will also keep potential confusions and misunderstandings small.)

Janis
Tim Rentsch
2024-12-21 01:28:29 UTC
Reply
Permalink
Post by Janis Papanagnou
Post by BGB
[...]
Pretty much all higher level control flow can be expressed via goto.
A 'goto' may be used but it isn't strictly *necessary*. What *is*
necessary, though, that is an 'if' (some conditional branch), and
either 'goto' or recursive functions.
Conditional branches, including 'if', '?:', etc., are not strictly
necessary either.
Janis Papanagnou
2024-12-21 20:31:24 UTC
Reply
Permalink
Post by Tim Rentsch
Post by Janis Papanagnou
Post by BGB
[...]
Pretty much all higher level control flow can be expressed via goto.
A 'goto' may be used but it isn't strictly *necessary*. What *is*
necessary, though, that is an 'if' (some conditional branch), and
either 'goto' or recursive functions.
Conditional branches, including 'if', '?:', etc., are not strictly
necessary either.
No? - Can you give an example of your statement?

(Unless you just wanted to say that in some HLL abstraction like
'printf("Hello world!\n")' there's no [visible] conditional branch.
Likewise in a 'ClearAccumulator' machine instruction, or the like.)

The comparisons and predicates are one key function (not any specific
branch construct, whether on HLL level, assembler level, or with the
(elementary but most powerful) Turing Machine). Comparisons inherently
result in predicates which is what controls program execution).

So your statement asks for some explanation at least.

Janis
Tim Rentsch
2024-12-21 21:51:27 UTC
Reply
Permalink
Post by Janis Papanagnou
Post by Tim Rentsch
Post by Janis Papanagnou
Post by BGB
[...]
Pretty much all higher level control flow can be expressed via goto.
A 'goto' may be used but it isn't strictly *necessary*. What *is*
necessary, though, that is an 'if' (some conditional branch), and
either 'goto' or recursive functions.
Conditional branches, including 'if', '?:', etc., are not strictly
necessary either.
No? - Can you give an example of your statement?
(Unless you just wanted to say that in some HLL abstraction like
'printf("Hello world!\n")' there's no [visible] conditional branch.
Likewise in a 'ClearAccumulator' machine instruction, or the like.)
The comparisons and predicates are one key function (not any specific
branch construct, whether on HLL level, assembler level, or with the
(elementary but most powerful) Turing Machine). Comparisons inherently
result in predicates which is what controls program execution).
So your statement asks for some explanation at least.
Start with C - any of C90, C99, C11.

Take away the short-circuiting operators - &&, ||, ?:.

Take away all statement types that involve intra-function transfer
of control: goto, break, continue, if, for, while, switch, do/while.
Might as well take away statement labels too.

Take away setjmp and longjmp.

Rule out programs with undefined behavior.

The language that is left is still Turing complete.

Proof: exercise for the reader.
Janis Papanagnou
2024-12-22 00:22:01 UTC
Reply
Permalink
Post by Tim Rentsch
Post by Janis Papanagnou
Post by Tim Rentsch
Post by Janis Papanagnou
Post by BGB
[...]
Pretty much all higher level control flow can be expressed via goto.
A 'goto' may be used but it isn't strictly *necessary*. What *is*
necessary, though, that is an 'if' (some conditional branch), and
either 'goto' or recursive functions.
Conditional branches, including 'if', '?:', etc., are not strictly
necessary either.
No? - Can you give an example of your statement?
(Unless you just wanted to say that in some HLL abstraction like
'printf("Hello world!\n")' there's no [visible] conditional branch.
Likewise in a 'ClearAccumulator' machine instruction, or the like.)
The comparisons and predicates are one key function (not any specific
branch construct, whether on HLL level, assembler level, or with the
(elementary but most powerful) Turing Machine). Comparisons inherently
result in predicates which is what controls program execution).
So your statement asks for some explanation at least.
Start with C - any of C90, C99, C11.
Take away the short-circuiting operators - &&, ||, ?:.
Take away all statement types that involve intra-function transfer
of control: goto, break, continue, if, for, while, switch, do/while.
Might as well take away statement labels too.
Take away setjmp and longjmp.
And also things like the above mentioned 'printf()' that most certainly
implies an iteration over the format string checking for it's '\0'-end.
And so on, and so on. - What will be left as "language".

Would you be able to formulate functionality of the class of Recursive
Functions (languages class of a Turing Machine with Chomsky-0 grammar).
Post by Tim Rentsch
Rule out programs with undefined behavior.
The language that is left is still Turing complete.
Is it? - But wouldn't that be just the argument I mentioned above; that
a, say, 'ClearAccumulator' machine statement wouldn't contain any jump?
Post by Tim Rentsch
Proof: exercise for the reader.
(Typical sort of your reply.)

Janis
Michael S
2024-12-21 22:20:32 UTC
Reply
Permalink
On Sat, 21 Dec 2024 21:31:24 +0100
Post by Janis Papanagnou
So your statement asks for some explanation at least.
Janis
I would guess that Tim worked as CS professor for several dozens years.
And it shows.
Janis Papanagnou
2024-12-22 00:13:07 UTC
Reply
Permalink
Post by Michael S
On Sat, 21 Dec 2024 21:31:24 +0100
Post by Janis Papanagnou
So your statement asks for some explanation at least.
I would guess that Tim worked as CS professor for several dozens years.
And it shows.
Ranks and titles are, per se, no guarantee. I'm not impressed; I've
seen all sorts/qualities of professors. YMMV.

If that is true (that he was one) I'm wondering why we observe so
often that he posts statements here and doesn't care to explain it.
At least the many _good_ professors I met in my life typically were
keen to explain their theses, statements, or knowledge (instead of
dragging that out of him).

Janis
Michael S
2024-12-22 00:18:51 UTC
Reply
Permalink
On Sun, 22 Dec 2024 01:13:07 +0100
Post by Janis Papanagnou
Post by Michael S
On Sat, 21 Dec 2024 21:31:24 +0100
Post by Janis Papanagnou
So your statement asks for some explanation at least.
I would guess that Tim worked as CS professor for several dozens
years. And it shows.
Ranks and titles are, per se, no guarantee. I'm not impressed; I've
seen all sorts/qualities of professors. YMMV.
If that is true (that he was one) I'm wondering why we observe so
often that he posts statements here and doesn't care to explain it.
At least the many _good_ professors I met in my life typically were
keen to explain their theses, statements, or knowledge (instead of
dragging that out of him).
Janis
It seems, you didn't understand me. (Ogh, it is contagious ;-)
Janis Papanagnou
2024-12-22 00:39:49 UTC
Reply
Permalink
Post by Michael S
On Sun, 22 Dec 2024 01:13:07 +0100
Post by Janis Papanagnou
Post by Michael S
On Sat, 21 Dec 2024 21:31:24 +0100
Post by Janis Papanagnou
So your statement asks for some explanation at least.
I would guess that Tim worked as CS professor for several dozens
years. And it shows.
Ranks and titles are, per se, no guarantee. I'm not impressed; I've
seen all sorts/qualities of professors. YMMV.
If that is true (that he was one) I'm wondering why we observe so
often that he posts statements here and doesn't care to explain it.
At least the many _good_ professors I met in my life typically were
keen to explain their theses, statements, or knowledge (instead of
dragging that out of him).
It seems, you didn't understand me. (Ogh, it is contagious ;-)
I'm sorry, no. - I certainly took it literally - as I do (at first)
with most people and their statements (until I get to know better).

If it was meant sarcastically or anything, I'd appreciate a smiley
or something like that. (It certainly wasn't obvious to me.)

If it was meant serious and I completely missed the point - which
may also happen occasionally - I'd appreciate a pointer.

Janis
Michael S
2024-12-22 01:04:51 UTC
Reply
Permalink
On Sun, 22 Dec 2024 01:39:49 +0100
Post by Janis Papanagnou
Post by Michael S
On Sun, 22 Dec 2024 01:13:07 +0100
Post by Janis Papanagnou
Post by Michael S
On Sat, 21 Dec 2024 21:31:24 +0100
Post by Janis Papanagnou
So your statement asks for some explanation at least.
I would guess that Tim worked as CS professor for several dozens
years. And it shows.
Ranks and titles are, per se, no guarantee. I'm not impressed; I've
seen all sorts/qualities of professors. YMMV.
If that is true (that he was one) I'm wondering why we observe so
often that he posts statements here and doesn't care to explain it.
At least the many _good_ professors I met in my life typically were
keen to explain their theses, statements, or knowledge (instead of
dragging that out of him).
It seems, you didn't understand me. (Ogh, it is contagious ;-)
I'm sorry, no. - I certainly took it literally - as I do (at first)
with most people and their statements (until I get to know better).
If it was meant sarcastically or anything, I'd appreciate a smiley
or something like that. (It certainly wasn't obvious to me.)
If it was meant serious and I completely missed the point - which
may also happen occasionally - I'd appreciate a pointer.
Janis
Part of the answer is in your previous response.
You wrote: "many _good_ professors I met in my life typically were
keen to explain their theses, statements, or knowledge (instead of
dragging that out of him)". You essentially admitted that not all good
professors behave like that.

There is more than one school of teaching. One school believes that
students learn from explanations and exercises. Other school believes
that students learn best when provided with bare basics and then asked
to figure out the rest by themselves. There is also the third school
that believes that student don't really learn anything before they try
to explain it to somebody else.

You make an impression of one that received basics of CS. Probably, 40
or so years ago, but still you have to know basic facts. Unlike me, for
example.
So, Tim expects that you will be able to utilizes his hints. And that
it would lead to much better understanding on your part then if he
feeds you by teaspoon.
That is one part. Another part is that he is annoyed by your tone.
Janis Papanagnou
2024-12-22 02:06:54 UTC
Reply
Permalink
Post by Michael S
[...]
Part of the answer is in your previous response.
You wrote: "many _good_ professors I met in my life typically were
keen to explain their theses, statements, or knowledge (instead of
dragging that out of him)". You essentially admitted that not all good
professors behave like that.
Oh, what I meant to express was different; that good professors
*would* explain it (only bad ones wouldn't).

(At least that was my experience; and not only covering the CS
domain, BTW.)
Post by Michael S
[ "schools of teaching" stuff snipped ]
You make an impression of one that received basics of CS. Probably, 40
or so years ago, but still you have to know basic facts. Unlike me, for
example.
So, Tim expects that you will be able to utilizes his hints.
The point [repeatedly] stated (also by others here) was that
he more often than not just provides no information but simple
arbitrary statements of opinion.

*Especially* if folks here that are discussing CS stuff have 40
or 50 years experience, as you say, with academical and practical
background one would think that a non-substantial "kindergarten"
statement is then effectively just an offense (or likely part of
a arrogant [professorial?] behavior).
Post by Michael S
And that
it would lead to much better understanding on your part then if he
feeds you by teaspoon.
Which he doesn't do.

Moreover, given that many of the folks here obviously *do* have
a solid background (or at least years long IT or CS experiences)
should, IMO, be a motivation to try to explain any arguable point
if one really cares about the topic. (Unless some habit, maybe of
being an inerrant authority, prevents one from such.)

Myself I'm at least trying to explain knowledges and backup by
experiences, not just throw short phrases into the pool.
Post by Michael S
That is one part. Another part is that he is annoyed by your tone.
(And I'm annoyed by his. But, anyway, his posting tone is that
same in most of his responses to folks here, not just to me.)

Janis
Tim Rentsch
2024-12-23 01:39:52 UTC
Reply
Permalink
Post by Michael S
[...]
Part of the answer is in your previous response.
You wrote: "many _good_ professors I met in my life typically were
keen to explain their theses, statements, or knowledge (instead of
dragging that out of him)". You essentially admitted that not all good
professors behave like that.
Oh, what I meant to express was different; that good professors
*would* explain it (only bad ones wouldn't).
(At least that was my experience; and not only covering the CS
domain, BTW.)
Post by Michael S
[ "schools of teaching" stuff snipped ]
You make an impression of one that received basics of CS. Probably, 40
or so years ago, but still you have to know basic facts. Unlike me, for
example.
So, Tim expects that you will be able to utilizes his hints.
The point [repeatedly] stated (also by others here) was that
he more often than not just provides no information but simple
arbitrary statements of opinion.
The comments I made here, in two responses to postings of yours,
were not statements of opinion but statements of fact. They are
no more statements of opinion than a statement about whether the
Riemann Hypothesis is true is a statement of opinion. Someone
might wonder whether an assertion "The Riemann Hypothesis is
true" is true or false, but it is still a matter of fact, not a
matter of opinion.
James Kuyper
2024-12-22 03:17:20 UTC
Reply
Permalink
On 12/21/24 20:04, Michael S wrote:
...
Post by Michael S
There is more than one school of teaching. One school believes that
students learn from explanations and exercises. Other school believes
that students learn best when provided with bare basics and then asked
to figure out the rest by themselves.
I personally believe that Tim generally thinks there's a justification
for what he says, and that we'd be better off figuring it out ourselves.
I also know, from the rare occasions when he's been convinced to provide
his justification, that I often don't consider his justification valid.
However, he says things that seem to be unjustified so often, I can't
help wondering if he doesn't occasionally say things he realizes are
unjustified (either at the time, or as the result of subsequent
discussion), and withholds his justifications in order to hide the fact
that he knows he was wrong. Probably not, but I keep wondering.
Janis Papanagnou
2024-12-22 18:51:13 UTC
Reply
Permalink
Post by James Kuyper
...
Post by Michael S
There is more than one school of teaching. One school believes that
students learn from explanations and exercises. Other school believes
that students learn best when provided with bare basics and then asked
to figure out the rest by themselves.
In context of this newsgroup where my impression is that there's a lot
of years long IT/CS experienced and quite old people discussing topics
the explanatory "model" of "schools of teaching" is anyway completely
inappropriate; there's not "one _teacher_ [who knows almost all]" and
"all the rest are [ignorant] _pupils_" that need to be "guided" (in
one way or the other). Not saying anything substantial on a topic can
certainly be perceived as some rhetorical move but it's surely not any
sort of teaching-didactics [of whatever "school of teaching"]).
Post by James Kuyper
I personally believe that Tim generally thinks there's a justification
for what he says, and that we'd be better off figuring it out ourselves.
(My impression is that he often says something on a topic where he has
no deeper knowledge, but is pretending to know by not saying anything
substantial.)
Post by James Kuyper
I also know, from the rare occasions when he's been convinced to provide
his justification, that I often don't consider his justification valid.
However, he says things that seem to be unjustified so often, I can't
help wondering if he doesn't occasionally say things he realizes are
unjustified (either at the time, or as the result of subsequent
discussion), and withholds his justifications in order to hide the fact
that he knows he was wrong. Probably not, but I keep wondering.
(This matches with my observations and I drew a similar conclusion.)

Janis
Waldek Hebisch
2024-12-22 06:01:52 UTC
Reply
Permalink
Post by Janis Papanagnou
Post by Tim Rentsch
Post by Janis Papanagnou
Post by BGB
[...]
Pretty much all higher level control flow can be expressed via goto.
A 'goto' may be used but it isn't strictly *necessary*. What *is*
necessary, though, that is an 'if' (some conditional branch), and
either 'goto' or recursive functions.
Conditional branches, including 'if', '?:', etc., are not strictly
necessary either.
No? - Can you give an example of your statement?
Look at example that I posted (apparently neither you nor Tim
looked at my posts where I explained in detail how to translate
goto program (with conditional jumps) into program that contains
no goto and no conditional jumps).

Or try to figure out how to do this knowing that C has function
pointers.
--
Waldek Hebisch
Michael S
2024-12-22 09:22:51 UTC
Reply
Permalink
On Sun, 22 Dec 2024 06:01:52 -0000 (UTC)
Post by Waldek Hebisch
Post by Janis Papanagnou
Post by Tim Rentsch
Post by Janis Papanagnou
Post by BGB
[...]
Pretty much all higher level control flow can be expressed via goto.
A 'goto' may be used but it isn't strictly *necessary*. What *is*
necessary, though, that is an 'if' (some conditional branch), and
either 'goto' or recursive functions.
Conditional branches, including 'if', '?:', etc., are not strictly
necessary either.
No? - Can you give an example of your statement?
Look at example that I posted (apparently neither you nor Tim
looked at my posts where I explained in detail how to translate
goto program (with conditional jumps) into program that contains
no goto and no conditional jumps).
Considering that Janis replied to your post I find a possibility that
he did not look at it unlikely. Although not completely impossible.
Post by Waldek Hebisch
Or try to figure out how to do this knowing that C has function
pointers.
bart
2024-12-22 11:35:53 UTC
Reply
Permalink
Post by Michael S
On Sun, 22 Dec 2024 06:01:52 -0000 (UTC)
Post by Waldek Hebisch
Post by Janis Papanagnou
Post by Tim Rentsch
Post by Janis Papanagnou
Post by BGB
[...]
Pretty much all higher level control flow can be expressed via goto.
A 'goto' may be used but it isn't strictly *necessary*. What *is*
necessary, though, that is an 'if' (some conditional branch), and
either 'goto' or recursive functions.
Conditional branches, including 'if', '?:', etc., are not strictly
necessary either.
No? - Can you give an example of your statement?
Look at example that I posted (apparently neither you nor Tim
looked at my posts where I explained in detail how to translate
goto program (with conditional jumps) into program that contains
no goto and no conditional jumps).
Considering that Janis replied to your post I find a possibility that
he did not look at it unlikely. Although not completely impossible.
He only replied to the first remark. And summarised the rest with:

"[ ponderings about where recursive functions might be used ]"

(18-Dec, 16:26 GMT)

I don't think JP does details, and I've struggled to find posts where he
writes actual code. His replies to mine have mostly been about trying to
beat me over the head.
Tim Rentsch
2024-12-22 18:38:11 UTC
Reply
Permalink
Post by Waldek Hebisch
Post by Janis Papanagnou
Post by Tim Rentsch
Post by Janis Papanagnou
Post by BGB
[...]
Pretty much all higher level control flow can be expressed via goto.
A 'goto' may be used but it isn't strictly *necessary*. What *is*
necessary, though, that is an 'if' (some conditional branch), and
either 'goto' or recursive functions.
Conditional branches, including 'if', '?:', etc., are not strictly
necessary either.
No? - Can you give an example of your statement?
Look at example that I posted (apparently neither you nor Tim
looked at my posts [...]
What makes you think I didn't?
Waldek Hebisch
2024-12-22 19:44:49 UTC
Reply
Permalink
Post by Tim Rentsch
Post by Waldek Hebisch
Post by Janis Papanagnou
Post by Tim Rentsch
Post by Janis Papanagnou
Post by BGB
[...]
Pretty much all higher level control flow can be expressed via goto.
A 'goto' may be used but it isn't strictly *necessary*. What *is*
necessary, though, that is an 'if' (some conditional branch), and
either 'goto' or recursive functions.
Conditional branches, including 'if', '?:', etc., are not strictly
necessary either.
No? - Can you give an example of your statement?
Look at example that I posted (apparently neither you nor Tim
looked at my posts [...]
What makes you think I didn't?
I made the same claim as you earlier and gave examples. You
did not acknowledge my posts. Why? For me most natural
explanation is that you did not read them.
--
Waldek Hebisch
Janis Papanagnou
2024-12-22 19:41:44 UTC
Reply
Permalink
Post by Waldek Hebisch
Post by Janis Papanagnou
Post by Tim Rentsch
Post by Janis Papanagnou
Post by BGB
[...]
Pretty much all higher level control flow can be expressed via goto.
A 'goto' may be used but it isn't strictly *necessary*. What *is*
necessary, though, that is an 'if' (some conditional branch), and
either 'goto' or recursive functions.
Conditional branches, including 'if', '?:', etc., are not strictly
necessary either.
No? - Can you give an example of your statement?
Look at example that I posted (apparently neither you nor Tim
looked at my posts where I explained in detail how to translate
goto program (with conditional jumps) into program that contains
no goto and no conditional jumps).
I'm not sure but may have just skimmed over your "C" example if it
wasn't of interest to the point I tried to make (at that stage).
Post by Waldek Hebisch
Or try to figure out how to do this knowing that C has function
pointers.
I will retry to explain what I tried to say... - very simply put...

There's "Recursive Functions" and the Turing Machines "equivalent".
The "Recursive Functions" is the most powerful class of algorithms.
Formal Recursive Functions are formally defined in terms of abstract
mathematical formulated properties; one of these [three properties]
are the "Test Sets". (Here I can already stop.)

But since we're not in a theoretical CS newsgroup I'd just wanted
to see an example of some common, say, mathematical function and
see it implemented without 'if' and 'goto' or recursion. - Take a
simple one, say, fac(n) = n! , the factorial function. I know how
I can implement that with 'if' and recursion, and I know how I can
implement that with 'while' (or 'goto').

If I re-inspect your example upthread - I hope it was the one you
wanted to refer to - I see that you have removed the 'if' symbol
but not the conditional, the test function; there's still the
predicate (the "Test Set") present in form of 'int c2 = i < n',
and it's there in the original code, in the goto transformed code,
and in the function-pointer code. And you cannot get rid of that.

Whether you have the test in an 'if', or in a ternary '?:', or
use it through a bool-int coercion as integer index to an indexed
function[-pointer] table; it's a conditional branch based on the
("Test Set") predicate i<n. You showed in your example how to get
rid of the 'if' symbol, but you could - as expected - not get rid
of the actual test that is the substance of a conditional branch.

I think that is what is to expect by the theory and the essence of
the point I tried to make.

Janis
Michael S
2024-12-22 22:20:48 UTC
Reply
Permalink
On Sun, 22 Dec 2024 20:41:44 +0100
Post by Janis Papanagnou
Post by Waldek Hebisch
Post by Janis Papanagnou
Post by Tim Rentsch
Post by Janis Papanagnou
Post by BGB
[...]
Pretty much all higher level control flow can be expressed via goto.
A 'goto' may be used but it isn't strictly *necessary*. What *is*
necessary, though, that is an 'if' (some conditional branch), and
either 'goto' or recursive functions.
Conditional branches, including 'if', '?:', etc., are not strictly
necessary either.
No? - Can you give an example of your statement?
Look at example that I posted (apparently neither you nor Tim
looked at my posts where I explained in detail how to translate
goto program (with conditional jumps) into program that contains
no goto and no conditional jumps).
I'm not sure but may have just skimmed over your "C" example if it
wasn't of interest to the point I tried to make (at that stage).
Post by Waldek Hebisch
Or try to figure out how to do this knowing that C has function
pointers.
I will retry to explain what I tried to say... - very simply put...
There's "Recursive Functions" and the Turing Machines "equivalent".
The "Recursive Functions" is the most powerful class of algorithms.
Formal Recursive Functions are formally defined in terms of abstract
mathematical formulated properties; one of these [three properties]
are the "Test Sets". (Here I can already stop.)
But since we're not in a theoretical CS newsgroup I'd just wanted
to see an example of some common, say, mathematical function and
see it implemented without 'if' and 'goto' or recursion. - Take a
simple one, say, fac(n) = n! , the factorial function. I know how
I can implement that with 'if' and recursion, and I know how I can
implement that with 'while' (or 'goto').
If I re-inspect your example upthread - I hope it was the one you
wanted to refer to - I see that you have removed the 'if' symbol
but not the conditional, the test function; there's still the
predicate (the "Test Set") present in form of 'int c2 = i < n',
and it's there in the original code, in the goto transformed code,
and in the function-pointer code. And you cannot get rid of that.
Whether you have the test in an 'if', or in a ternary '?:', or
use it through a bool-int coercion as integer index to an indexed
function[-pointer] table; it's a conditional branch based on the
("Test Set") predicate i<n. You showed in your example how to get
rid of the 'if' symbol, but you could - as expected - not get rid
of the actual test that is the substance of a conditional branch.
I think that is what is to expect by the theory and the essence of
the point I tried to make.
Janis
You make no sense. I am starting to suspect that the reason for it
is ignorance rather than mere stubbornness.

https://godbolt.org/z/EKo5rrYce
Show me conditional branch in the right pane.
Waldek Hebisch
2024-12-22 23:29:50 UTC
Reply
Permalink
Post by Janis Papanagnou
Post by Waldek Hebisch
Post by Janis Papanagnou
Post by Tim Rentsch
Post by Janis Papanagnou
Post by BGB
[...]
Pretty much all higher level control flow can be expressed via goto.
A 'goto' may be used but it isn't strictly *necessary*. What *is*
necessary, though, that is an 'if' (some conditional branch), and
either 'goto' or recursive functions.
Conditional branches, including 'if', '?:', etc., are not strictly
necessary either.
No? - Can you give an example of your statement?
Look at example that I posted (apparently neither you nor Tim
looked at my posts where I explained in detail how to translate
goto program (with conditional jumps) into program that contains
no goto and no conditional jumps).
I'm not sure but may have just skimmed over your "C" example if it
wasn't of interest to the point I tried to make (at that stage).
Post by Waldek Hebisch
Or try to figure out how to do this knowing that C has function
pointers.
I will retry to explain what I tried to say... - very simply put...
There's "Recursive Functions" and the Turing Machines "equivalent".
The "Recursive Functions" is the most powerful class of algorithms.
Formal Recursive Functions are formally defined in terms of abstract
mathematical formulated properties; one of these [three properties]
are the "Test Sets". (Here I can already stop.)
Classic definition uses some number of base functions, some
number of base conditions, conditional definitions and
"minimum operator". "Minimum operator" given a (possibly
partially defined) function f and l computes smallest n such that
f(k, l) is defined for k=0,1,...,n and f(n, l) = 0 and is undefined
otherwise. Some texts require minimum to be effective, that
is f should be total and for each l there should be n >= 0 such
that f(n, l) = 0. Clearly "minimum operator" is equvalent to
'while' loop. IIRC, if instead of "minimum operator" you only
recursion, then resulting class of functions is strictly smaller.
So assuming that I remember correctly, in framework of recursive
functions claim that conditianals and recursion give Turing
completness is false, one needs some "programming" constructs.

Anyway, using recursion you clearly need some way to stop it. If you
restrict yourself to eagerly evaluated total integer valued functions
only, then clearly there is no way to stop recursion. But if
you have different system like lambda calculus or C, then there
are ways to stop recursion that are quite different than 'if'
or tertiary operator.
Post by Janis Papanagnou
But since we're not in a theoretical CS newsgroup I'd just wanted
to see an example of some common, say, mathematical function and
see it implemented without 'if' and 'goto' or recursion.
To be clear: I need recursion in general. I do not need 'if'
to stop recursion.
Post by Janis Papanagnou
- Take a
simple one, say, fac(n) = n! , the factorial function. I know how
I can implement that with 'if' and recursion, and I know how I can
implement that with 'while' (or 'goto').
If I re-inspect your example upthread - I hope it was the one you
wanted to refer to - I see that you have removed the 'if' symbol
but not the conditional, the test function; there's still the
predicate (the "Test Set") present in form of 'int c2 = i < n',
You failed to see that this is on ordinary total function: it
evaluates both arguments and produces a value. If I take the
following C function:

int lt(int a, int b) {
return (a < b);
}

and compile it using 'gcc -O -S' I get:

lt:
.LFB0:
.cfi_startproc
cmpl %esi, %edi
setl %al
movzbl %al, %eax
ret

As you can see the only control transfer there is 'ret' at the
end of the function. 'if' and C ternary oprators are quite
different, you can not implement them as ordinary functions
(some special case can be optimized to jumpless code, but not
in general).
Post by Janis Papanagnou
and it's there in the original code, in the goto transformed code,
and in the function-pointer code. And you cannot get rid of that.
I can, but for something like factorial code would be quite
ugly. One can implement reasonable Turing machine emulator
using just integer and function pointer arrays, array accesses
and assignments, direct and indirect funcction calls.

By reasonable I mean that as long as Turning machine stays
in part of tape modeled as C array emulator and Turing machine
would move in step. Stop in Turing machine would exit emulator.
Only when Turing machine exceeds memory of C program, C program
would exhibit undefined behaviour. If you allow yourself also
C arithmetic operators (crucualy '/' and '%'), then you can
stop execution.

If you assume C implementation with infinite memory such that
'malloc' newer fails, then instead of array you can use
doubly linked list which gets extended when Turing machine
tries to get outside allocated space.

IIUC such infinite C implementation would exhibit undefined
behaviour as C standard requires finite bound on integers and
injective cast from from pointers to some integer type.
Post by Janis Papanagnou
Whether you have the test in an 'if', or in a ternary '?:', or
use it through a bool-int coercion as integer index to an indexed
function[-pointer] table; it's a conditional branch based on the
("Test Set") predicate i<n. You showed in your example how to get
rid of the 'if' symbol, but you could - as expected - not get rid
of the actual test that is the substance of a conditional branch.
Wrong, one can use properties of C23 division (actually,
what is needed division and remainder by a fixed positive
number, say 3).
Post by Janis Papanagnou
I think that is what is to expect by the theory and the essence of
the point I tried to make.
One point that I wanted to make is that programming languages are
different than theory of integer functions, in particular programming
constructs may be surprisingly powerful. For example, there was
a theorem about some special concurrecy problem saying that
desired mutial exclusion can not be done by binary semaphores.
David Parnas showed that it in fact can be solved using arrays
of binary semaphores. The theorem had unstated assumption that
only scalar semaphore variables are in use. Of course, once you eliminate
all useful constructs from a language, then one can not do anything
is such a language (as a joke David Parnas defined such a language).

Second point was that function calls in tail position are quite similar
to goto, and in case of indirect calls they can do job of 'if' or 'switch'.
So if you consider elimination of 'if' (or 'goto') as a cheat, the
cheat is in using function calls, and not in predicates.
--
Waldek Hebisch
Ben Bacarisse
2024-12-22 14:19:13 UTC
Reply
Permalink
Post by Janis Papanagnou
Post by Tim Rentsch
Post by Janis Papanagnou
Post by BGB
[...]
Pretty much all higher level control flow can be expressed via goto.
A 'goto' may be used but it isn't strictly *necessary*. What *is*
necessary, though, that is an 'if' (some conditional branch), and
either 'goto' or recursive functions.
Conditional branches, including 'if', '?:', etc., are not strictly
necessary either.
No? - Can you give an example of your statement?
I don't want to speak for Tim, but as far as I am concerned, it all
boils down to what you take to be a model of (effective) computation.
In some purely theoretical sense, models like the pure lambda calculus
and combinator calculus are "complete" and they have no specific
conditional "branches".

Going into detail (such as examples of making a "choice" in pure lambda
calculus) are way off topic here.

This is exactly what comp.theory should be used for, so I will cross
post there and set the followup-to header. comp.theory has been trashed
by cranks but maybe a topical post will help it a but.
--
Ben.
Ben Bacarisse
2024-12-22 15:30:30 UTC
Reply
Permalink
Post by Ben Bacarisse
Post by Janis Papanagnou
Post by Tim Rentsch
Post by Janis Papanagnou
Post by BGB
[...]
Pretty much all higher level control flow can be expressed via goto.
A 'goto' may be used but it isn't strictly *necessary*. What *is*
necessary, though, that is an 'if' (some conditional branch), and
either 'goto' or recursive functions.
Conditional branches, including 'if', '?:', etc., are not strictly
necessary either.
No? - Can you give an example of your statement?
I don't want to speak for Tim, but as far as I am concerned, it all
boils down to what you take to be a model of (effective) computation.
In some purely theoretical sense, models like the pure lambda calculus
and combinator calculus are "complete" and they have no specific
conditional "branches".
Going into detail (such as examples of making a "choice" in pure lambda
calculus) are way off topic here.
This is exactly what comp.theory should be used for, so I will cross
post there and set the followup-to header. comp.theory has been trashed
by cranks but maybe a topical post will help it a but.
I see from a post I had not read before replying that Tim's point was
very much focused on C. Given that theory is off topic here (and
comp.theory is a mess) there is probably no point in trying to
discussing the more general point.
--
Ben.
Kaz Kylheku
2024-12-22 21:45:14 UTC
Reply
Permalink
Post by Janis Papanagnou
Post by Tim Rentsch
Post by Janis Papanagnou
Post by BGB
[...]
Pretty much all higher level control flow can be expressed via goto.
A 'goto' may be used but it isn't strictly *necessary*. What *is*
necessary, though, that is an 'if' (some conditional branch), and
either 'goto' or recursive functions.
Conditional branches, including 'if', '?:', etc., are not strictly
necessary either.
No? - Can you give an example of your statement?
In a functional langauge, we can make a decision by, for instance,
putting two lambdas into an array A, and then calling A[0] or A[1],
where the index 0 or 1 is comes from some Boolean result.

The only reason we have a control construct like if(A, X, Y) where X
is only evaluated if A is true, otherwise Y, is that X and Y
have side effects.

If X and Y don't have side effects, then if(A, X, Y) can be an ordinary
function whose arguments are strictly evaluated.

Moreover, if we give the functional language lazy evaluation semantics,
then anyway we get the behavior that Y is not evaluated if A is true,
and that lazy evaluation model can be used as the basis for sneaking
effects into the functional language and conctrolling them.

Anyway, Turing calculation by primitive recursion does not require
conditional branching. Just perhaps an if function which returns
either its second or third argument based on the truth value of
its first argument.

For instance, in certain C preprocessor tricks, conditional expansion
is achieved by such macros.

When we run the following through the GNU C preprocessor (e.g. by pasting
into gcc -E -x c -p -):

#define TRUE_SELECT_TRUE(X) X
#define TRUE_SELECT_FALSE(X)

#define FALSE_SELECT_TRUE(X)
#define FALSE_SELECT_FALSE(X) X

#define SELECT_TRUE(X) X
#define SELECT_FALSE(X)

#define PASTE(X, Y) X ## Y

#define IF(A, B, C) PASTE(TRUE_SELECT_, A)(B) PASTE(FALSE_SELECT_, A)(C)

#define FOO TRUE
#define BAR FALSE

IF(FOO, foo is true, foo is false)
IF(BAR, bar is true, bar is false)

We get these tokens:

foo is true
bar is false

Yet, macro expansion has no conditionals. The preprocessing language has
#if and #ifdef, but we didn't use those. Just expansion of computed names.

This is an example of not strictly needing conditionals to achieve
conditional evaluation or expansion: an IF(A, B, C) operator that
yields B or C depending on the truth of A, and so forth.

John MacCarthy (Lisp inventor) wrote himself such an IF function
in Fortran, in a program for calculating chess moves. It evaluated
both the B and C expressions, and so it wasn't a proper imperative
conditional, but it didn't matter.
--
TXR Programming Language: http://nongnu.org/txr
Cygnal: Cygwin Native Application Library: http://kylheku.com/cygnal
Mastodon: @***@mstdn.ca
bart
2024-12-22 23:22:36 UTC
Reply
Permalink
Post by Kaz Kylheku
Post by Janis Papanagnou
Post by Tim Rentsch
Post by Janis Papanagnou
Post by BGB
[...]
Pretty much all higher level control flow can be expressed via goto.
A 'goto' may be used but it isn't strictly *necessary*. What *is*
necessary, though, that is an 'if' (some conditional branch), and
either 'goto' or recursive functions.
Conditional branches, including 'if', '?:', etc., are not strictly
necessary either.
No? - Can you give an example of your statement?
In a functional langauge, we can make a decision by, for instance,
putting two lambdas into an array A, and then calling A[0] or A[1],
where the index 0 or 1 is comes from some Boolean result.
The only reason we have a control construct like if(A, X, Y) where X
is only evaluated if A is true, otherwise Y, is that X and Y
have side effects.
If X and Y don't have side effects, then if(A, X, Y) can be an ordinary
function whose arguments are strictly evaluated.
Moreover, if we give the functional language lazy evaluation semantics,
then anyway we get the behavior that Y is not evaluated if A is true,
and that lazy evaluation model can be used as the basis for sneaking
effects into the functional language and conctrolling them.
Anyway, Turing calculation by primitive recursion does not require
conditional branching. Just perhaps an if function which returns
either its second or third argument based on the truth value of
its first argument.
For instance, in certain C preprocessor tricks, conditional expansion
is achieved by such macros.
When we run the following through the GNU C preprocessor (e.g. by pasting
#define TRUE_SELECT_TRUE(X) X
#define TRUE_SELECT_FALSE(X)
#define FALSE_SELECT_TRUE(X)
#define FALSE_SELECT_FALSE(X) X
#define SELECT_TRUE(X) X
#define SELECT_FALSE(X)
#define PASTE(X, Y) X ## Y
#define IF(A, B, C) PASTE(TRUE_SELECT_, A)(B) PASTE(FALSE_SELECT_, A)(C)
#define FOO TRUE
#define BAR FALSE
IF(FOO, foo is true, foo is false)
IF(BAR, bar is true, bar is false)
foo is true
bar is false
So, how long did it take to debug? (I've no idea how it works. If I
change all TRUE/FALSE to BART/LISA respectively, it still gives the same
output. I'm not sure how germane such an example is.)
Post by Kaz Kylheku
Yet, macro expansion has no conditionals. The preprocessing language has
#if and #ifdef, but we didn't use those. Just expansion of computed names.
This is an example of not strictly needing conditionals to achieve
conditional evaluation or expansion: an IF(A, B, C) operator that
yields B or C depending on the truth of A, and so forth.
John MacCarthy (Lisp inventor) wrote himself such an IF function
in Fortran, in a program for calculating chess moves. It evaluated
both the B and C expressions, and so it wasn't a proper imperative
conditional, but it didn't matter.
You mean like this one:

int IF(int c, int a, int b) {
return a*!!c + b*!c;
}

I think most languages can manage that. I guess there was a reason
McCarthy needed it rather than use Fortran's existing IF statement,
other than, being the Lisp guy, that was how his mind worked.)
Kaz Kylheku
2024-12-22 23:47:25 UTC
Reply
Permalink
Post by bart
Post by Kaz Kylheku
Post by Janis Papanagnou
Post by Tim Rentsch
Post by Janis Papanagnou
Post by BGB
[...]
Pretty much all higher level control flow can be expressed via goto.
A 'goto' may be used but it isn't strictly *necessary*. What *is*
necessary, though, that is an 'if' (some conditional branch), and
either 'goto' or recursive functions.
Conditional branches, including 'if', '?:', etc., are not strictly
necessary either.
No? - Can you give an example of your statement?
In a functional langauge, we can make a decision by, for instance,
putting two lambdas into an array A, and then calling A[0] or A[1],
where the index 0 or 1 is comes from some Boolean result.
The only reason we have a control construct like if(A, X, Y) where X
is only evaluated if A is true, otherwise Y, is that X and Y
have side effects.
If X and Y don't have side effects, then if(A, X, Y) can be an ordinary
function whose arguments are strictly evaluated.
Moreover, if we give the functional language lazy evaluation semantics,
then anyway we get the behavior that Y is not evaluated if A is true,
and that lazy evaluation model can be used as the basis for sneaking
effects into the functional language and conctrolling them.
Anyway, Turing calculation by primitive recursion does not require
conditional branching. Just perhaps an if function which returns
either its second or third argument based on the truth value of
its first argument.
For instance, in certain C preprocessor tricks, conditional expansion
is achieved by such macros.
When we run the following through the GNU C preprocessor (e.g. by pasting
#define TRUE_SELECT_TRUE(X) X
#define TRUE_SELECT_FALSE(X)
#define FALSE_SELECT_TRUE(X)
#define FALSE_SELECT_FALSE(X) X
#define SELECT_TRUE(X) X
#define SELECT_FALSE(X)
#define PASTE(X, Y) X ## Y
#define IF(A, B, C) PASTE(TRUE_SELECT_, A)(B) PASTE(FALSE_SELECT_, A)(C)
#define FOO TRUE
#define BAR FALSE
IF(FOO, foo is true, foo is false)
IF(BAR, bar is true, bar is false)
foo is true
bar is false
So, how long did it take to debug? (I've no idea how it works. If I
I typed it out right in the middle of my article and piped it out to
gcc, iterating a few times. I made a few silly mistakes in IF, mostly
due to referencing the wrong A, B, C.

Also, the SELECT_TRUE and SELECT_FALSE macros are dead code; not used.
Post by bart
change all TRUE/FALSE to BART/LISA respectively, it still gives the same
output. I'm not sure how germane such an example is.)
If you rename consistently, it will work. But it's not hygienic in that
since the solution relies on calculated identifiers, you have to change
TRUE_SELECT_TRUE to TRUE_SELECT_BART.

How it works is very simmple in that PASTE(TRUE_SELECT_, A) calculates
TRUE_SELECT_TRUE or TRUE_SELECT_FALSE depending on whether A contains
TRUE or FALSE. Then the argument list (B) is combined with this
calculated name, resulting in a macro call to TRUE_SELECT_TRUE(B) or
TRUE_SELECT_FALSE(B) with the value of B as an argument.
If the former is used, then it expands to B; if the latter, then to
nothing.

One of the two PASTE calls in the expansion of IF() produces tokens, and
the other nothing. The two results are catenated together into one token
sequence, so we get the result of whichever one is nonempty.
--
TXR Programming Language: http://nongnu.org/txr
Cygnal: Cygwin Native Application Library: http://kylheku.com/cygnal
Mastodon: @***@mstdn.ca
Tim Rentsch
2024-12-23 01:22:01 UTC
Reply
Permalink
Post by Kaz Kylheku
Post by Janis Papanagnou
Post by Tim Rentsch
Post by Janis Papanagnou
Post by BGB
[...]
Pretty much all higher level control flow can be expressed via goto.
A 'goto' may be used but it isn't strictly *necessary*. What *is*
necessary, though, that is an 'if' (some conditional branch), and
either 'goto' or recursive functions.
Conditional branches, including 'if', '?:', etc., are not strictly
necessary either.
No? - Can you give an example of your statement?
In a functional langauge, we can make a decision by, for instance,
putting two lambdas into an array A, and then calling A[0] or A[1],
where the index 0 or 1 is comes from some Boolean result.
The only reason we have a control construct like if(A, X, Y) where X
is only evaluated if A is true, otherwise Y, is that X and Y
have side effects.
If X and Y don't have side effects, then if(A, X, Y) can be an ordinary
function whose arguments are strictly evaluated.
Moreover, if we give the functional language lazy evaluation
semantics, then anyway we get the behavior that Y is not evaluated
if A is true, and that lazy evaluation model can be used as the
basis for sneaking effects into the functional language and
conctrolling them.
Anyway, Turing calculation by primitive recursion does not require
conditional branching. Just perhaps an if function which returns
either its second or third argument based on the truth value of its
first argument.
For instance, in certain C preprocessor tricks, conditional
expansion is achieved by such macros.
When we run the following through the GNU C preprocessor (e.g. by
#define TRUE_SELECT_TRUE(X) X
#define TRUE_SELECT_FALSE(X)
#define FALSE_SELECT_TRUE(X)
#define FALSE_SELECT_FALSE(X) X
#define SELECT_TRUE(X) X
#define SELECT_FALSE(X)
#define PASTE(X, Y) X ## Y
#define IF(A, B, C) PASTE(TRUE_SELECT_, A)(B) PASTE(FALSE_SELECT_, A)(C)
#define FOO TRUE
#define BAR FALSE
IF(FOO, foo is true, foo is false)
IF(BAR, bar is true, bar is false)
foo is true
bar is false
Yet, macro expansion has no conditionals. The preprocessing
language has #if and #ifdef, but we didn't use those. Just
expansion of computed names.
Nice example.
Lawrence D'Oliveiro
2024-12-16 21:23:08 UTC
Reply
Permalink
Post by BGB
As an IL, even C is a little overkill, unless turned into a restricted
subset ...
Why not use WASM as your IL?
Michael S
2024-12-17 09:16:58 UTC
Reply
Permalink
On Mon, 16 Dec 2024 21:23:08 -0000 (UTC)
Post by Lawrence D'Oliveiro
Post by BGB
As an IL, even C is a little overkill, unless turned into a
restricted subset ...
Why not use WASM as your IL?
Because he is DIY character.
bart
2024-12-17 12:04:29 UTC
Reply
Permalink
Post by Lawrence D'Oliveiro
Post by BGB
As an IL, even C is a little overkill, unless turned into a restricted
subset ...
Why not use WASM as your IL?
Have you tried it? I mean, directly generating WASM from a compiler
front-end, not just using somebody else's tool to do so.

WASM is a stack-based language, but one that supposedly doesn't even
have branching, although there is a 'br' statement, with some restrictions.

Information about it is quite elusive; it took me 5 minutes to even get
examples of what it looks like (and I've seen it before).

C can apparently compile to WASM via Clang, so I tried this program:

void F(void) {
int i=0;
while (i<10000) ++i;
}

which compiled to 128 lines of WASM (technically, some form of 'WAT', as
WASM is a binary format). The 60 lines correspondoing to F are shown
below, and below that, is my own stack IL code.

So, what do you with your WASM/WAT program once generated? I've no idea,
except that WASM is inextricably typed up with with browsers and with
JavaScript, in which I have no interest.

With C, you run a compiler; with ASM, an assembler; these formats are
well understood.

You can appreciate that it can be easier to devise your own format and
your own tools that you understand 100%.




--------------------------------------
F: # @F
.functype F () -> ()
.local i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32,
i32, i32
# %bb.0:
global.get __stack_pointer
local.set 0
i32.const 16
local.set 1
local.get 0
local.get 1
i32.sub
local.set 2
i32.const 0
local.set 3
local.get 2
local.get 3
i32.store 12
.LBB0_1: # =>This Inner Loop Header: Depth=1
block
loop # label1:
local.get 2
i32.load 12
local.set 4
i32.const 10000
local.set 5
local.get 4
local.set 6
local.get 5
local.set 7
local.get 6
local.get 7
i32.lt_s
local.set 8
i32.const 1
local.set 9
local.get 8
local.get 9
i32.and
local.set 10
local.get 10
i32.eqz
br_if 1 # 1: down to label0
# %bb.2: # in Loop: Header=BB0_1 Depth=1
local.get 2
i32.load 12
local.set 11
i32.const 1
local.set 12
local.get 11
local.get 12
i32.add
local.set 13
local.get 2
local.get 13
i32.store 12
br 0 # 0: up to label1
.LBB0_3:
end_loop
end_block # label0:
return
end_function

-----------------------------

proc F::
local i32 i.1
load i32 0
store i32 i.1
jump #2
#4:
load u64 &i.1
incrto i32 /1
#2:
load i32 i.1
load i32 10000
jumplt i32 #4
#3:
#1:
retproc
endproc
BGB
2024-12-17 18:51:04 UTC
Reply
Permalink
Post by bart
Post by Lawrence D'Oliveiro
Post by BGB
As an IL, even C is a little overkill, unless turned into a restricted
subset ...
Why not use WASM as your IL?
Have you tried it? I mean, directly generating WASM from a compiler
front-end, not just using somebody else's tool to do so.
WASM is a stack-based language, but one that supposedly doesn't even
have branching, although there is a 'br' statement, with some restrictions.
Information about it is quite elusive; it took me 5 minutes to even get
examples of what it looks like (and I've seen it before).
 void F(void) {
    int i=0;
    while (i<10000) ++i;
 }
which compiled to 128 lines of WASM (technically, some form of 'WAT', as
WASM is a binary format). The 60 lines correspondoing to F are shown
below, and below that, is my own stack IL code.
So, what do you with your WASM/WAT program once generated? I've no idea,
except that WASM is inextricably typed up with with browsers and with
JavaScript, in which I have no interest.
With C, you run a compiler; with ASM, an assembler; these formats are
well understood.
You can appreciate that it can be easier to devise your own format and
your own tools that you understand 100%.
Hmm... It looks like the WASM example is already trying to follow SSA
rules, then mapped to a stack IL... Not necessarily the best way to do
it IMO.


But, yeah, in BGBCC I am also using a stack-based IL (RIL), which
follows rules more in a similar category to .NET CIL (in that, stack
items carry type, and the stack is generally fully emptied on branch).


In my IL, labels are identified with a LABEL opcode (with an immediate),
and things like branches work by having the branch target and label
having the same immediate (label ID).

I ended up considering this preferable to byte offsets, as:
Easier to generate from the front-end;
LABEL also marks the start/end of basic blocks;
...

There are also opcodes to convey the source filename and line number,
these don't generate any output but merely serve to transport filename
and line number information (useful for debugging).


RIL was a little weird in that functions and variables are themselves
defined via bytecode operations. This is unlike both JVM and .NET CIL,
which had used external metadata/structures for defining functions and
variables (nevermind the significant differences between JVM and .NET in
this area).

This is pros/cons, main downside of the current format is that it
requires the bytecode modules to be loaded sequentially and fully. This
works OK for a compiler on a modern PC, but does impose on RAM somewhat
for a compiler on a more memory-constrained target. One idea would be to
individually wrap functions and have a mechanism so that they can be
loaded dynamically. But, this hasn't really been done for my existing
IL. Most likely option is that metadata continues to be defined via
bytecode operations, just that each function is separately wrapped, and
there may be an index to map function names to the corresponding "lump"
(say, if using a WAD variant as the top-level container).

Say:
Lump name is "FNC01234" (IWAD) or "func_1234" (WAD2).
And there is a table mapping "FOO_SomeFunction" to "FNC01234" or
"func_1234".
But, this sort of things, along with past ideas to try moving this over
to a format along similar lines to RIFF/AVI, have generally fizzled
(along with possible debate over to to the merits of a WAD-like or
RIFF-like format).

Though, an arguably simpler option might be to just individually wrap
the bytecode for each translation unit, and have an manifest of what
symbols are present. In this way, it would function more like a
traditional static library (as opposed to the current strategy of
globing all of the translation units in the library into a single large
blob of bytecode); and probably dumping the bytecode for each
translation unit into a WAD (again, possibly either IWAD or WAD2, though
probably WAD2 in this case, as the comparably larger lumps would
eliminate most concern over the larger directory entries).



When converting to the 3AC IR, there is the quirk that function calls
are split into multiple parts:
The CALL operation, which ends the current basic-block;
A CSRV operation, which is at the start of a new basic block.
CSRV = Caller Save Return Value.

In cases where the 3AC was being interpreted, this was better, as the
CSRV operation serves to save the return value from the called function
to the correct place in the caller's frame (where the interpreter does
not use recursion for its own operation).
Internal conversion to 3AC was faster than trying to directly interpret
a stack bytecode (as well as 3AC being a better format for code generation).
Post by bart
--------------------------------------
    .functype    F () -> ()
    .local      i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32,
i32, i32, i32
    global.get    __stack_pointer
    local.set    0
    i32.const    16
    local.set    1
    local.get    0
    local.get    1
    i32.sub
    local.set    2
    i32.const    0
    local.set    3
    local.get    2
    local.get    3
    i32.store    12
.LBB0_1:                                # =>This Inner Loop Header: Depth=1
    block
    local.get    2
    i32.load    12
    local.set    4
    i32.const    10000
    local.set    5
    local.get    4
    local.set    6
    local.get    5
    local.set    7
    local.get    6
    local.get    7
    i32.lt_s
    local.set    8
    i32.const    1
    local.set    9
    local.get    8
    local.get    9
    i32.and
    local.set    10
    local.get    10
    i32.eqz
    br_if       1                               # 1: down to label0
# %bb.2:                                #   in Loop: Header=BB0_1 Depth=1
    local.get    2
    i32.load    12
    local.set    11
    i32.const    1
    local.set    12
    local.get    11
    local.get    12
    i32.add
    local.set    13
    local.get    2
    local.get    13
    i32.store    12
    br          0                               # 0: up to label1
    end_loop
    return
    end_function
-----------------------------
           local    i32       i.1
    load     i32       0
    store    i32       i.1
    jump               #2
    load     u64       &i.1
    incrto   i32 /1
    load     i32       i.1
    load     i32       10000
    jumplt   i32       #4
    retproc
endproc
bart
2024-12-18 12:08:24 UTC
Reply
Permalink
Post by BGB
  void F(void) {
     int i=0;
     while (i<10000) ++i;
  }
which compiled to 128 lines of WASM (technically, some form of 'WAT',
as WASM is a binary format). The 60 lines correspondoing to F are
shown below, and below that, is my own stack IL code.
I'm not even sure what format that code is in, as WAT is supposed to use
S-expressions. The generated code is flat. It differs in other ways from
examples of WAT.
Post by BGB
Hmm... It looks like the WASM example is already trying to follow SSA
rules, then mapped to a stack IL... Not necessarily the best way to do
it IMO.
I hadn't considered that SSA could be represented in stack form.

But couldn't each push be converted to an assignment to a fresh
variable, and the same with pop?

As for Phi functions, the only similar thing I encounter (but could be
mistaken), is when there is a choice of paths to yield a value (such as
(c ? a : b) in C; my language has several such constructs).

With stack code, the result conveniently ends up on top of the stack
whichever path is taken, which is a big advantage. Unless you then have
to convert that to register code, and need to ensure the values end up
in the same register when the control paths join up again.
Post by BGB
But, yeah, in BGBCC I am also using a stack-based IL (RIL), which
follows rules more in a similar category to .NET CIL (in that, stack
items carry type, and the stack is generally fully emptied on branch).
In my IL, labels are identified with a LABEL opcode (with an immediate),
and things like branches work by having the branch target and label
having the same immediate (label ID).
So, you jump to label L123, and the label looks like:

L123:

I think that is pretty standard! But it sounds like you use a very tight
encoding for bytecode, while mine uses a 32-byte descriptor for each IL
instruction.

(One quibble with labels is whether a label definition occupies an
actual IL instruction. With my IL used as a backend for static
languages, it does. And there can be clusters of labels at the same spot.

With dynamic bytecode designed for interpretation, it doesn't. It uses a
different structure. This means labels don't need to be 'executed' when
encountered.)
BGB
2024-12-18 18:50:20 UTC
Reply
Permalink
Post by bart
Post by BGB
  void F(void) {
     int i=0;
     while (i<10000) ++i;
  }
which compiled to 128 lines of WASM (technically, some form of 'WAT',
as WASM is a binary format). The 60 lines correspondoing to F are
shown below, and below that, is my own stack IL code.
I'm not even sure what format that code is in, as WAT is supposed to use
S-expressions. The generated code is flat. It differs in other ways from
examples of WAT.
Dunno there...

It looks like WASM has changed slightly from what I remember when I
originally looked at it, so it could be "possible" if it could be made
to support separate compilation and similar.
Post by bart
Post by BGB
Hmm... It looks like the WASM example is already trying to follow SSA
rules, then mapped to a stack IL... Not necessarily the best way to do
it IMO.
I hadn't considered that SSA could be represented in stack form.
But couldn't each push be converted to an assignment to a fresh
variable, and the same with pop?
As for Phi functions, the only similar thing I encounter (but could be
mistaken), is when there is a choice of paths to yield a value (such as
(c ? a : b) in C; my language has several such constructs).
I was mostly noting that it appeared that every operation was creating a
new variable and only assigning to it once.

I didn't look too much more closely than this, only to note that it was
different.
Post by bart
With stack code, the result conveniently ends up on top of the stack
whichever path is taken, which is a big advantage. Unless you then have
to convert that to register code, and need to ensure the values end up
in the same register when the control paths join up again.
With JVM, the rule was that all paths landing at the same label need to
have the same stack depth and same types.

With .NET, the rule was that the stack was always empty, any merging
would need to be done using variables.


BGBCC is sorta mixed:
In most cases, it follows the .NET rule;
A special-case exception exists mostly for implementing the ?: operation
(which in turn has special stack operations to signal its use).

BEGINU // start a ?: operator
L0:
... //one case
SETU
JMP L2
L1:
... //other case
SETU
JMP L2
ENDU
L2:


This is a bit of wonk, if I were designing it now, would likely do it
the same as .NET, and use temporary variables.


Actually, I might be tempted to use a 3AC IR as well (though, probably
non-SSA). And, probably design things a bit differently.


In this case, if I did a 3AC IR, might design a textual syntax along
similar lines to BASIC or FORTRAN 77 (albeit probably without the
fixed-column formatting or line numbers).

Though, the nominal format for use in the compiler would remain binary.
Post by bart
Post by BGB
But, yeah, in BGBCC I am also using a stack-based IL (RIL), which
follows rules more in a similar category to .NET CIL (in that, stack
items carry type, and the stack is generally fully emptied on branch).
In my IL, labels are identified with a LABEL opcode (with an
immediate), and things like branches work by having the branch target
and label having the same immediate (label ID).
Yeah, in textual form.
Though, the label is internally represented as, say:
LABEL 123

IIRC, usually numbering starts over from 0 for each function, though in
the backend IR all labels get a unique number within a 24-bit numbering
space.

The labels are then split into several categories:
Global labels, used to identify functions/variables, with an associated
name;
IL labels, which were mapped over from the RIL bytecode;
Temporary labels, which exist solely in the backend;
Line numbers, not true labels, mostly exist to convey line-number info
(associated with a file-name and line number);
Special/Architectural, used as placeholders for things like CPU
registers (for variable load/store).
Post by bart
I think that is pretty standard! But it sounds like you use a very tight
encoding for bytecode, while mine uses a 32-byte descriptor for each IL
instruction.
(One quibble with labels is whether a label definition occupies an
actual IL instruction. With my IL used as a backend for static
languages, it does. And there can be clusters of labels at the same spot.
With dynamic bytecode designed for interpretation, it doesn't. It uses a
different structure. This means labels don't need to be 'executed' when
encountered.)
In my interpreters, it always uses a bytecode operation.
However, apart from my very early interpreters, typically the stack IL
is not used directly.

So, a personal timeline was like:
2003/2004: BGBScript came into existence
First version used DOM and directly walked the DOM tree.
Used a GC, generated lots of garbage objects;
Syntax was based on JavaScript with some wonk;
Was horridly slow.
2006:
BGBScript VM (BS-VM) was rewritten to S-Expressions internally;
Dropped some of the original wonk, moving to a cleaner JS syntax;
Went to a bytecode interpreter.
2007:
BGBCC was written using the frontend from the 2003 VM as a base;
The IL design was based on 2006 BS-VM;
Replaced the original DOM with a custom stand-in;
Used parts of the 2006 VM as well.
2009:
The BS-VM was modified to turn the stack IL into 3AC and run this;
Also had a JIT and similar by this point;
Using 3AC and JIT made things significantly faster;
Also tended to leak a lot less garbage,
operating mostly at "steady state".
Syntactically, it had become more like ActionScript3 or HaXE.
2013: Created BGBScript2 (BS2)
This mostly resembled a Java/C#/AS3 hybrid;
Eliminated the GC in favor of primarily static + manual MM.
2015/2016: Created the BGBTech2 3D engine
Partly written in a mix of C and BGBScript2
Was my biggest project to use BS2

Then:
2017: Started on my BJX1 project
Revived BGBCC, used it as the compiler.
2019: Rebooted the project to BJX2.
BJX1 quickly turned into a huge mess
which was non-viable to implement in an FPGA.
Until now, BJX2 project has continued.

Some stuff following the design of the BS2 VM was back-ported onto
BGBCC, but in many ways, BGBCC has a lot more cruft.



In the BS2 VM, the image format is a TLV container.
There is a string table, data area for functions/etc;
Index tables;
...
Generally, functions could be loaded and converted to 3AC on demand.


The IL in the BS2 VM was not a pure stack machine, but more like:
OP with 2 stack args, stack dest (common with BGBCC)
OP with 2 stack args, local dest (common with BGBCC)
OP with 2 local args, stack dest
OP with 2 local args, local dest (like in 3AC)
OP with local and immediate, stack dest
OP with local and immediate, local dest
OP with local and stack, stack dest
OP with local and stack, local dest

This was more complicated, but reduced the number of IL operations.
Internally, it all converted to 3AC for the backend interpreter.


The incentive to do this for BGBCC was less, as folding the
local-variable or constant-loads into the operator is less immediately
beneficial to a compiler; but does make the bytecode loader more
complicated. Folding the destination register into the bytecode ops in
many cases is still relevant, as it is comparably harder to fold the
destination-store into the 3AC op than to fold a source load.


Generally, bytecode ops and operands were encoded with VLNs (variable
length numbers).

Generally (numberic VLN):
00..7F: 0..127
00..BF XX: 128..16383
C0..DF XX XX: 16384..2M
...

These values were encoded in MSB first order, and could directly
represent values up to 64 bits (in both the BS2VM and BGBCC, 128-bit
values tend to be represented as pairs of 64-bit values).

For signed integer values, the sign was folded into the LSB.
Floating point values were represented as a base/exponent VLN pair.
Basically, an integer value scaled by a power-of-2 exponent.


Opcodes were different, IIRC:
00..DF: Single Byte
E0..EF: Two Byte (224..4095)
F0..F7: Three Byte
...

But, generally, only 1 and 2 byte cases were used.

IIRC, did not define a textual notation for the BS2VM's ASM.


Local variables, labels, etc, were all identified as numeric indices.
Typically a single byte.

Like JVM, and unlike BGBCC, in the BS2VM, all the variables (including
arguments) were held in an array of local variables (BGBCC has locals,
arguments, and temporaries, as 3 separate spaces).

IIRC, BS2VM had still used variable type-tagging (like BGBCC and .NET),
rather than the untyped variables with typed operators scheme (what JVM
had used).

But, typed operators more make sense if you intend to interpret the
stack bytecode directly, which was generally not done in my VMs (except
in very early versions). Otherwise, implicitly typed operators probably
make more sense.


...
bart
2024-12-18 23:37:11 UTC
Reply
Permalink
Post by BGB
Post by bart
With stack code, the result conveniently ends up on top of the stack
whichever path is taken, which is a big advantage. Unless you then
have to convert that to register code, and need to ensure the values
end up in the same register when the control paths join up again.
With JVM, the rule was that all paths landing at the same label need to
have the same stack depth and same types.
With .NET, the rule was that the stack was always empty, any merging
would need to be done using variables.
In most cases, it follows the .NET rule;
A special-case exception exists mostly for implementing the ?: operation
(which in turn has special stack operations to signal its use).
BEGINU  // start a ?: operator
...  //one case
SETU
JMP L2
... //other case
SETU
JMP L2
ENDU
This is a bit of wonk,
Well, this is pretty much what I do in stack code. I consider it impure,
as in needing artificial hints, but also the simplest solution.

I use opcodes STARTMX, RESETMX, ENDMX. They are no-ops when the IL is
interpreted. But during the linear scan needed during code generation,
where it has to keep track of the IL's operand stack, RESETMX will reset
the stack too.

(As mentioned, I have a lot more constructs that can yield N values not
just two. Apart from N-way select, if-else, switch-when and case-when
statements can also return values.)
Post by BGB
if I were designing it now, would likely do it
the same as .NET, and use temporary variables.
In 3AC then it's easy, all paths write to the same temporary.
Lawrence D'Oliveiro
2024-12-17 19:40:53 UTC
Reply
Permalink
Information about it is quite elusive ...
Did you try the usual place for Web-related stuff?

<https://developer.mozilla.org/en-US/docs/WebAssembly>
bart
2024-12-17 19:45:49 UTC
Reply
Permalink
Post by Lawrence D'Oliveiro
Information about it is quite elusive ...
Did you try the usual place for Web-related stuff?
<https://developer.mozilla.org/en-US/docs/WebAssembly>
That's all at the wrong level, eg:

"When you've written code in C/C++, you can then compile it into Wasm
using a tool like Emscripten"

It's not aimed at people /implementing/ such a tool.
Lawrence D'Oliveiro
2024-12-17 22:25:44 UTC
Reply
Permalink
Post by bart
Post by Lawrence D'Oliveiro
Information about it is quite elusive ...
Did you try the usual place for Web-related stuff?
<https://developer.mozilla.org/en-US/docs/WebAssembly>
It's not aimed at people /implementing/ such a tool.
It is aimed at those capable of following the links to relevant specs.
bart
2024-12-17 22:55:53 UTC
Reply
Permalink
Post by Lawrence D'Oliveiro
Post by bart
Post by Lawrence D'Oliveiro
Information about it is quite elusive ...
Did you try the usual place for Web-related stuff?
<https://developer.mozilla.org/en-US/docs/WebAssembly>
It's not aimed at people /implementing/ such a tool.
It is aimed at those capable of following the links to relevant specs.
It also a pretty terrible link. Trying to extract useful info a snippet
at a time is like pulling teeth. Here I was merely after an example of
WASM textual format.

WASM is somewhat like LLVM in that there the docs are so extensive that
they become impossible.

Show me (I assume you know all about it) how to write Hello, World in
WAT format, and what tool I need to download and use to run it. On Windows.

I can do it with my IL in half a page.
Lawrence D'Oliveiro
2024-12-18 05:55:31 UTC
Reply
Permalink
Post by bart
Post by Lawrence D'Oliveiro
Post by bart
It's not aimed at people /implementing/ such a tool.
It is aimed at those capable of following the links to relevant specs.
It also a pretty terrible link.
Did you see this link
<https://developer.mozilla.org/en-US/docs/WebAssembly/Reference>? Lots of
examples from there.
bart
2024-12-19 00:32:47 UTC
Reply
Permalink
Post by Lawrence D'Oliveiro
Post by bart
Post by Lawrence D'Oliveiro
Post by bart
It's not aimed at people /implementing/ such a tool.
It is aimed at those capable of following the links to relevant specs.
It also a pretty terrible link.
Did you see this link
<https://developer.mozilla.org/en-US/docs/WebAssembly/Reference>? Lots of
examples from there.
I promised a example of Hello World using my IL, and how to process and
run it, in half a page. This is it for Windows :

------------------------------------
Paste the indented code into a file hello.pcl:

addlib "msvcrt"
extproc puts

proc main:::
setcall i32 /1
load u64 "Hello World!"
setarg u64 /1
callf i32 /1 &puts
unload i32
load i32 0
stop
retproc
endproc

Download the pc.exe file here:
https://github.com/sal55/langs/blob/master/pc.exe, which is a 65KB file
(UPX-compressed from 180KB). (Advice to navigate AV not included here.)

At a command prompt with both files present, type:

pc -r hello

This will convert it to x64 code and run it. Use 'pc' by itself to see
the 6 other processing options.
------------------------------------

So 20 non-blank lines. It would be nice if an equally simple example
existed for WASM/WAT, or if people who suggested that choice could post
a link to such an example /that/ works on Windows.
Lawrence D'Oliveiro
2024-12-16 21:22:31 UTC
Reply
Permalink
Post by Bonita Montero
C++ is more readable because is is magnitudes more expressive than C.
And it is certainly more surprising than C. Often unpleasantly so.
Post by Bonita Montero
You can easily write a C++-statement that would hunddres of lines in C
Yes, but *which* hundreds of lines, exactly, would be the correct C
equivalent?
Loading...