What is the one features you would want in Zig from another language?

It’s a fair point, I don’t want the maintainers to have too much on their plate either they are already doing crazy stuff, and have really tough battle to fight. Also as far as I remember I don’t think they were totally against the idea. It’s just not their priority at all, same with comptime interfaces, they don’t want it right now, but they haven’t discarded the possibility that it might be part of the language in the future. In any case they know what’s good for the language, better than I do. And I might give it more thought on how to do it in Zig, in a way that I like, I’m still relatively new to the language, and It’s really everything I wish C was. Still once you’ve used “match” and traits, you just want them everywhere, it might also be a bad idea in the first place.

Also thanks for the detailed explanation, I wasn’t really sure how they were doing the stack trace. :slight_smile:

1 Like

Constraints on value ranges within the type system in combination with type refinement. This is a feature of ATS2 (also LiquidHaskell) which allows you to express that a parameter must be of a given range (e.g power of two fn (x: u32) u32 where (@popCount(x) == 1) and fn (x: u32) r: u32 where (r >= x)).

This guards against wrong parameters, makes if (slice.len < 4) slice[5]; always a compile error as the compiler knows that the above case is impossible, etc. This also applies to assertions as with inline fn assert(ok: bool) void { if (!ok) unreachable; } the compiler is then capable of warning you with a compile error that your use of the values later on violates the assertion and thus is invalid.

It would then also allow for switch case narrowing such that:

var e: E = getExternalE();
switch (e) {
  .a, .b, .c => switch (e) {
    .a, .b, .c => comptime assert(e != .d and e != .d and e != .f),
  },
  .d, .e, .f => comptime assert(e != .a and e != .b and e != .c),
}

