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.


Topics - lerno

Pages: [1] 2 3 ... 5
1
Ideas / Distinct types
« on: December 17, 2020, 02:54:38 AM »
An interesting feature in Odin is that of "distinct" types. That is, it acts like you actually created a completely new type, not a typedef alias. That way you can for example have a "UserId" type which is an int underneath but typechecks as if it was another type.

2
General Discussion / The return of the forums
« on: December 15, 2020, 10:37:34 PM »
I thought the forums were gone. Happy to see that they're back.

3
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.

4
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;
  do_some_other_thing(x);
  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;
  do_some_other_thing(x);
  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;
  do_some_other_thing(x);
  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.

 

5
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.
}


6
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");

Prints:

More!
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.

7
Ideas / Explicit non-null-ness
« on: February 02, 2019, 12:28:24 PM »
I suggest we reuse & arg to mean a pointer that is guaranteed to be non-null.

Consider the following methods:

Code: [Select]
Foo& foo();
Foo* foo2();
void bar(Foo& f);
void bar2(Foo* f);

Unlike in C++, both Foo& and Foo* are pointer, the former guaranteed to be not null.

Code: [Select]
Foo *f1 = foo(); // Non null to nullable ok
Foo &f2 = foo2(); // Nullable to non null not allowed

A check allows conversion:

Code: [Select]
Foo *f = foo2();
assert(f);
Foo &f2 = f;

Or:

Code: [Select]
Foo *f = foo2();
Foo &f2 = f ? f : foo();

With the elvis operator:

Code: [Select]
Foo &f = foo2() ?: foo();

Using pointer without nullcheck is a warning:

Code: [Select]
Foo* f = foo2();
return f.a; // warn, f may be null.

Solution is adding the assert test, or supress null warning with an attribute

Code: [Select]
Foo *f = foo2();
return f.a @(notnull);

8
Ideas / Require explicit (un)initialization.
« on: January 30, 2019, 06:07:18 PM »
Instead of warning on non-initialized variables and explicitly initializing, consider the following change:

Code: [Select]
int a; // a initialized to 0, as if it was static.
int a = 10; // a initialized to 10
int a = uninitialized; // a not initialized, warning if used before later assignment.

Alternatives to uninitialized:

Code: [Select]
int a = ---;
int a = *;
int a = ?;

9
Implementation Details / C2 in C
« on: January 23, 2019, 01:12:38 AM »
I'm implementing a version of the C2 parser in C right now. Not all that much to see. It parses all of C2 as far as I know (...probably a quite a few bugs...) and mostly follows C2C's architecture. Reading recipe files and a some other stuff and also the semantic analysis is just started.

However the idea is that the parser is prepared from the first for some compile time evaluation. For example, this would be completely valid and resolved during compile time:

Code: [Select]
Foo foo[sizeof(i32) > 0 ? 1 : 2];

I'm intending to have everything constant folded by default to open up for some lightweight compile time resolution. I suspect it will help with future semantic macros and generics.

That said it's far from being done.

10
Ideas / A brief and simple generics proposal
« on: December 28, 2018, 08:04:34 PM »
Since we want semantic macros we need to consider the usecase where C uses macros to create generic code. Here is a sketch of how that could be implemented.

1. A generic implementation is implemented in a generics module:

Code: [Select]
module vector (A, B, C); // A, B, C are generic parameters.

type Foo struct {
   A a;
}

func C test(B b, Foo *foo) {
   return a + b;
}

To use this generic code, simply import:

Code: [Select]
import vector(f64, f32, i32) as generic_tests;

