1 Punkte von GN⁺ 3 시간 전 | 1 Kommentare | Auf WhatsApp teilen
  • Jam ist eine Sprache vor Version 1.0, die die unmittelbare Nutzbarkeit von Sprachen der C-Familie beibehalten und zugleich Sicherheit, eine niedrige Lernkurve und hohe Performance ohne GC anstreben will
  • Im Kern stehen mutable value semantics und ein Rust-ähnliches Drop-System, bei dem der Compiler Ownership, Borrowing und automatische Bereinigung übernimmt, ohne Referenzen oder Lifetime-Syntax im Benutzercode offenzulegen
  • Das Initialisierungsmodell vermeidet sowohl undefined als auch implizite Nullinitialisierung; verzögerte Initialisierung und Out-Parameter werden mit Maybe(T) und der Analyse von unsafeAssumeInit() behandelt
  • export legt Jam-Funktionen mit C ABI offen, und Jam-Structs sind mit C-kompatiblem Layout entworfen, um den Aufwand für separate unsafe Shims oder repr-Annotationen zu verringern
  • Der Compiler befindet sich noch in der Bootstrap-Phase und ist in C++ implementiert, noch nicht öffentlich, und soll nach 108 unterschiedlichen Projekten in Jam als Open Source veröffentlicht werden

Welche Sprachposition Jam anvisiert

  • Jam ist noch vor v1.0, und die derzeit beschriebenen Mechanismen funktionieren zwar im Compiler, Details können sich vor der Stabilisierung aber noch ändern
  • Ziel ist eine sichere Sprache, die die Anmutung der C-Familie bewahrt, wie man sie aus Go, Zig oder modernem C kennt, dabei aber Bug-Klassen von C reduziert
  • Im Zentrum des Designs stehen zwei Achsen
    • Mutable value semantics nach Racordon, Abrahams et al. 2022
    • Das Drop-System von Rust
  • Ausgangspunkt ist die Einsicht, dass Teams in der Praxis gemischte Erfahrungsstände haben und weniger erfahrene Mitglieder eher Fehler machen; die Sprache soll deshalb schon vor dem Review mehr Fehler verhindern

Unterschiede zu Rust, Zig und C++

  • Rust hat eine starke Sicherheitsphilosophie, aber der Abstand zwischen „Rust einigermaßen benutzen können“ und „mit Rust produktiv sein“ ist groß, was die Lernkurve im Team zur Belastung machen kann
  • Zig bietet eine kleine, C-ähnliche Oberfläche und ein unmittelbares mentales Modell, ist aber auf Sprachebene keine sichere Sprache
    • uninitialized read, manuelle Bereinigung und die Verhinderung von use-after-free werden nicht auf Sprachebene erzwungen
    • Große Produktionsprojekte in Zig oder C++ verlassen sich stark auf Prüfwerkzeuge wie Valgrind, AddressSanitizer und Fuzzing
  • Im Zeitalter von KI wird ein großer Teil von Produktionscode nicht mehr von Menschen, sondern von Tools geschrieben oder vorformuliert; der Flaschenhals verschiebt sich daher vom Code schreiben zum Code Review
    • Das Codevolumen steigt, die Review-Oberfläche bleibt aber flach, daher muss der Compiler mehr Bugs abfangen

