Skip to content

Releases: Neat-Lang/neat

Neat 0.7.1: I Can't Think Of A New Title Every Time

13 Mar 20:20
Compare
Choose a tag to compare

Implement quoted format string "$(a + b =)" => "a + b = 5".
-2.0 is now a float literal instead of an expression.
1f is equivalent to 1.0f.
Add Vulkan demo. Some C importer fixes.
--unittest now only enables unittest for the root package.
--unittest=name enables unittests for the name package in particular.
Also some caching fixes involving case().

Neat 0.7.0: Tentative Windows Support?!

24 Feb 10:45
Compare
Choose a tag to compare

For the first time, we have a Windows build in the downloads!

Note that you must have llvm-mingw with LLVM 15 installed and its \bin\ folder in the PATH for this to work. Then, running build.bat should produce a neat.exe that you can run.

Breaking change: the fail tuple attribute has been removed.
It was redundant with the Error class, which is supposed to be the superclass of every error class. For identical behavior, map the former fail type to breakelse with case.

The rest in v0.7.0 is mostly just small bugfixes.

Neat 0.6.0: The Cache What Was Promised

30 Jan 16:22
Compare
Choose a tag to compare
  • Breaking change: class fields without mut can no longer be mutated.
    This change is necessary to allow optimization of refcounting on access to immutable class fields.
    In other words, for a class field assigned to an immutable variable:
    Class obj = new Class;
    Field field = obj.field;
    
    The field access now incurs zero refcounting overhead.
  • Macros are now cached between compiler runs, providing a significant speedup on warm cache.
    As usual, if you get strange compiler or linker errors, rm -rf .obj.

I have been looking to add macro caching for years. The entire functionality of macro import is designed around it. And yet, there were always complications. Well, I've finally come upon a straightforward way that reuses the taskpool system to track changes in files that contributed to a macro ("bill of materials") without requiring a full reparse. The cache files are stored in .obj/macrocache_* and are simple JSON. Performance benefit varies widely based on project complexity, but should be "noticeable" in any case, with the greatest benefit seen in the testsuite, where macro compilation previously consumed the majority of runtime.

Neat 0.5.3: Prelude to Breakage

20 Jan 18:11
Compare
Choose a tag to compare
  • Assignment to index now works through alias:
    alias x = array[0]; x = 5;
  • Allow class fields to be declared mut.
    This as yet does nothing; it's just parser prep.

Obviously the big upcoming change in 0.6 here is that class fields will no longer be mutable by default. This will require a lot of rewriting; all I can say in excuse is that it is absolutely necessary for performance reasons. The upside of requiring mut on class fields is that class fields that are not mutable will no longer require a ref increase when accessing the field on a lexical-scoped expression. This should give a moderate but solid speedup going forward.

Neat 0.5.2: Finally Fast-ish

05 Jan 01:03
Compare
Choose a tag to compare

I finally figured out why Neat was unexpectedly a lot slower than D. Turns out, passing 16 bytes (a D array) on AMD64 is very speedy as it's just passed in registers; however, passing 24 bytes (a Neat array) requires an alloca because the SysV ABI demands it be passed as a pointer. For string heavy code, this forces a lot of allocas and ends up with programs spending most of their time doing stack shuffling.

Luckily, while we need to pass structs conforming to the SysV ABI, arrays aren't actually structs and we can decide how we pass them. So structs, tuples and sumtypes are now passed as separate parameters. This alone basically brings the benchmark to a 2x speedup and brings Neat, hopefully, within striking distance of D.

Neat 0.5.1: Make related_post_gen Fast Hopefully

01 Jan 22:01
Compare
Choose a tag to compare

We're not supposed to add optimizations directly for the benchmark. So have Neat 0.5.1, with a whole bunch of generic performance optimizations added for "no particular reason."

But first:

Semi-big language change: &var now requires mut.

You can no longer take the address of any variable not declared mut. This has been an open problem for a long time: the refcounter absolutely relies on mut to tell it if a variable is allowed to change. if you can just take a pointer to a variable and assign to it, that bypasses all protections. Since you can just make (almost) any variable mut this should not present any issues.

This used to be an issue for the refcounter, but as of this release neat will engage in more intense optimization of non-mut variables. As a result, if you somehow manage to mutate a non-mut variable, your changes may straight up not be visible in other parts of the code.

I'll take this opportunity to draw your attention to a gaping hole in the language. You can call struct methods on variables that are not mut. These methods can then mutate variables in the struct. If you do this, you will basically break everything. The long-term fix to this is to annotate methods with mut and only allow mut methods to mutate this. The short-term fix is to be careful with what methods you call.

__moveEmplace, __copyEmplace

These are mainly useful for macro writers. __moveEmplace(source, dest) will write source into dest without any refcounting. __copyEmplace(source, dest) will write a copy of source into dest. Note that neither of them refcount dest, so they should be used when initializing uninitialized variables and arrays. Use __copyEmplace if you wish to access source after the call; with __moveEmplace, this is illegitimate code.

Loop speedups

for (value in array) type loops no longer produce pointless bounds checks. This is, I want to emphasize again, a fully generic optimization that has absolutely nothing to do with any particular benchmark that Neat did absolutely terribly on.

And some small stuff

  • Fix LLVM backend spacer alignment.
  • Implement a %= b;.
  • Add std.algorithm.zip.
  • Vectors are now index assignable.
  • Add ArraySource, ArraySink
  • Add Time.year, month, etc. Add Time.monotonic for benchmarking.

Neat 0.5.0: Let me try something.

12 Nov 21:56
Compare
Choose a tag to compare