func f64 test()
{
   generic_tests.Foo foo = { 2.0 };
   return generic_tests.test(3.0, foo);

Thanks to the namespacing in C2, we can don't actually need to create an alias if we want to. The module is all we need in order to make this work.

One issue

There is only one issue here. This solution has pretty much the same issue as C++ templates – lack of good error messages.

A solution would therefore be to set up a clause with contracts the types must pass. This is similar to the solution proposed in C++ / Go2:

Code: [Select]
module vector (A, B, C);

contract(A a, B b, C c) {
   a + b;           // Only expressions allowed
   c == a + b;   // Only expressions allowed
}

... code ...

A generic is not completely error checked until it is instantiated, with the contract however, the expressions are FIRST validated, and reported as errors.

Code: [Select]
import vector(struct Bar, f32, i32) as gen_test;

// This would give the error
---> Illegal arguments for generic module vector, breaks contract 'struct Bar' == 'f32' + 'i32'

This proposal is minimal, does not clutter the language with type arguments (i.e. no Foo<f32> etc), prevents misuse and requires very little extra syntax and complexity.

11
Implementation Details / Implementation based on Clang or...?
« on: December 20, 2018, 08:25:20 PM »
There are two lightweight C-based compilers of C, TCC and recently 9cc.

One idea would be to implement the C2 compiler on top of either of those instead. TCC is known for it's very fast compilations speed. The fact that both are written in C allows us to potentially implement them in C2 instead.

Possibilities

1. Use C2-Clang/LLVM to compile a mod of 9cc/TCC rewritten in C2
2. Write C2-9cc/TCC which then is piece by piece replaced by C2 implementation.

Obviously this is something for the future and should not detract from the current work on C2-Clang/LLVM.

12
Ideas / Extended switch statement
« on: December 16, 2018, 04:08:21 PM »
Something I’ve been considering is an extended switch statement that is a structured if-else. So consider something like this:

Code: [Select]
switch (x) {
  case x > 200: return 0;
  case x < 2:
     small_x_warning();
     break;
  case 0:
     ....
  case x > y && a < 1:
     ...
}

So it’s a glorified if-else actually. It has better readability over if-else though.

I’m thinking about some other things:

  • In the code above x is explicit i conditionals, but that does not make sense for switch (foo()) {...}. However using ”broken” expressions is harder to parse and sometimes impossible. For simple cases we can do ”case >100:” but what about the case of x > 10 && x < 20? It can’t be expressed that way.
  • A solution to the above is to allow the switch to introduce switch scoped variables: switch (int i = foo()) { ... }. Multiple variables could be permitted: switch (int i = foo(); int j = bar()) {}
  • If using (2) we need to decide whether additional variables could skip initialization if they aren’t used in the case.
  • I think there is a sweet spot between the plain switch and full pattern matching, especially when dealing with ranges and matching strings.

Has anyone given this any thought?

I think C2 should certainly /evolve/ switch-case a bit. But how far and if it should be a completely new statement type or not - that’s something I’m unsure of.

The main two use-cases for me to replace (1) long if-else chains (2) switch-case where we really want ranges instead single numbers and often need to use if-else instead of switch.

13
Ideas / Error handling [proposal]
« on: December 05, 2018, 02:34:37 PM »
The main idea is to pass results using an error channel that is separate from the underlying result. The call is compatible with C.

A return value can be described as:

Code: [Select]
struct _result {
   union {
     Result result;
     Error error;
   }
   bool error;
}

Where "Result" is the result type which will vary by method.

For example, consider a method createFoo() that could return an error or a Foo*. The C signature would look like this:

Code: [Select]
struct _resultFooStar {
   union {
     Foo* result;
     Error errorResult;
   }
   bool error;
}

struct _resultFooStar createFoo() {
   ...
}

For C it can then be used in this manner:

Code: [Select]
struct _resultFooStar res = createFoo();
if (res.error) {
  // Do something with res.errorResult
  return;
}
Foo *foo = res.result;

For C2 we obviously want some more syntactially pleasing. The tricky part is to assign both error and normal result, even though they are exclusive.

Go would write something like foo, errorResult := createFoo() but this still requires an extra test and is something they actively work on to improve. Not to mention that C2 doesn't have tuples.

Keyword or not?

We can decide on using keyword (catch, rethrow) or use symbols, e.g. !!, !? etc. Below I will offer both versions with keywords and without keywords.

Rough common ground

First of all we need a way to rethrow any error to the top. In Go this would be:

Code: [Select]
res, err := createFoo();
if (err) return nil, err;

Zig on the other hand uses "try" to signal that it will rethrow:
Code: [Select]
res = try createFoo();

Here I suggest we use the same method as Zig, but with postfix:

Code: [Select]
res = createFoo()!!; // !! to signal rethrow.
res = createFoo() rethrow; // using keyword instead.
res = createFoo(); // compile time error since error is not handled.

Secondly, we need a way to ignore the error and replace it with a dummy value if there was an error.

In Zig:

Code: [Select]
res = createFoo() catch defaultFoo; // Return defaultFoo in case of error.

For C2 I propose the following:

Code: [Select]
res = createFoo() !! defaultFoo; // The idea is using !! as "||"
res = createFoo() ?! defaultFoo; // Instead borrowing from ?: syntax
res = createFoo() catch defaultFoo; // Using keyword catch
res = createFoo() else defaultFoo; // Using keyword else

Different possibilities

For handling errors we have two major directions (that actually can be used together). One is using error handlers defined separately, the other is handling direct at the callpoint.

We can look at Zig's handling:

Code: [Select]
if (createFoo()) |foo| {
  doSomethingWithFoo(foo);
} else |err| switch (err) {
  error.FooCreation => {
    // handle this...
  },
  // handle next error here ...
}

In the Go2 proposal we have handlers:

Code: [Select]
handle err {
  if (err == "FooCreation") {
     // handle this
  } else {
    // Handle other error
}

foo := check createFoo()

C2 errors with inline error handling (like Zig)

Code: [Select]
// Using !!
FILE *f = fopen("Foo") !! (err) {
  // Switch-like structure opened by default
  case error.FooCreation:
    // handle this...
  case ...
   // handle next error here ...
};

// Using catch keyword
FILE *f = fopen("Foo") catch (err) {
  // Switch-like structure opened by default
  case error.FooCreation:
    // handle this...
  case ...
   // handle next error here ...
};

// Using !=> with error variable being implicitly defined (does not allow nesting)
FILE *f = fopen("Foo") !=> {
  // Switch-like structure opened by default
  case error.FooCreation:
    // handle this...
  case ...
   // handle next error here ...
};

The downside of the above is when wanting to handle things in an if statement. Consider:

Code: [Select]
while (Foo* foo = createFoo()) {
   ...
}

Now envision error handling of the above type. Not pretty. Here error handlers shine.

C2 errors with error handlers

Code: [Select]
handler errHandler(err) {
  case error.FooCreation:
    // handle this...
  case ...
   // handle next error here ...
}

Foo *foo = createFoo() catch errHandler;

while (Foo* foo = createFoo() catch errHandler) {
   ...
}
;

This also allows for reuse of handlers at several points in the code.

Note that they are not mutually exclusive, they can actually coexist.

Examples

Finally I end with a few examples that can be used to imagine real life situations:

Rethrowing or using defaults:

Code: [Select]
if (a_may_throw() rethrow + 2 > 0) {
    // do something
}

if (a_may_throw()!! + 2 > 0) {
    // do something
}

if ((a_may_throw() !! 0) + 2 > 0) {
    // do something
}

if ((a_may_throw() ?! 0) + 2 > 0) {
    // do something
}

(I'll add some more later)

Proposals rules out

I considered a solution like this:

Code: [Select]
FILE *f ! err = fopen("Foo");

// read(f) - Would be compiler error
// as err is not tested in the scope

if (err) {
    printf("Found error! %s", err);
    raise err;       
}   

// f can be used here since err is checked

But it did not seem attractive enough compared to the other methods (inline and error handlers), and it also requires more analysis and special syntax / grammar without adding clarity.

14
Implementation Details / CTC (partial, full, none)
« on: December 01, 2018, 05:34:50 PM »
I'd like some clarification on how this one works and what the expectation is for each state.

15
Ideas / Typeinfo at runtime
« on: December 01, 2018, 10:59:10 AM »
I want to suggest that type info should be available at runtime. There are various way to implement this. In the end, the type should only be reduced to a number that’s used to access a generated a set of functions that are built during code generation.

To do this we need to introduce another built in type, ”vtype”.

Code: [Select]
vtype x = typeof(a);
bool y = is_struct(x);

What’s interesting here is for scripting and very dynamic plugins.
A function may take a void* plus the vtype and work on the data as if it was the given type, even though the code was built BEFORE the type was written.

The most general use for this is to dump structure data for debugging.
The macro can then be something like:

Code: [Select]
#define DUMP(x) dump_data(typeof(x), (void*)&x)

(Very rough, but hopefully you see what I mean)

dump_data here is a FUNCTION not a macro.

This is just a discussion starter. Add to the idea!

Pages: [1] 2 3 ... 5