Automatisches Drop-System

  • Ein Binding in Jam besitzt den Wert, und wenn ein Binding eines drop-tragenden Typs den Scope verlässt, erzeugt der Compiler einen Drop-Aufruf
  • Der Beispieltyp File deklariert fn drop(self: mut File), und in useFile() steht nur const f: File = { fd: 7 };
    • Es gibt keine explizite Bereinigung, kein defer und keine Markierung für das Ende der Lifetime
    • Im LLVM-IR wird direkt vor ret ein call void @__drop_File(ptr %1) erzeugt
  • Der gemangelte Name __drop_File verhindert, dass Drop-Funktionen verschiedener Typen auf LLVM-Ebene kollidieren
  • self: mut File wird auf einen Pointer-Parameter abgesenkt, und die Aufrufstelle übergibt direkt die Adresse des Bindings
  • In Zig müsste dieselbe Bereinigung mit defer f.deinit() explizit angegeben werden
    • Entfernt man diese Zeile, verschwindet auch der deinit-Aufruf im IR
    • Ein Leak von File Descriptors entsteht, wenn der Programmierer die Bereinigung vergisst
  • Auch C++-RAII führt Destruktoren beim Verlassen des Scope automatisch aus, aber Jam übernimmt das einfache Drop-Modell von Rust
    • Komplexität wie die rule of 0/3/5 in C++, virtuelle Destruktoren, Exceptions in Konstruktoren oder Destruktoren, std::exit, std::abort, longjmp oder Signale soll vermieden werden
    • Jam hat pro Typ genau eine Drop-Funktion, die bei jedem Scope-Exit ausgeführt wird

Initialisierung und Maybe(T)

  • In Jam gibt es keine undefined-Werte, und ein Binding kann nicht ohne Wert deklariert werden
    • Jedes var und const verlangt einen echten Initializer
    • Bei Structs werden zuerst die Feldwerte berechnet, dann wird per Struct-Literal erzeugt und anschließend gebunden
  • Zig erlaubt var f: File = undefined; return f.fd;, was zur Laufzeit Stack-Müll lesen kann
    • Im Debug-Modus wird zur Sichtbarmachung von Fehlgebrauch mit 0xaa aufgefüllt
    • Im Release-Modus sind es beliebige Bytes
  • Go nullinitialisiert alle var, um Garbage-Reads zu verhindern, zahlt dafür aber auch bei Feldern, die gleich überschrieben werden, die Kosten des Zero-Patterns
  • Jam vermeidet sowohl undefined als auch implizites Nullsetzen
  • Für verzögerte Initialisierung und Out-Parameter verwendet Jam Maybe(T)
    • empty() erzeugt einen Slot mit noch bedeutungslosem Inhalt
    • write() füllt den Slot
    • unsafeAssumeInit() extrahiert den Wert
  • Ein Lint-Pass verfolgt, ob der Slot beschrieben wurde, und lehnt unsafeAssumeInit()-Aufrufe, deren Initialisierung der Analyzer nicht beweisen kann, als Compile Error ab
    • Das Präfix unsafe bleibt als per grep auffindbarer Anker für menschliche und KI-Reviewer erhalten

Scope-Exit, return, break, continue

  • Der Compiler verfolgt einen Drop-Scope-Stack und pusht an jeder lexikalischen Blockgrenze einen neuen Scope
  • Wenn ein Block endet oder per Branch verlassen wird, erzeugt er unmittelbar davor die Drops für die Bindings dieses Scope
    • Bindings in if, else, match-Armen, while- und for-Bodies werden am Ende des jeweiligen Blocks gedroppt
    • Ein return in einem verschachtelten Block droppt aktive Scopes vom innersten nach außen, bevor tatsächlich ret erfolgt
    • break und continue droppen erst die im Loop-Body geöffneten Scopes und springen dann zum Loop-Exit oder zur nächsten Iteration
  • Im Beispiel mit verschachteltem break wird outer am Ende von Iteration 0 gedroppt, und auf dem break-Pfad in Iteration 1 zuerst inner, dann outer

