Show Posts

This section allows you to view all posts made by this member. Note that you can only see posts made in areas you currently have access to.

Messages - lerno

Pages: [1] 2 3 ... 17
General Discussion / Re: Is a self-hosting c2 compiler a project goal?
« on: December 16, 2019, 10:04:25 PM »
Also, self hosting creates a lot of problems with bootstrapping, so it might not be a great thing to do anyway.

General Discussion / Re: Is a self-hosting c2 compiler a project goal?
« on: December 16, 2019, 10:03:51 PM »
I started on a C2 compiler written in C, since moving form C to C2 would be pretty easy. I didn't finish it though.

Ideas / Re: Remove const
« on: July 31, 2019, 05:56:33 PM »
D has all of this in the way you write it.

Ideas / Re: Remove const
« on: July 14, 2019, 08:35:44 PM »

Ideas / Remove const
« on: July 14, 2019, 08:27:14 PM »
Const, as used in C, is often not doing what people think it's doing, and does not offer the protection people think it is protecting.

For pointers, a pointer to a constant struct is saying "using this pointer we will not modify the struct itself (but we may modify what the struct refers to)"

Because of const-ness, functions must guarantee const-ness if they are to be useful. Consider a function foo(int* ptr) it is not allowed to pass a (const int)* pointer to such a function, even if the function does not actually modify the memory region that the pointer points to. For maximal usefulness, functions are therefore forced to declare constness, that is, it should always be foo(i(const int)* ptr)

A valid point is that by declaring an inparameter const it communicates that the function does not modify the parameter, but the injury done by *requiring* could be said to outweigh the benefits.

To try to retain the advantages of const, which avoiding the syntax pollution const can mean, this is a thing C2 could do:

1. A function declares itself const or not const entirely. This is usually a more important distinction than whether each parameter is const or not:

func void foo(int *ptr) const { ... } – will not alter the contents of anything related to the in-pointer. That is, if the signature instead would have been func void foo(int **ptr) const we would not have been allowed to do *ptr = nor **ptr =

The other use that one would retain is declaring global constants.

Ideas / Re: Built-in managed pointers
« on: March 24, 2019, 05:19:15 PM »
Rust is very high maintenance in regards to memory management, but I think we should look for a small effort that can take us maybe 70% of the way.

Ideas / Re: Defer on functions.
« on: March 24, 2019, 04:21:54 PM »
That's why the defer is part of the function signature and not of the function itself, acting as a decorator macro.

It has some interesting consequences: what about the function pointer?

We could also consider this even simpler by introducing a qualifier, eg

Code: [Select]
// func const FILE open(...) { ... } =>
func const managed FILE open(...) { ... }

And here any struct that is "managed" must have a "StructName.release(StructName *struct)" call.

There are various ways to do it actually.

Ideas / Defer on functions.
« on: March 22, 2019, 11:35:51 AM »
What if functions could add defer to the scope where they are invoked?

Here is some code:

Code: [Select]
func bool do_stuff(i32 resource_id)
  Resource *x = get_resource(resource_id);
  defer release_resource(x);
  if (!play_around_with(x)) return false;
  return foo(x);

This is fine, the resource is released no matter what path is taken. However, this always requires the defer.

I envisioned a defer sugar, like this:

Code: [Select]
func bool do_stuff(i32 resource_id)
  Resource* x = get_resource(resource_id) @defer(release_resource);
  if (!play_around_with(x)) return false;
  return foo(x);

This would sort of make it more succinct and would also make it possible to use it in an expression.

However, what if there was a "safe" version?

Code: [Select]
func Resource get_resource_with_release(i32 resource_id) @defer(release_resource)
{ ... }

This would be fully equivalent to the code with defer above, but could then be written as:

Code: [Select]
func bool do_stuff(i32 resource_id)
  Resource* x = get_resource_with_release(resource_id); // inserts an implicit defer!
  if (!play_around_with(x)) return false;
  return foo(x);

Although resource management is a fine example of this, it's *really* nice for refcounting as if you write the following imaginary code:

Code: [Select]
Foo@ foo = @rcmalloc(sizeof(Foo)); // What is the refcount of foo after this?
foo_something(@rcmalloc(sizeof(Foo))); // Does this leak?

If @rcmalloc returns 1, then rc would be 2 in the first case (except if we have special handling of assignment of RC) and a leak on the second line.
However, if @rcmalloc returns 0, then the second line also leaks.

However, if we let @rcmalloc return rc = 1 AND have the profile of @defer(release), then an implicit defer would ensure that in the scope where called the rc would eventually be decreased (unless assigned to). And this is basically what @autorelease in ObjC does too, but in a less controlled manner.

Even if the above example doesn't make sense, or refcounting shouldn't have language support, it's still a very good way to cheaply enable manual RC built on top of the language.


Ideas / Built-in managed pointers
« on: March 22, 2019, 11:15:17 AM »
Taking a hint from Cyclone, Rust etc one could consider managed pointers / objects. There are several possibilities:

1. Introduce something akin to move/borrow syntax with a special pointer type, eg. Foo@ x vs Foo* y and make the code track Foo@ to have unique ownership.
2. Introduce ref-counted objects with ref-counted pointers. Again use Foo@ x vs Foo* y with the latter being unretained. This should be internal refcounting to avoid any of the issues going from retained -> unretained that shared_ptr has. Consequently any struct that is RC:ed needs to be explicitly declared as such.
3. Managed pointers: you alloc and the pointer gets a unique address that will always be invalid after use. Any overflows will be detected, but use of managed pointers is slower due to redirect and check.

