From fa23a4c125f9fa915138e18a53b7de1a93404429 Mon Sep 17 00:00:00 2001 From: Michael Schimmel Date: Mon, 9 Mar 2026 11:08:30 +0100 Subject: [PATCH] Refactor UntypedKind::Parameter to Identifier The `Parameter` kind in `UntypedKind` was only used for identifiers that were being declared or referenced. This commit renames it to `Identifier` and updates all the necessary code to reflect this change. This simplifies the AST and makes it more consistent. Additionally, a new macro `repeat` has been added to `src/ast/system.myc`. --- .geminiignore | 1 + docs/todo/record_optimizations_plan_soa.md | 73 ---------------------- examples/macro_issue.myc | 5 ++ examples/pipeline_script_ticker.myc | 4 +- examples/record_optimizations.myc | 4 +- examples/record_specialization_test.myc | 4 +- src/ast/compiler/binder.rs | 9 +-- src/ast/compiler/macros.rs | 15 +---- src/ast/environment.rs | 9 ++- src/ast/nodes.rs | 2 +- src/ast/parser.rs | 2 +- src/ast/system.myc | 11 ++++ 12 files changed, 34 insertions(+), 105 deletions(-) delete mode 100644 docs/todo/record_optimizations_plan_soa.md create mode 100644 examples/macro_issue.myc diff --git a/.geminiignore b/.geminiignore index 15a2026..f7dbc4c 100644 --- a/.geminiignore +++ b/.geminiignore @@ -1,3 +1,4 @@ *.snap *.snap.new target/ +docs/delphi/ diff --git a/docs/todo/record_optimizations_plan_soa.md b/docs/todo/record_optimizations_plan_soa.md deleted file mode 100644 index a7f6513..0000000 --- a/docs/todo/record_optimizations_plan_soa.md +++ /dev/null @@ -1,73 +0,0 @@ -# Optimierung des Speicherlayouts für Series und Records - -## Motivation - -Der Kern einer jeden DSL für die Finanzanalyse ist die Verarbeitung enormer Mengen an Zeitreihendaten (Ticks, Kerzen, abgeleitete Indikatoren). Diese Daten fließen in Form von **Series**, **Streams** und **Pipes** durch das System. - -In der aktuellen Rust-Portierung basiert die Laufzeitumgebung (`VM`) vollständig auf dem `Value`-Enum: - -```rust -pub enum Value { - Int(i64), - Float(f64), - Record(Arc, Rc>), - // ... -} -``` - -Auf einem 64-Bit-System hat dieses Enum durch Alignment und Tagging eine Größe von exakt **24 Bytes**. Jedes `Value::Int(42)` oder `Value::Float(3.14)` belegt somit das Dreifache an Speicher im Vergleich zu einem nativen 8-Byte Skalar. - -Für einzelne lokale Variablen ist das vernachlässigbar. Für Zeitreihen führt dieses Layout jedoch zu massiven Problemen: -1. **Cache Misses:** Arrays von 24-Byte-Strukturen zerstören die Cache-Lokalität, die für rechenintensive Indikatoren essentiell ist. -2. **Heap Allokationen:** Ein `Value::Record` erzeugt für seine Felder einen `Rc>`. Ein einfaches `push(series, { price: 100.0, volume: 10 })` würde aktuell jedes Mal eine Heap-Allokation für den Record auslösen. -3. **Pointer Chasing:** Das ständige Dereferenzieren von Rc/Arc-Pointern in engen Loops verhindert Vektorisierung (SIMD) auf CPU-Ebene. - -In der alten Delphi-Codebase wurde dies durch spezielle Code-Pfade für skalare Typen gelöst. In Rust können wir dank des `Specializers` und des statischen Typsystems einen noch effizienteren Weg gehen. - -## Konzept: Data-Oriented Design & Struct of Arrays (SoA) - -Anstatt Series als einfache Listen von `Value`-Objekten zu implementieren, werden Series basierend auf ihrem statischen Typ spezialisiert. - -### Skalare Serien -Eine Serie von Float-Werten (`Series`) speichert intern keine `Value`s, sondern nutzt ein natives Array (z. B. einen Ringpuffer): `Vec`. - -### Record Serien (SoA) -Besonders bei Records zeigt sich die Stärke von SoA. Angenommen, wir haben einen Record `Tick { price: Float, volume: Int }`. -Eine `RecordSeries` speichert **kein** Array von Structs (AoS), sondern spaltet die Felder in separate Arrays auf: - -```rust -// Konzeptionelles Layout -struct TickSeries { - prices: Vec, // Perfekt sequenziell im Cache - volumes: Vec, -} -``` - -Wenn ein Skript z. B. den gleitenden Durchschnitt über `prices` berechnet, liest die CPU einen dichten Block von `f64` direkt in den L1-Cache, ohne jemals das `volume` laden zu müssen. - -## Integration in die Compiler-Pipeline - -Die Optimierung erfordert keine Änderungen an der Syntax der Skriptsprache. Der AST bleibt aus Sicht des Benutzers dynamisch, wird aber vom Compiler monomorphisiert ("Fast Paths"). - -### 1. Das `Object` Trait -Spezialisierte Serien (wie `FloatSeries` oder `TickSeries`) implementieren das bereits vorhandene `Object`-Trait. Dadurch können sie bei Bedarf in ein `Value::Object(Rc)` verpackt und dynamisch durch das System gereicht werden. - -### 2. Node Fusion im Specializer -Da der `Analyzer` den `StaticType` jeder Variable kennt, weiß der `Specializer` zur Compile-Zeit exakt, ob eine Variable eine generische Liste oder eine spezialisierte `FloatSeries` ist. - -**Beispiel: Element auslesen** -`let p = my_series[0].price` -Anstatt einen generischen `GetElement` und `GetField` Node zu erzeugen, verschmilzt ("fusioniert") der Specializer dies zu einem spezialisierten VM-Node: `ExecNode::GetSeriesRecordFieldFloat`. -Zur Laufzeit führt dieser Node folgendes aus: -1. Holt die Serie via `downcast_ref` aus dem `Value::Object`. -2. Greift direkt auf `series.prices[0]` zu. -3. Boxed das Ergebnis *einmalig* in ein `Value::Float` (oder hält es auf dem Stack). - -**Beispiel: Werte pushen (Zero-Allocation)** -`push(my_series, { price: 100.0, volume: 10 })` -Der Specializer erkennt, dass der Record nur für den Push erzeugt wird. Er generiert einen `PushToRecordSeries`-Node. Dieser Node erzeugt niemals ein `Value::Record` auf dem Heap. Stattdessen liest er die Argumente als primitive `f64`/`i64` und schreibt sie direkt in die parallelen Arrays der `TickSeries`. - -## Nächste Schritte -1. **Prototyping:** Implementierung der primitiven Ringpuffer (`RingBuffer`) und der basischen Serien-Strukturen für Skalare und Records. -2. **Specializer-Erweiterung:** Erkennung von Typmustern im AST und Generierung der Fast-Path-Knoten (`BoundKind`-Erweiterungen). -3. **VM-Integration:** Anpassung der Evaluierungs-Loop, um die spezialisierten Knoten auszuführen und via `Any::downcast_ref` sicher und performant in die nativen Strukturen zu greifen. \ No newline at end of file diff --git a/examples/macro_issue.myc b/examples/macro_issue.myc new file mode 100644 index 0000000..c881b6c --- /dev/null +++ b/examples/macro_issue.myc @@ -0,0 +1,5 @@ +;; Output: 42 +(do + (macro t [x] `(fn [~x] ~x)) + (def id (t a)) + (id 42)) \ No newline at end of file diff --git a/examples/pipeline_script_ticker.myc b/examples/pipeline_script_ticker.myc index 10b13b3..d34953f 100644 --- a/examples/pipeline_script_ticker.myc +++ b/examples/pipeline_script_ticker.myc @@ -1,5 +1,5 @@ -;; Benchmark: 3.2us -;; Benchmark-Repeat: 613 +;; Benchmark: 2.3us +;; Benchmark-Repeat: 870 ;; Output: (do diff --git a/examples/record_optimizations.myc b/examples/record_optimizations.myc index 6d52ba2..8bcbc93 100644 --- a/examples/record_optimizations.myc +++ b/examples/record_optimizations.myc @@ -1,5 +1,5 @@ -;; Benchmark: 900.6us -;; Benchmark-Repeat: 4 +;; Benchmark: 1.0ms +;; Benchmark-Repeat: 3 ;; Tests the effect of record inlining and field lookup optimization ;; Output: 10000 (do diff --git a/examples/record_specialization_test.myc b/examples/record_specialization_test.myc index 3fd677f..87d3bbd 100644 --- a/examples/record_specialization_test.myc +++ b/examples/record_specialization_test.myc @@ -1,5 +1,5 @@ -;; Benchmark: 1.5us -;; Benchmark-Repeat: 1348 +;; Benchmark: 1.1us +;; Benchmark-Repeat: 1895 ;; Test Record SoA specialization in Pipelines ;; Dank der neuen Spezialisierung wird hierfür im Hintergrund ;; eine SharedRecordSeries mit SoA-Layout (Float-Puffer für mid und range) erstellt. diff --git a/src/ast/compiler/binder.rs b/src/ast/compiler/binder.rs index b05c434..a20bd45 100644 --- a/src/ast/compiler/binder.rs +++ b/src/ast/compiler/binder.rs @@ -197,11 +197,6 @@ impl Binder { } } - UntypedKind::Parameter(_) => { - diag.push_error("Unexpected 'Parameter' node in general binder context. This should be handled via 'bind_pattern'.", Some(node.identity.clone())); - self.make_node(node.identity.clone(), BoundKind::Error) - } - UntypedKind::FieldAccessor(k) => { self.make_node(node.identity.clone(), BoundKind::FieldAccessor(*k)) } @@ -230,7 +225,7 @@ impl Binder { UntypedKind::Def { target, value } => { // Special case: Single identifier (to support recursion) - if let UntypedKind::Parameter(ref name) = target.kind { + if let UntypedKind::Identifier(ref name) = target.kind { let addr_opt = self.declare_variable( name, node.identity.clone(), // Identity of the Def node @@ -543,7 +538,7 @@ impl Binder { diag: &mut Diagnostics, ) -> BoundNode { match &node.kind { - UntypedKind::Parameter(sym) => { + UntypedKind::Identifier(sym) => { if let Some(addr) = self.declare_variable(sym, node.identity.clone(), kind, diag) { self.make_node( node.identity.clone(), diff --git a/src/ast/compiler/macros.rs b/src/ast/compiler/macros.rs index 1b02ab0..357f85b 100644 --- a/src/ast/compiler/macros.rs +++ b/src/ast/compiler/macros.rs @@ -344,7 +344,7 @@ impl MacroExpander { fn extract_param_names(&self, node: &Node) -> Result>, String> { match &node.kind { - UntypedKind::Parameter(sym) => Ok(vec![sym.name.clone()]), + UntypedKind::Identifier(sym) => Ok(vec![sym.name.clone()]), UntypedKind::Tuple { elements } => { let mut names = Vec::new(); for el in elements { @@ -372,15 +372,6 @@ impl MacroExpander { }) } - UntypedKind::Parameter(mut sym) => { - sym.context = Some(state.expansion_id.clone()); - Ok(Node { - identity: node.identity, - kind: UntypedKind::Parameter(sym), - ty: (), - }) - } - UntypedKind::Placeholder(inner) => { // Break out of template for substitution/evaluation if let UntypedKind::Identifier(ref sym) = inner.kind @@ -654,11 +645,11 @@ mod tests { } = &exprs[1].kind { if let UntypedKind::Def { target, .. } = &result.kind { - if let UntypedKind::Parameter(sym) = &target.kind { + if let UntypedKind::Identifier(sym) = &target.kind { assert_eq!(sym.context, Some(call.identity.clone())); assert_eq!(sym.name.as_ref(), "y"); } else { - panic!("Expected Parameter target, got {:?}", target.kind); + panic!("Expected Identifier target, got {:?}", target.kind); } } else { panic!("Expected Def result, got {:?}", result.kind); diff --git a/src/ast/environment.rs b/src/ast/environment.rs index 7ab5e5e..297fe5f 100644 --- a/src/ast/environment.rs +++ b/src/ast/environment.rs @@ -381,7 +381,7 @@ impl Environment { fn discover_globals(&self, node: &Node) { match &node.kind { UntypedKind::Def { target, .. } => { - if let UntypedKind::Parameter(sym) = &target.kind { + if let UntypedKind::Identifier(sym) = &target.kind { let mut names = self.global_names.borrow_mut(); if !names.contains_key(sym) { let idx = GlobalIdx(names.len() as u32); @@ -391,12 +391,11 @@ impl Environment { } UntypedKind::MacroDecl { name, params, body } => { let mut registry = self.macro_registry.borrow_mut(); - + fn extract_names(node: &Node) -> Vec> { match &node.kind { - UntypedKind::Parameter(sym) => vec![sym.name.clone()], - UntypedKind::Tuple { elements } => { - elements.iter().flat_map(extract_names).collect() + UntypedKind::Identifier(sym) => vec![sym.name.clone()], + UntypedKind::Tuple { elements } => { elements.iter().flat_map(extract_names).collect() } _ => vec![], } diff --git a/src/ast/nodes.rs b/src/ast/nodes.rs index 4094a4f..2a2c7f3 100644 --- a/src/ast/nodes.rs +++ b/src/ast/nodes.rs @@ -63,8 +63,8 @@ impl Clone for Box { pub enum UntypedKind { Nop, Constant(Value), + /// A general identifier (used for both references and declarations in the untyped AST). Identifier(Symbol), - Parameter(Symbol), /// A first-class field accessor (e.g. .name) FieldAccessor(crate::ast::types::Keyword), If { diff --git a/src/ast/parser.rs b/src/ast/parser.rs index d4e3e42..2e56028 100644 --- a/src/ast/parser.rs +++ b/src/ast/parser.rs @@ -358,7 +358,7 @@ impl<'a> Parser<'a> { }; Node { identity: NodeIdentity::new(token.location), - kind: UntypedKind::Parameter(sym), + kind: UntypedKind::Identifier(sym), ty: (), } } diff --git a/src/ast/system.myc b/src/ast/system.myc index c7d81a4..1057b8a 100644 --- a/src/ast/system.myc +++ b/src/ast/system.myc @@ -8,6 +8,17 @@ ))) ) + (macro repeat [var limit body] + `((fn [~var __limit] + (if (< ~var __limit) + (do + ~body + (again (+ ~var 1) __limit) + ) + )) + 0 ~limit) + ) + ;; Creates a stateful cache (Series) from a stateless stream. (macro cache [lookback type src] `(do