Parameter-Modi und das Entfernen erstklassiger Referenzen

  • Ob beim Funktionsaufruf ein Binding gedroppt wird, entscheidet der Parameter-Modus
  • Der Standardmodus ist ein schreibgeschütztes Borrow
    • Der Callee liest den Wert, und das Binding des Callers bleibt initialisiert
    • Bei der Rückkehr des Aufrufs findet kein Drop statt
  • mut ist ein exklusives Lese-/Schreib-Borrow
    • Das Binding des Callers bleibt auch nach dem Aufruf initialisiert
  • Nur move konsumiert den Wert
    • Der Callee erhält Ownership und droppt am Ende des Callee
    • Das Binding des Callers wird nach dem Aufruf zu Uninit, und ein Lesezugriff darauf ist ein Compile Error
  • Es gibt keinen Marker an der Aufrufstelle; f(x) sieht in allen Modi gleich aus
  • Jam hat keinen erstklassigen Referenztyp
    • Borrows können nicht in Variablen gespeichert, zurückgegeben oder in Struct-Feldern abgelegt werden
    • Parameter-Borrows existieren nur während des Call-Frames und verfallen beim Return
    • Lifetime-Annotationen sind nicht nötig, weil es keine Lifetimes gibt, die man daran hängen könnte
  • Auch Collection-APIs behalten eine wertförmige Gestalt
    • v[i] = x wird zu v.setAt(i, x) desugared
    • let y = v[i] ruft den Getter v.at(i) auf, der das Element als Wert zurückgibt
  • Die Exklusivitätsprüfung an der Aufrufstelle untersucht Pfadüberlappungen in den durch Argumente erzeugten Borrow-Sets
    • swap(p.x, p.y) ist wegen disjunkter Unterpfade erlaubt
    • moveX(p, p.x) ist ein Fehler, weil p und p.x sich überlappen

C ABI und FFI

  • Das native ABI von Rust ist instabil, daher müssen Werte über Distributionsgrenzen hinweg wieder in C-Form codiert werden
    • Das Dereferenzieren roher Pointer ist unsafe
    • Ownership wird manuell mit Box::into_raw und Box::from_raw übertragen
    • Wer Structs by value übergibt, braucht zusätzliche Annotationen wie #[repr(C)]
    • Werkzeuge wie cbindgen und abi_stable existieren, um die Handarbeit an dieser Grenze zu verringern
  • Jam geht davon aus, dass Jam-Werte durchgehend value-shaped sind, weil es keine erstklassigen Referenzen, Lifetimes oder niche-packed Layouts gibt
    • Jam-Structs sind bereits mit C-kompatiblem Layout entworfen
  • export legt Jam-Funktionen mit dem C-Calling-Convention und einem einfachen unmangled Namen offen
    • export fn counterAdd(c: mut Counter, n: i64) i64 kann von C als int64_t counterAdd(Counter *c, int64_t n); aufgerufen werden
    • Der Parameter mut Counter wird zu einem Counter * auf caller-owned storage abgesenkt
  • Der Funktionsrumpf auf Jam-Seite bleibt gewöhnlicher Jam-Code, daher gelten Drop, Initialisierungsanalyse und Exklusivitätsregeln an der Aufrufstelle weiterhin
  • In Richtung C werden C-Signaturen mit extern deklariert
    • Eine extern-Funktion folgt dem C ABI wortwörtlich
    • Die Parameter-Modus-Mechanik gilt außerhalb dieser Grenze nicht
    • Übergibt man C einen Buffer per rohem Pointer, verifiziert Jam nicht, was C mit dem Pointer macht
  • Was Jam bieten will, ist, dass die Jam-Seite standardmäßig safe bleibt und man beim Export einer Jam-Bibliothek über C ABI keine separate unsafe API-Spiegelung oder Shim-Schicht bauen muss

Pattern Matching

  • match in Jam hat die Form Pattern Block und verwendet kein =>
    • Das Scrutinee wird wie in match (opcode) in Klammern gesetzt
    • _ ist ein Catch-all-Arm
    • Die Arme werden von oben nach unten in first-match-Reihenfolge geprüft, ohne implizites Fallthrough
  • Ein zentraler Anwendungsfall ist der Opcode-Dispatcher eines Game-Boy-Emulators
    • Dabei werden 256 Basis-Opcodes und 256 Prefix-Opcodes verteilt
  • Auch Matching auf Enum-Payloads wird unterstützt
    • Das Variantenmuster matched auf den Tag und bindet Payload-Felder innerhalb des Arms an frische lokale Variablen
    • Der Compiler prüft die Exhaustiveness der Variantenmenge
    • Fügt man eine neue Variante hinzu, schlagen alle match-Stellen beim Kompilieren fehl, die diese Variante nicht behandeln
  • match funktioniert auch als Expression
    • Jeder Arm-Block erzeugt den Wert seiner abschließenden Expression
    • Alle Arme müssen denselben Typ erzeugen
    • Ein match muss exhaustiv sein
  • Intern wird jedes match über eine Entscheidungsbaum-Pipeline auf Basis von Luc Maranget 2008 kompiliert
    • Eine Kaskade von Integer-Literalen faltet LLVM simplifycfg bei Rentabilität zu switch und Jump Tables zusammen

