Success in programming languages is not simply if-you-build-it-they-will-come. Bootstrapping a community that loves programming in it is the hard part. You'll either need a killer interop story with an already built ecosystem, or you'll need your own HTTP server, JSON parser, etc. etc. etc.
Best of luck! I'll watch this space.
That is good advice, I’m definitely planning on including some batteries soon, once I’m very happy with where the compiler is at.
On that note: allow me to "import" or "use" a C header without writing any FFI code.
if you're all-in on ai, you want to prioritize familiarity for agents.
Also, what you're saying implies Python and Rust should stop evolving as languages, as changing the language would invalidate the vast quantity of work the AI has been trained on.
I disagree. It's not worth changing languages for new ones if you want to use AI, unless you can manage to create so much training data that it ends up baked into the models.
> Also, what you're saying implies Python and Rust should stop evolving as languages
Yes, much the way x86 assembly has stopped evolving for the user, outside of smallish additions like wider vector ops. Adding small, high value extensions extensions can be worth paying the token cost and increased failure rates if they add strong new capabilities.
If it's just ergonomic, it's a mistake, unless you want to target hand coders.
There are a shrinking number of reasons to geenfield projects with non-strict or unsafe languages now.
Mine has a formally proven borrow checker built on linear types, along with algebraic effects, with an LLVM compiler written in lean -- the type checker uses exactly the proven calculus.
If you are going to build a language with claude, I highly, highly recommend writing it in Lean or Coq and starting by formalizing a pre-existing paper or system that has the properties you want.
Claude gets confused and takes shortcuts constantly, and if you don't have a proven base, you are really going ot struggle. I had to throw away months of work as i got bogged down trying to resolve special cases and bugs that just evaporated when i switched to a proven calculus.
That is exactly my motivation. The purpose of adding effects to my language, for example, was at least partially so you can track changes to things like IO usage in PRs. I also plan on adding a simple proof DSL that surfaces some lean functionality directly into the surface, so you can prove invariants about code as in this language, or surface exactly what is being trusted when writing the equivalent of unsafe code.
The main thing I am trying to solve is the LLM code review problem, really.
1. I'm curious how this handles I/O? Proving stuff is often really useful for distributed systems where we would like to prove our system behaves correctly under any ordering of I/O operations.
2. How does this compare to just using Rust and some proof tool like Kani or Verus?
let x = vec![1,2,3]; let a = &mut x[0]; let b = &mut x[1]; swap(a, b);
This is not allowed. And actually in the compiler’s mind it’s for the same reason as this:
let x = vec![1,2]; let a = &mut x[0]; x.push(3); a = a + 1;
Which is definitely not safe and should be rejected. But the compiler cannot reason that the above case is actually safe, because they both fail to compile for lifetime reasons.
Another thing I’m working on are “patterns” which are like enums on steroids. In Rust you cannot define how an enum is laid out, thus requiring optimizations for types like Option<&T> to be in the compiler. In evergreen this will be in the language via a similar process to trait implementation.
How are you going to solve the problem with other means? Lifetimes basically guarantee, that there are no use-after-free bugs.
Each “where” is called a plane, which is a tree. The stack plane contains all your function calls memory, a structure allocated on the stack has a child plane for each field. The compiler tags which plane something is from at compile time and tracks when it dies.
Things do get complicated in scenarios like:
fn max(a: &usize, b: &usize) -> &usize {
if *a > *b { a } else { b }
}In such a scenario the compiler tags the result with both of the input planes. However, you cannot define add a postcondition ensures clause to make it more precise
let x = vec![1, 2, 3]; let a = &mut x[0]; let b = &mut x[1]; swap(a, b);
Because Rust doesn't really know the difference between the whole Vec an element within the vec. Therefore it treats any mutation as one that could potentially invalidate anything with the same lifetime and doesn't compile. So I believe the plane model to be strictly better because it allows for this and still prevents the real scary thing:
let x = vec![1, 2]; let a = &mut x[0]; x.push(3); a = a + 1;
In a sibling comment I said the types seem more likely to be refinement (or Liquid) types instead of being full-spectrum dependent types. Is this correct? I certainly haven’t been through enough of the docs to be confident, but I took a swing when I posted my comment.
How are you doing resolution of your dependent(refined?) types? I've been using Z3, but you're clearly doing something else here.
There is some apparently AI-generated prose that needs rewriting for clarity. For example, in the tutorial:
> The caller proves where; the implementation proves ensures. A precondition failure stops compilation before any program runs.
Fron the readme:
> I think you get it now.
Nope, I skimmed, because the syntax is unfamiliar.
> Memory model is in a tree of "planes". The stack is a tree. Each function is a node. Each variable is another node. If it's a struct then it's also a node. Down to the most atomic types, which are just the integers.
Meaning what? This needs to be explained better.
Maybe, to put it another way: Why should people want to spend time developing feedback for you when you were not willing to spend commensurate time on the things you want to show them first? You're putting the cart before the horse.
Google has a two-part technical writing crash course:
- https://developers.google.com/tech-writing/one
- https://developers.google.com/tech-writing/two
I've had good success with adding a directive to the guide that points to those two URLs and tells the agent to adhere to them in all of its documentation.I'd assume, what you do is based on some research, with articles and other projects like yours - these references are what's missing from your project, you don't have to write them yourself.
The most important missing explanation, that only you can provide, is this: Why a new language? There are projects that attach proof obligations to standard Rust in order to reuse its vast ecosystem and existing LLM skills. That would be a far easier and more productive approach to take.
I wish we would stop xxx::yyy::zzz for namespace.
It was horrible in C++ from day one, and is still horrible in Rust.
It had some reason to be this way in C++ but not in any other newer language.
Real negative part: it looks to me that the authors of those languages are parroting other languages rather than understanding the reason of those language choice.
This simply worries me because I judge the level of expertise and makes me not consider the language in the first place.
(I know C++ and Rust are alive but they share their fair share of poor design choice)
From "A History of C++: 1979− 1991".
> In C with Classes, a dot was used to express membership of a class as well as expressing selection of a member of a particular object. This had been the cause of some minor confusion and could also be used to construct ambiguous examples. To alleviate this, :: was introduced to mean membership of class and . was retained exclusively for membership of object.
It was taken from Algol68.
Then, this was reused for namespaces for consistency reason.
I just like "dots" for everything, and there are some reasons to distinguish package/module qualification, struct/class member access (.), or static member access (::).
I don't get it. Is it syntactically horrible or are there real reasons that I'm missing?
I write Rust mostly for my day job (and it's also my preferred language on the side these days), so I usually end up explicitly importing all the symbols I actually use because module::func is just additional noise in an already pretty verbose language and is what's considered 'idiomatic'.
I don't really get the issue with :: being the separator used here. It's slightly ugly, but it's visually distinct from . for field access, which I do appreciate.
I don't mean this to sound combative, but I think you are the one who doesn't understand the reason for the language choice. I assume you're objecting to the fact that these languages use different operators for module/namespace paths (`::`) and object member/method access (`.`), not the `::` symbol specifically.
You seem to assume that Rust blindly imitated C++, but that's not the case. The `::` syntax allows the compiler to identify and resolve all the static paths before any type inference or other analysis. This is important because macros are expanded between these stages, and can in turn introduce more static names which must also be resolved before any further compilation.
You might be able to introduce a rule that any `.`-path that can be resolved statically will be, trumping any non-static members (or maybe not, it might cause ambiguities I'm not thinking of), but Rust's stated design philosophy favors explicitness.
I don't know what this language's reason for using it was, as I don't see any mention of macros. But in general it simplifies compiler design. If you can resolve all the static names up front, the following compiler stages can ignore the concept of namespaces altogether.
However, I believe Rust purposely imitated C++, for reasons. And I still don't like it.
A lot of language are doing fine using dots. And if you are going to use '::' you can have your reasons. I would still find it annoying to type and read.
Just to add precision: I critic the syntax (which is very subjective), and I critic the parroting, as two separated arguments.
I have implemented one such language, and while it is indeed doing fine, the dots-everywhere syntax really does complicate name resolution. It only uses it because the distinction between two different operators would be lost on its intended less-technical audience. It is a (in my opinion unfortunate) tradeoff made for a specific reason.
And one thing those languages all (as far as I know) have in common is that they do not have Rust's macro system. It may well be impossible to implement that without a distinct operator.
I have no opinion as to whether that operator should be `::` or some other arbitrary symbol, but I believe that no matter what symbol they chose, someone would complain that it doesn't fit their personal taste. Can't please everybody.
Your "overflow, then inspect" example illustrates this incoherency. If overflow is allowed, and you try to fit 250 + 10 into a u8, so the value should be 4, which is less than 10, so it should fit your precondition, not violate it. And this is a simple example where 250+10 is known at compile-time - what happens when a user provides the number at run-time? Does the compiler attempt to provide the proof anyway? How?
I mean.... I have a lot of respect for Haskell and Idris and the goals they set out. But they're not systems languages.
Your hardware does this OR You implement it in software
```
macro_rules! nonzero {
($n:expr) => {{
const {
assert!($n != 0, "Non Zero Condition could not be verified");
}
$n as i64
}};
}fn main() {
let x: i64 = nonzero!(0); // Throws verification error
};```
If you need something to be provably mathematically correct you should write it in Lean, or Coq, some kind of dedicated verification program, if you merely need to guarantee certain conditions at runtime, you can just write some macros! I understand that they want certain facilities like proof by induction, and the ability to prove arbitrary theorems but why would you do that when you can write it in a dedicated language (Lean/Coq) that has been around for a long time, with far more tooling?
That aside, I think Evergreen is still a nice language, and can have applications, its just the ecosystem around it that needs development.
fn foo(x: usize) -> NonZero where x != 5 { NonZero::new(x - 5) }
I do not believe this is achievable in Rust.
let l = text.len();
text.push_str(" has ");
text.push_usize(l);I understand the author has posted this, and explicitly several times in the README and here on HN has said this is a sketch and is for learning purposes and experimentation. That's great and I support such efforts wholeheartedly. So my criticism is given in the spirit that this project can be significantly improved.
However, the LLM presentation means the author has cosigned statements I don't think they should be cosigning.
For example, your homepage makes some pretty outrageous claims: for a language that didn't exist before last week, you're claiming it is "more powerful than C" and "safer than Rust". There is very little here to support these claims, and I think they should be retracted.
First, how is this language more powerful than C and why can't C adopt what this language does? Why can't C use your technique as a framework / preprocessor and do exactly what Evergreen does? If it's technically possible for C to do all this I don't think you can claim Evergreen is more powerful.
Also, I don't see any benchmarks, maybe I missed them. How can you claim the proofs don't affect performance? You say that the proofs are erased after they're checked but that doesn't necessarily mean there's no performance cost for having to write the code a particular way. To credibly make this claim you'll have to show proof.
And please when you do the benchmarks, make sure the AI actually optimizes the baseline version. There was another AI-authored language posted here recently which also made similar grand performance claims, but they didn't make sure they were being fair in what they were comparing: https://news.ycombinator.com/item?id=50238979. The AI will show your language is faster than Rust or C or whatever by writing suboptimal Rust / C.
When you say it's safer than Rust, you're leaving out that your system is incomplete and can't handle some things that Rust already handles, like sync + send. Moreover, the claim seems to be the existence of the features in your language defacto make it safer than Rust, but again that's not proven. Do they really? Also, several times in the Readme you claim Rust can't do particular things, when it actually can, just not statically e.g. using Rc and Refcell. In another case I don't think you're aware of https://doc.rust-lang.org/std/num/type.NonZeroU64.html.
Is the claim you can do those things where Rust can't, or that you can do them statically where Rust can only do it dynamically?
Finally, for some of these examples they seem to take a very long time to check, despite being so small. The overflow example takes almost 10 seconds on my machine, it's only 9 lines of code. How does this scale to an entire program? What's the largest program you've written to test this?
Anyway, I'm not saying any of this to discourage you, just to say that you really can't make the claims you're making without proof, and it's not enough to say "well the AI made the website and it's prone to making overblown claims without proof" -- no, when you publish them, the AI's words are your words. You can stand by what you can prove but otherwise you should be careful making grand claims.
This is an INSANE claim.
type NonZero = usize where self != 0;
fn main() -> NonZero {
let my_nonzero = NonZero(1);
my_nonzero - 1
}
Which doesn't feel right.I also get a weird error message pointing into the stdlib trying to do this:
fn main() {
let s = std::string::String::new();
let b = s.bytes;
s.len();
}
I assume because b takes ownership of s.bytes? Doing &s.bytes fixes it.