Jam Programming Language

32 points by bittermandel


eschew

Trait solving is a search problem. Borrow checking is a whole-function region analysis. Monomorphization multiplies the code volume before LLVM, the slowest stage, even sees it. None of this is waste from Rust’s point of view; each pass buys a guarantee. But you pay for all of them on every build.

[...]

The analyses Rust spreads across HIR and MIR, the drop placement, the init-before-use check, and the call-site exclusivity rule from the earlier sections, all run as local dataflow passes over JIR rather than as a whole-program lifetime-and-region inference. And because Jam annotates a type at every binding, the front end does far less of the global type inference and open-ended trait search that dominate rustc’s middle.

This LLM-generated writing is doing something I think engineers, especially younger engineers, should be wary of: substituting qualitative/evocative prose in lieu of quantitative data. Persuasion via storytelling is easier (for both author and audience) than going through the hassle of collecting and analyzing hard numbers. Human brains love stories, and stories work best when they're simple and clean. Real world data often reflects a complicated world filled with as much nuance as you care to look for.

Compare and contrast this quantitative blog post profiling the Rust compiler by a rustc contributor.

jmillikin

Is the primary difference from Zig the presence of drop and absence of specific easily misused constructs (undefined)?

Maybe(T) is a regular generic struct with three operations: empty() constructs a slot whose contents are not yet meaningful, write() fills it, and unsafeAssumeInit() extracts the value. The naming carries the safety story. Every consume site contains the word unsafe, so reviewers (human and AI) can grep for it and find every place a runtime invariant is being asserted.

OK so there's no undefined and every value must be initialized, but also Maybe(T).empty() returns something with contents that are "not yet meaningful", and presumably immediately following that a unsafeAssumeInit() would return garbage. So it's not "safe" in the sense that Rust is, where the compiler treats unsafe as a taint that requires an explicit unsafe { .. } for un-taint.

This is the code sample used to indicate safety and drop functionality:

const File = struct {
    fd: i32,
    fn drop(self: mut File) {
        close(self.fd);
    }
};

export fn useFile() i32 {
    const f: File = { fd: 7 };
    return f.fd;
}

Do I have it wrong, or is this unsafe? Ignoring the manual file descriptor assignment, it's going to call close(7) and then return 7 -- there's no lifetime tracking so no way for the user to express that the lifetime of the file descriptor ended before useFile() returned.

Jam doesn’t have this seam, because the thing that makes Rust’s ABI unstable doesn’t exist in Jam. There are no first-class references, no lifetimes, no niche-packed layouts to erase. A Jam value is value-shaped all the way down, the same property that made the collection APIs by-value earlier, so a Jam struct already has a C-compatible layout. There is nothing to translate.

In the given example, where export fn counterAdd(c: mut Counter, n: i64) i64 { .. } becomes int64_t counterAdd(Counter *c, int64_t n);, how do you express whether c is allowed to be NULL or not? Rust has a well-defined ABI for that, you can have extern "C" fn counterAdd(c: &mut Counter, n: i64) -> i64 or extern "C" fn counterAdd(c: Option<&mut Counter>, n: i64) -> i64.

No repr annotation, no raw-pointer cast, and crucially no unsafe.

But the Rust version doesn't need to have unsafe either. You can define the API with references. Ironically the only place where unsafe might be needed is for #[no_mangle] which is #[unsafe(no_mangle)] in more recent Rust, but the example constructs Rust that uses raw pointers for some reason.

Later on it gives this example:

extern fn snprintf(buf: *mut[] u8, size: u64, fmt: *const[] u8, ...) i32;

fn render(value: i32) i32 {
    var buf: [16]u8 = [0; 16];
    return snprintf(&buf[0], 16, "n=%d", value);
}

Shouldn't there be an unsafe there? Somewhere? snprintf is taking raw pointers so per the earlier guidance about unsafe operations being discoverable by name, it should be unsafeSnprintf with some sort of symbol override.

One honest caveat: at the extern line you are talking to C, and C’s rules win.

Hmm