Compile-Time-Design

  • Die Compile-Pipeline von Rust durchläuft mehrere IRs und Analysephasen
    • tokens → AST → HIR → THIR → MIR → monomorphization → LLVM IR → machine code
    • Trait-Solving ist ein Suchproblem, Borrow Checking eine regionsbasierte Analyse über die gesamte Funktion
    • Monomorphisierung vergrößert das Codevolumen schon vor LLVM
  • Die Pipeline von Jam ist kürzer ausgelegt
    • tokens → AST → AstGen → JIR → codegen → LLVM IR → machine code
    • Es gibt nur ein typisiertes IR: JIR
  • JIR ist bereits beim Erzeugen durch AstGen typisiert
    • Jam geht davon aus, dass es kein comptime-as-values gibt, das untypisierte Lowerings erzwingen würde
    • Drop-Platzierung, Init-before-use-Prüfung und Exklusivitätsregeln an der Aufrufstelle laufen als lokale Dataflow-Pässe über JIR
  • Weil jedes Binding eine Typannotation hat, soll der Aufwand für globale Typinferenz und offene Trait-Suche geringer sein
  • AST und JIR sind flache Datenstrukturen
    • Kleine Knoten fester Größe werden in zusammenhängende Arrays gepackt
    • Statt Pointern werden Indizes verwendet, übergroße Payloads landen in Side-Pools
    • Der Compiler soll cache-freundliche Arrays traversieren, statt Heap-allozierte Bäume zu verfolgen
  • Im Backend dominiert LLVM die Optimierungszeit von Release-Builds
    • Geplant ist eine Aufteilung mit Cranelift für Debug-Builds und LLVM für Release-Builds
    • Cranelift steht auf der Roadmap und ist noch nicht fertig
  • Der aktuelle Compiler ist noch eine C++-Implementierung in der Bootstrap-Phase, und es gibt noch keine zitierfähigen Benchmarks zur Build-Zeit
    • Aussagen zur Compile-Time sind Design-Behauptungen, keine Messergebnisse

Laufzeit-Performance und Beispiele

  • Ziel ist, dass Jam bei der Performance zu Rust und Zig aufschließt
  • Jam hat weder GC noch ein Managed-Memory-Runtime noch per-Allocation-Header
    • Das Codegen besteht aus geradlinigem LLVM-IR
  • Derzeit wird nicht behauptet, dass Jam bereits das Niveau von Rust und Zig erreicht
    • Rust und Zig haben über lange Zeit viel Arbeit in target-spezifische Intrinsics der Standardbibliothek, Auto-Vectorization-Hinweise, allocator-bewusste Container, Tuning von Hot Paths und LLVM-Pass-Tuning gesteckt
    • Auch Jam wird für die letzten 10–30 % dieselbe Art von Arbeit brauchen
  • Bei den bisher gemessenen Workloads liegt die Lücke nach eigener Einschätzung nicht in einer „anderen Klasse“, sondern innerhalb eines kleinen konstanten Faktors
  • Eine im Terminal laufende Tetris-Demo wurde in Jam geschrieben