Again with the new syntax! When will it end?

Two big new things.

If let(var)

This month's first change is complementary with 0.4.0's breakelse feature.

You can now write if statements like so:

    if let(int var = expr?) {}

If you use the let form, the truth value of the variable expression will not be considered.
Only the breakelse branches from within the expression can prevent the if branch from being taken.
In other words, it acts equivalent to if (true) { int var = expr?; ... }.

(Why not just use that then? Some day, I'll probably disable breakelse outside the if expression.
It's just too confusing. But then, this idiom will still work.)

The goal is to enable usecases where, for instance, you want to conditionally access an integer
from a data struct, but you don't actually care about the value of the integer, only that it is present.
C interprets 0 as false because it has an impoverished typesystem that heavily overloads integers,
ie. see error codes and -1. I was (am) considering removing if's ability to parse integers as booleans,
but then people coming from C would be very confused, especially if there is another reason (breakelse)
why the branch might not be taken. Better to throw in a new syntax, at least that way
they know that they have to go look at the manual.

std.macro.easymacro

Previously, the only way to write macros was the cumbersome "load a function, the function instantiates a
macro class, which then hooks the compiler and adds a new syntax rule, which then..." workflow. This is
clearly not competetive with even C++, let alone D's CTFE. So I am proud to introduce std.macro.easymacro:

void test() {
    macro {
        print("Hello World from compile-time code!");
        code {
            print("Hello World from runtime code!");
        }
    }
}

The code statement can quasiquote-inject any variables from the macro block with the standard $var
syntax. code and macro blocks can be mixed and recursed arbitrarily.

Nested macro blocks can access variables from surrounding macro blocks directly.

If a code statement is evaluated multiple times, it is inserted into the surrounding function multiple
times.

The macro statement contents are loaded into the compiler and run at compiletime, just like regular macros.

A good example for how this all works is the new JSON stream parser at std/json/stream.nt.

JSON stream parser

Oh yeah, we have a JSON stream parser now. Basic types, arrays and structs can be automatically encoded
and decoded. See the unittests in std/json/stream.nt for examples.

fail annotations are out (ish), Error is in

There were always problems with the fail attribute. Why does it only work inside tuples? What happens if
you remove all non-fail types from a tuple with one fail type? As a result of these issues, the
functionality has been "soft-deprecated": it still works fine, but you are recommended to use
subclasses of std.error.Error instead. Error classes are also automatically returned by ?, but also if
the only return type is Error, ? correctly results in a bottom expression.

__CALLER__

__RANGE__ is a built-in expression that evaluates to the range expression of its syntax node.
Complementing this, we now have __CALLER__. If __CALLER__ is set as the default value of a LocRange
parameter, it will be set to the range expression of the call to that function.
Note that it is an error to use this expression outside a parameter's default value.

That's it! Enjoy!

Neat 0.4.3: Fixing else for once

12 Oct 20:31
Compare
Choose a tag to compare

Minor bugfix: In a if x else b and a else b, b is now correctly refcounted if it is a
local variable.

State of play is still:

auto var = obj?.field? else return false;

if (auto var = obj?.field?) { }

The compiler has been ported to use this idiom internally: field.notNull has been replaced across
the board with the somewhat more ruthless, if accurate, field? else die (die being a thin wrapper around exit(1)). The relevant commit serves as a practical demonstration of breakelse in the guise of ?.

Note that ? is overloaded between error propagation and breakelse handling: it will return errors from
a sumtype, and it will treat nullable T as a sumtype of (T | null) and breakelse the null,
but not both at once. This occasionally necessitated the appropriately-irate fun()?? else die.

Neat 0.4.2: Breaking else again

12 Oct 11:34
Compare
Choose a tag to compare

Okay, I'm restructuring breakelse again. Now, the ternary operator a if b else c fully supports breakelse and has a shortened form: a else b (the "short ternary operator"). In that case, the if-test is always true, so the else case can only be reached by breakelse or ?.

In exchange, .else() is removed entirely: it was redundant with ?, and the code ended up with two different operations in the same expression that did the same thing. a.else(b) should be replaced with (a? else b), or a.(that? else b) if you really need the property form.

State of play:

auto var = obj?.field? else return false;

if (auto var = obj?.field?) { }

Neat 0.4.1: Breakelse, amended

11 Oct 07:36
Compare
Choose a tag to compare

Some more polish on breakelse in this one.

  • expr.else now creates a breakelse targetable scope in expr.
    This is because if you have a feature called breakelse, and a
    property called else, one better jump to the other.
    (I'm a bit worried that I'm giving too much weight here to the
    term breakelse, which I basically picked out of a hat without
    too much thought, but it seems to be working so far.)
  • nullableObject? no longer returns null. Instead, it will breakelse.
    Generally, null is treated equivalently to :else in breakelse features.
    This allows nullableObject?.property.else(...) and generally nullableObject?
    in if statements.

There's a bit of an ugly detail here: .else must be called on a sumtype with
:else or nullptr_t (like nullable Object). So you end up with an expression
containing a chain of ?, and a final .else that ... also does
the thing that ? does. foo?.bar.else(...).

That is, what you may expect, foo?.bar?.else(...), won't actually work,
because bar? already got rid of the null. The reason for this is that it
is not entirely trivial from a compiler perspective to check if the else
branch actually ever gets taken. So allowing .else to be called on arbitrary
expressions would lead to code with .else properties that are not actually
needed (anymore).

This is one of the implementation details of this design that I'll very
likely revisit later.

edit: You know what? That's stupid and broken, I'll definitely revisit it. Look out for 0.4.2!