Is valid code as the set of possible values for e within the switch branch is comptime known. Same for if, while, for`, etc.

Thus bool now means something more than just “something is true” as const k: bool = x > 5; carries the information that x is indeed larger than 5 if true and thus a branch on k makes that proof available.


The general pitch for this is safety around bounds, calculations that may overflow at runtime (there have already been plenty issues around this with non-power of two integer sizes), and better dead code elimination.


The proposal itself proposal: learn comptime-known-ranges from branches on runtime values · Issue #12872 · ziglang/zig · GitHub

4 Likes

Some features I miss from Julia are:

  1. Lisp-style macros that transform an abstract syntax tree (AST) to another AST at parse time. This will allow the designers of Zig libraries to come up with more creative and user-friendly APIs using domain-specific sub-languages of Zig that exploit valid Zig syntax. Valid Zig syntax is one that parses but doesn’t necessarily compile prior to the macro expansion. This will also likely replace the need for operator over-loading and can be used to implement some version of function over-loading by wrapping function definitions with the same name in a macro and then using the macro to combine the individual method bodies with comptime if statements into a single function.
  2. Function return value type inference. I don’t have too much experience using Zig, but one thing I miss from Julia is its really good function return type inference. There is no reason why Zig can’t have that in theory, I think. Even in cases where type inference fails, giving the compiler type annotations/hints in the function body is often sufficient to allow the compiler to infer output types on its own. For functions returning deeply nested structs and multiple outputs, it can be very tedious to figure out output types from input types using comptime functions, possible but tedious.
  3. Just-in-time (JIT) compilation, interactive development and introspection. Interactivity is very useful when learning a new language or library, e.g checking types of objects or probing around fields of objects. LFortran (https://lfortran.org) is an example of a statically typed language with a JIT compiler. I am sure there are others.
  4. Static multiple dispatch. Julia has dynamic multiple dispatch semantics but if type inference works and all types are known at compile time, dispatch happens statically at compile time giving zero overhead. Julia also has well tested dispatch rules deciding which method is more specific than another in every case, handling ambiguity at compile-time. This feature can probably be implemented as a library if one can do macros in Zig.
1 Like

I am sorry but this thread is not helpful anymore. It quickly devolved into a laundry list of all possible features and requests that go against Zig’s philosophy of being explicit, reasonably simple, orthogonal and very fast (both run-time and compilation speeds). For example, a request for a macro system has little sense in the presence of comptime.

5 Likes

I respectfully disagree. Please explain how domain specific languages and function overloading can be implemented using comptime. Perhaps it makes little sense to you but in the applications I work with, it makes a lot of sense.

I agree with @slonik-az I think we also need to be mindful of how a feature can play with a language. Macro, that play with the AST, are not really aligned with Zig’s core values, of performant, explicit, and maintainable code. My respect goes to people smart enough to figure how to modify the AST to create DSL to solve their problems, but Zig doesn’t seems like a language suited to do that. The idea behind that topic was mainly to see what features might miss from Zig but also how someone see them fit in the language. There is plenty of language out there that have said feature, but I don’t think that Zig should be one of them, there’s a fine balance between making a language feature-full and bloated. I think someone looking for that particular feature should use another more appropriate language instead.

4 Likes

Have you seen The Road to Zig 1.0? I think it clarifies why Zig doesn’t have some of these features.

4 Likes

Macro system is redundant if you have comptime. This is the whole point – replace macros with comptime. Now tell me how are you going to add macro system on top of comptime and implement lazy evaluation and incremental compilation? These tasks are already super hard as they are without macros.

[I am signing off from this thread. Do not want to waste my time, sorry.]

@pierrelgol I appreciate your nice response and respect Zig’s overall philosophy. For examples of DSLs that make for killer APIs, see Turing.jl for Bayesian inference and JuMP.jl for mathematical programming.

@dude_the_builder thanks for the pointer.

@slonik-az I would hate to waste your time. You could try to be a little nicer in the future though. A stuck up community is really bad for language adoption.

4 Likes

I apologize if the tone of my message offended you. It was never my intention.

7 Likes

A big part of the reason that they’re cutting back on language proposals (from my understanding) is due to wanting to faster compilation overall. @andrewrk has already stated that they’ll support features that fit the faster compilation model and break features that don’t. So @pierrelgol is correct as far as I’m aware - it’s not that these things don’t have a future home in Zig, but they’re not the priority right now. From the core team’s perspective, they probably don’t want to lay a bunch of floorboards only to rip them back up again.

Regarding the nature of this thread… I personally never took this thread to be dedicated serious language proposals only. I think it’s okay to do some off-roading in a public way so long as we’re all clear that certain things probably won’t be considered on principle.

3 Likes

Totally fair, It’s a very sensible approach, adding too much features, when you don’t have a very polished and stable compiler doesn’t make much sense in the long term, especially for the maintainers. Once they have a fast and correct compiler, they’ll have a lot more freedom to explore “features”, if they deem them worthy of attention, which seems like a healthy way of improving the language. On top of that I’m also a big believer that LLVM is also a bottleneck in exploring what the language could offer in term of semantic.

2 Likes
  1. The issue is that this doesn’t work with the goal of being explicit and makes code much harder to reason about which is generally why it’s not in the language currently. comptime is a decent in-between here as it offers the good parts that macros can provide (optimization, generating cases, etc) while avoiding the difficult to debug parts of it. EDSLs do help with cleaning up domain logic but they hide complexity which one will eventually have to pay for.
  2. This is an open proposal Proposal: `@Result` to match cast builtins inference API · Issue #16313 · ziglang/zig · GitHub
  3. JIT is explicitly not on the roadmap Runtime code generation · Issue #6691 · ziglang/zig · GitHub while Hot Code swapping is hot code swapping · Issue #68 · ziglang/zig · GitHub so halfish of this is already planned but with several open questions on how it should work.
  4. In zig you can have a look at the std/mem/Allocator.zig interface for dynamic dispatch and switch statements (also inline else in switch statements) for static dispatch. I don’t think there’s much of a need for any other scheme that cannot be implemented using current comptime semantics.
2 Likes

Thanks for all the pointers and explanation. I like the direction Zig is going. I guess if I really want macros, I can just build my own superset of Zig with a very shallow transpiler!

2 Likes

Not a feature, but different semantics about mutability. Currently, if I pass something by value or const ref, I can still modify it’s content if it holds a var pointer to it. I would mutability to affect the pointers inside the data. Like this:


fn function(mut: *Type, immut: Type) void {
    // legal
    mut.ptr.* = ...;
    // illegal
    immut.ptr.* = ...;
} 

I mean it would probably change the way zig handles parameters. But it would be so much easier to track side effects!


(please forgive me, I just had to share this cartoon :innocent:)

7 Likes
  • *Type means that the pointer points to mutable data.
  • * const Type means that the pointer points to immutable data. It is similar to the slice []const u8 used for immutable strings.
fn function(mut: *const Type, immut: Type) void {
    // illegal
    mut.ptr.* = ...;
    // illegal
    immut.ptr.* = ...;
}
1 Like

Exploring what you can do with comptime alone is also an option given that most cases where macros are used can be solved with it. I would also ask if zig is the right language for what you’re trying to do if you say you really need a macro system for your EDSL to be comfortable to use.

Maybe it’s easier to write a separate assembler/compiler for the DSL you want which produces the in-memory format you need for your program? This avoids complicating your zig code while allowing you to procide a better development experience overall as you can have DSL specific errors rather than zig stack traces. This is at least the approach I’ve taken for describing actor behaviour which also allowed for optimization based on domain knowledge which would be lost / complex if done using a zig macro preprocessor.

Working with the language is easier than trying to work around it / force it to be something that it isn’t.

3 Likes

Alright, I throw one in here for fun.

Namespace injection via keyword. This is a tricky one to explain, so I’ll do my best.

Jai has a very interesting take on this and it has powerful implications for refactoring. I’ll probably open a brainstorming post on this at some point because I’m not yet convinced that it cannot be achieved in Zig.

Let’s take a simple example…

Say I have two structs - one called A and one called B.

const A = struct {
    field_1: Type1 = ...
    field_2: Type2 = ...
};

const B = struct {
    field_3: Type3 = ...
}

Let’s say I compose them with another type C

const C = struct {
    a: A,
    b: B
};

Now I’ll make a function (or a bunch of functions) that use type C:

pub fn foo(c: *C) bar {

    doSomething1(c.a.field_1, ...);

    // later...

    const baz = c.b.field_3  + c.a.field_2....


   // etc...
}

Now at some point, I decide "Hey, field_2 really belongs to B, not A

Well, that means everywhere I wrote c.a.field_2 now needs to become c.b.field_2.

With namespace injection, you can write:

pub fn foo(c: *C) bar {
    using c.a;
    using c.b;

    doSomething(field_1);

    // later....

    const baz = field_3 + field_2...
}

And now it doesn’t matter if A or B contains field_1, field_2, or field_3. No refactoring is required for foo. It’s very powerful and some old Jai demos showed what could be done with it. I don’t recall if it made it into the language (I haven’t checked in a while), but I like that idea. Of course, you could have name collisions but we have shadowing and collisions rules aleady.

2 Likes

I’m going to write a Doc for this because this is a common misconception about pointers.

1 Like