Veröffentlichungsplan und offene Arbeit

  • Jam ist noch nicht öffentlich
    • Der Compiler existiert und funktioniert, befindet sich aber noch vor einer breiteren Freigabe
  • Für die Alltagstauglichkeit wird derzeit an Folgendem gearbeitet
    • stabile Oberfläche
    • Package Manager
    • LSP
    • Formatter
    • übriges Tooling
  • Es gibt noch Themen, die in eigenen Beiträgen behandelt werden sollen
    • Parameter-Modus-System
    • Exklusivitätsregel
    • Generics
    • Jam-Comptime
    • Standardbibliothek
    • Allocator-Systeme
    • Panic-Modell
    • MLIR-Exploration für eine GPU-Codegen-Pipeline
    • Rust-ABI-Arbeit für FFI
    • Cranelift
    • Pfad zu einem self-hosted Compiler
  • Geplant ist die Open-Source-Veröffentlichung erst nach 108 unterschiedlichen Projekten in Jam
    • Die Zahl 108 ist ein willkürliches Milestone, abgeleitet von den 108 Stars of Destiny aus Suikoden 2
    • Aktuell ist Jam bei einer kleinen Nutzergruppe im Einsatz; mit reiferem Tooling soll der Kreis erweitert werden
  • Early Access ist über die Beta-Liste auf jamlang.org möglich