Sample code for (2)
Code: [Select]
type Foo struct @(refcounted) {
   i32 a;

func Bar(Foo@ a)
    printf("%d\n", sizeof(Foo)); // prints 8 due to RC
    printf("%d\n", rc(a)); // prints 1
    Foo@ x = a;
    printf("%d\n", rc(a)); // prints 2
    x = nil;
    printf("%d\n", rc(a)); // prints 1
    Foo* y = a;
    printf("%d\n", rc(a)); // prints 1
    Foo* z = malloc(sizeof(Foo)); // Not recommended!
    // TOOD discuss how to properly initialize a RC:ed variable.

Ideas / Re: Switch proposal
« on: March 12, 2019, 04:40:22 PM »
Well, if you look at the alternatives for fallthrough:

(a) fallthrough
(b) goto next
(c) goto case
(d) goto case 4
(e) continue case
(f) next
(g) nextcase

Here obviously b, c, d, e, g all clearly indicate that there's a jump. (d) is even more explicit of course, not to mention flexible.

Ideas / Re: Macro-system design
« on: March 01, 2019, 02:38:23 PM »
Did you think about my generics proposal? "import foo (i32, Bar, f32) as local;" "module foo (A, B, C);"

Ideas / Re: Explicit non-null-ness
« on: March 01, 2019, 02:35:27 PM »
Semantic analysis can guarantee non-nullness. Don't confuse it with C++, it's just borrowing the operator. int &a means int *a @(non-null).

General Discussion / Unsigned conversions
« on: February 20, 2019, 10:51:52 PM »
In C, comparing a i32 with an u32 will do the somewhat counterintuitive thing of promoting the i32 to u32.

This leads to the somewhat confusing situation that the following C code:

Code: [Select]
  int a = -1;
  unsigned int b = 1;
  unsigned short c = 1;
  if (a < b) printf("Less!\n");
  if (a > b) printf("More!\n");
  if (a < c) printf("Less short!\n");
  if (a > c) printf("More short!\n");


Less short!

The following rules might help:

1. The comparison operations < > <= >= are not allowed between signed and unsigned where the promotion would be to unsigned (that is, where the unsigned type is has greater or equal bit size of the signed). So i32 < u16 is fine, but not i32 < u32 or i16 < u32.

2. >= 0 is always an error for unsigned numbers. This would prevent the following bug: for (unsigned i = size; i >= 0; i--). The analyser should detect this condition as always being true and creating an *error* for it.

A more drastic change that could be considered instead of (1) would be to always do sign promotion whenever there is a comparison with signed and unsigned values.

So !i32 < u64) would cast both sides to i64. The downside of that (or any similar scheme) is that it differs from C in a critical, rarely learned part of the language. It feels dangerous. Prohibiting conversions feel more "safe". Note that comparisons (except for ==) is where the conversion from signed to unsigned is where the danger is. For example cast<i32>(-1) + cast<u64>(12) will still yield 11 as expected due to the behaviour of unsigned arithmetics.

Implementation Details / Re: C2 in C
« on: February 12, 2019, 01:17:55 AM »
My pure C implementation of C2 is moving forward slowly (at - look at the development branch). It's using LLVM exclusively (no C gen).

I'm focusing on the areas where C2C is weak today, so that would both be LLVM and constant folding.

The compiler uses BigInt for integer constants (and really should do the same for floats, but right now it doesn't). I recently worked on the implicit casts and in order to make sense out of it I've changed a bit from C's implicit casting, mostly following Zig.

To recap C's casting rules:

  • with two operands where at least one is float, upgrade everything to the biggest float (float -> double -> long double)
  • if no float is found, int conversion resumes.
  • If both types are signed or both types are unsigned: promote to the largest of the (eg short -> int -> long etc) (unsigned short -> unsigned int -> unsigned long etc)
  • If they have different sign and the signed version can represent represent all numbers of the unsigned (e.g. u16 - i32), promote to the signed version.
  • If it's not possible to represent it, promote to the unsigned version of the type. (e.g. u32 - i32 => u32, u64 - i32 => u64)

For constant folding I've followed these rules instead:

  • Float conversion as with C
  • Folding an operation with bigint and bit limited int will convert the bigint to that bit size. If it's not possible to convert the constant, a compile time error will occur. E.g. const i8 c = 1; const i32 d = c + 200; is illegal since 200 cannot be converted to i8 without loss.
  • If two non bigints are folded, it's converted to the biggest type (like in C), for signed / unsigned conversion: if the unsigned constant may be contained in the signed type, then the conversion is valid, otherwise it is a compile time error. (e.g. cast<u8>(200) + cast<i8>(1) is a compile time error)
  • In addition, constant overflow is a compile time error. So cast<u8>(200) + cast<u8>(100) would be a compile time error as 300 does not fit in u8.

In order to make unsigned <=> signed conversions I'm thinking of some very simple lossy conversion, like

Code: [Select]
i8 a = -1;
u8 b = @ucast(a); // Bitcast of a
a = @scast(b); // Bitcast of b

Maybe even add a special assignment like (placeholder syntax!):

Code: [Select]
b u= a; // same as b = @ucast(a)
if (b u== a) ...; // same as if (b == @ucast(a)) ...
a s= b; // same as a = @scast(a)
if (b s== a) ...; // same as if (@scast(b) == a) ...

Implementation Details / Re: CTC (partial, full, none)
« on: February 12, 2019, 12:47:59 AM »
Of what use is CTC partial? I don't really see it.

Pages: [1] 2 3 ... 17