1 Kommentare

 
GN⁺ 3 시간 전
Meinungen auf Lobste.rs
  • Trait-Auflösung ist ein Suchproblem. Borrow Checking ist eine Analyse über den gesamten Funktionsbereich. Monomorphisierung erhöht die Codemenge, noch bevor LLVM, die langsamste Phase, sie überhaupt sieht …

    Solche von LLMs erzeugten Texte tun etwas, wovor sich Engineers, insbesondere junge Engineers, in Acht nehmen sollten: Sie ersetzen quantitative Daten durch qualitative, plausibel klingende Prosa.
    Mit einer Geschichte zu überzeugen ist für Autor und Leser einfacher, als belastbare Zahlen zu sammeln und zu analysieren. Das menschliche Gehirn mag Geschichten, und Geschichten funktionieren am besten, wenn sie einfach und sauber sind. Reale Daten spiegeln oft eine komplexe Welt wider, die so nuanciert ist, wie man bereit ist, hinzuschauen.
    Man kann das mit diesem quantitativen Blogbeitrag zum Profiling des Rust-Compilers eines rustc-Contributors vergleichen.

    • Schon die Frage ist, ob man dieses Projekt ernst nehmen sollte.
    • Die Formulierung „quantitative Daten durch qualitative/evokative Prosa ersetzen“ gefiel mir besonders gut.
      Gute technische Texte können und sollten, wenn es passt, beides enthalten, dürfen aber nicht das verfehlen, was wirklich vermittelt werden muss. Nachdem ich eine größere Organisation für Assurance-Arbeit betrieben habe, ist mir klar geworden, wie schwierig technisches Schreiben ist; und mit der leichteren Zugänglichkeit von LLMs muss man vorsichtig sein, wie viel schlimmer dieses Problem werden kann.
  • Der zentrale Unterschied zu Zig ist, dass es drop gibt und dass eine bestimmte, leicht falsch zu verwendende Komponente, nämlich undefined, fehlt?

    Es gibt kein undefined, und alle Werte müssen initialisiert werden, aber Maybe(T).empty() gibt offenbar einen Wert zurück, dessen Inhalt „noch bedeutungslos“ ist, und wenn man direkt danach unsafeAssumeInit() aufruft, dürfte ein Müllwert zurückkommen. Damit ist es nicht sicher in dem Sinn wie Rust, wo der Compiler unsafe als Verunreinigung behandelt, die ein explizites unsafe { .. } erfordert.

    Das Beispiel, das Sicherheit und die drop-Funktion zeigen soll, ist dieser Code:

    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;  
    }  
    

    Wenn ich das nicht falsch sehe, ist das unsicher, oder? Selbst wenn man die manuelle Zuweisung des File Descriptors beiseitelässt, wird close(7) aufgerufen und danach 7 zurückgegeben. Ohne Lifetime-Tracking hat der Nutzer keine Möglichkeit auszudrücken, dass die Lebensdauer des File Descriptors vor der Rückgabe von useFile() endet.

    Wenn im ABI-Beispiel export fn counterAdd(c: mut Counter, n: i64) i64 { .. } zu int64_t counterAdd(Counter *c, int64_t n); wird: Wie drückt man aus, ob c NULL sein darf oder nicht? In Rust gibt es dafür eine definierte ABI, und sowohl extern "C" fn counterAdd(c: &mut Counter, n: i64) -> i64 als auch extern "C" fn counterAdd(c: Option<&mut Counter>, n: i64) -> i64 sind möglich.

    Auch die Rust-Version braucht kein unsafe. Man kann die API über Referenzen definieren. Ironischerweise ist die einzige Stelle, an der unsafe in aktuellem Rust nötig sein könnte, ungefähr #[no_mangle], das inzwischen #[unsafe(no_mangle)] ist; das Beispiel ist jedoch aus irgendeinem Grund so aufgebaut, dass auf der Rust-Seite Raw Pointer verwendet werden.

    Auch dieses spätere Beispiel:

    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);  
    }  
    

    Müsste hier nicht irgendwo unsafe stehen? Da snprintf Raw Pointer entgegennimmt, müsste es gemäß der zuvor genannten Richtlinie, dass unsafe-Operationen am Namen erkennbar sein sollten, wohl so etwas wie unsafeSnprintf und eine Symbol-Neudefinition geben.

    „Ein ehrlicher Hinweis: In der extern-Zeile spricht man mit C, und die Regeln von C gewinnen“ – hm

    • Ich habe es auch so gelesen. Allerdings ist es nicht anders als Rusts .as_raw_fd(), und dort gibt es dasselbe Sicherheitsproblem.
  • Was Rusts ABI instabil macht, gibt es in Jam nicht. Keine First-Class-Referenzen, keine Lifetimes, keine niche-packed Layouts, die weg-erased werden müssten

    Das missversteht die FFI-Stabilität der Rust-Standardbibliothek. Shared References, Mutable References, Box und ihre jeweiligen Option-Varianten haben alle eine definierte und stabile ABI. Daher ist der gesamte Box::into_raw/from_raw-Ablauf im Beispiel unnötig.
    Lifetimes existieren auf Binärebene überhaupt nicht. Wenn man sich dafür entscheidet, für ein Enum eine stabile ABI zu definieren, wird die Niche-Optimierung deaktiviert.

    Der Grund, warum die meisten Typen keine stabile ABI definieren, ist, dass man häufig gar keine stabile ABI will, weil man sonst das Innere des Typs nicht mehr ändern kann.

  • Jam ist noch nicht öffentlich. Der Compiler existiert und läuft, aber die breitere Veröffentlichung wird aufgeschoben, während an den Dingen gearbeitet wird, die ihn für den täglichen Gebrauch angenehm machen — eine stabile Oberfläche, ein Paketmanager, LSP, Formatter und der ganze Rest an Tools, die man erst bemerkt, wenn sie fehlen …

    Diese Entscheidung verstehe ich nicht. Es gibt einen großen Unterschied zwischen etwas Unvollständiges zu „releasen“ und einfach den Source Code öffentlich zu machen. Wenn man es später ohnehin tun will, was schadet es, das Projekt schon während der Entwicklung öffentlich zu machen?
    Der Vorteil wäre, dass Leute, denen die Richtung gefällt, es selbst ausprobieren und vielleicht sogar beitragen können. Natürlich ist im „Zeitalter der KI“ nicht klar, ob solche Beiträge netto ein Gewinn wären. Außerdem würde es den Leuten ermöglichen, besser zu verstehen, was gebaut wird, und die Behauptungen darüber zu bewerten, warum es großartig ist. Ohne das wird das Projekt deutlich weniger interessant.

    Außerdem gibt es auch Leute, die keines dieser Tools verwenden. Mein aktuelles Team kann sich nicht einmal darauf einigen, einen automatischen Formatter einzuführen, ist ansonsten aber großartig. Daher macht es kaum einen Unterschied, die Veröffentlichung aufzuschieben, während man solche Tools baut.

  • Die Leute versuchen immer wieder, „Rust ohne nervige Lifetimes“ zu bauen, und scheitern immer wieder. Ein anderer Kommentar hat bereits eine Art des Scheiterns behandelt: Das Problem, einen Teil eines dropten Werts zurückzugeben, entsteht, weil man keine Referenz zurückgeben kann. Ein anderes klassisches Problem ist dieses:

    let mut arr = vec![1];  
    let x = &arr[0];  
    arr.push(2);  
    // Was passiert, wenn man `x` verwendet?  
    

    Es gibt drei Antworten:

    1. Man lehnt es ab. Dafür braucht man irgendeine Form von Borrowing-Konzept. Üblicherweise ist das „shared XOR mutable“; nur mutable ist unbequem, nur shared ist unsicher
    2. Man erlaubt es. Weil es keine Referenzen über andere Variablen gibt und alles GC- oder referenzgezählte Pointer sind
    3. Man erlaubt es und verursacht zur Laufzeit undefiniertes Verhalten

    Für jede dieser drei Optionen gibt es gute Gründe, aber Jam scheint wie Rust Option 1 sein zu wollen, ist in der Praxis wegen der Wertsemantik aber eher Option 2. Wenn das bedeutet, dass alles kopiert wird, dürfte es verhindern, sichere und zugleich effiziente Datenstrukturen zu schreiben

    • Inko macht das meiner Ansicht nach ziemlich gut. Meine offensichtliche Voreingenommenheit muss man dabei natürlich ausblenden, aber es gibt dort ganz klar eigene Kompromisse
      Insbesondere wird es, wenn man den Borrow-Checker weglässt, deutlich schwieriger, stack-alloziierte Typen zu unterstützen, ohne mehrere Annotationen einzuführen. Ein Beispiel ist Kopieren beim Borrowing; sowohl Inko als auch Swift machen das so
    • Bei Jam weiß ich es nicht genau, aber Hylo-artige mutable Wertsemantik hat mit subscripts eine Form von Borrowing. Damit liegt es eher in der Mitte
    • Die erste Frage, die mir beim Lesen dieses Abschnitts kam, war: „Wie speichert man Referenzen in einer Struktur, wenn es weder Referenzen noch Lifetime-Annotationen gibt?“
      In der Sprachreferenz habe ich gesehen, dass es zwar keine Referenzen gibt, aber mut- und const-Pointer; zu deren Sicherheit konnte ich jedoch nichts finden
  • Ein großer Teil dessen, was Zig zu Zig macht, ist, dass es kein RAII gibt, und bei Rust ist es der Borrow-Checker. Aber bei dem Punkt, an dem diese Designentscheidungen hier ankommen – „RAII ohne Referenzen“ –, bin ich mir nicht sicher, wer das tatsächlich braucht
    Trotzdem denke ich, dass es in dieser Nische Raum zum Experimentieren gibt, und solche Versuche sehe ich positiv. Nur dieser Ansatz scheint es mir nicht zu sein

    Die Richtung, über die ich in letzter Zeit ständig nachdenke, ist eine Kombination aus Zigs comptime, Pony-ähnlichen Referenz-Capabilities, Lifetimes als Compile-Time-Werte und dem Branding von Lifetimes an Allocators
    Was ich mir davon erhoffe, ist, Zigs Allocator-Strategie um Referenzsicherheit zu ergänzen und Lifetimes zu bekommen, die kaum annotiert werden müssen

  • Neue Sprachen sind gut, aber ich mag es nicht, wenn alles nur ein LLVM-Frontend wird. Ich weiß, dass Backends schwierig sind, aber manchmal hätte ich gern auch andere Optionen

  • Klingt fast wie Swift