feat(series): ship the kernel-tier Series library extension (#61)

Series a is a bounded ring buffer with financial-style indexing
(index 0 = newest), shipped as a PURE AILang library extension over
RawBuf — zero Rust codegen, zero intercepts, zero (intrinsic)
markers. It is the first kernel-tier module that depends on another
kernel-tier module (raw_buf). This turns the committed SMA headline
pin (6b7ee45) green and closes the step-1 scope of #61.

The module lives at crates/ailang-kernel/src/series/{source.ail,
mod.rs} and is auto-injected by the loader exactly like raw_buf
(parse_series + injection in ailang-surface/src/loader.rs, re-export
in lib.rs). Five real-bodied ops: new / push / at / len /
total_count, all built on RawBuf.{new,set,get}.

The source is the model-0007 sketch with every defect corrected and
each correction verified against the live tool:
- param-in narrowed to (a Int Float); Bool deferred per #61.
- inner alloc is `(new RawBuf lookback)` with no element type-arg —
  the element is solved from the `(con RawBuf a)` ctor-field context,
  which the base-tier mono fix (b11a6d9) carries through to the
  caller's concrete type. This is the capability that lets Series be
  pure AILang.
- element-typed params/rets are bare `a`, not `(con a)` (a type
  variable, not a nullary ctor).
- op signatures bind `a` via `(forall (vars a) ...)`.
- comparison uses the named helper `lt`, not the non-existent `<`.
- the at-index formula is `(head - 1 - i + 2*lookback) mod lookback`
  (0007 had the sign on i wrong, walking the wrong direction).

Cross-kernel-module visibility — the flagged risk — needed ZERO
mechanism change: the loader derives its implicit-import set from
every kernel module, so series's body resolves `RawBuf.*` and
`(con RawBuf a)` via the existing kernel auto-visibility.

Both CLAUDE.md lockstep pairs stay intact:
- INTERCEPTS <-> (intrinsic) markers: unchanged. Series is
  pure-AILang, adds neither; intercepts_bijection_with_intrinsic_
  markers still green.
- Term::New typecheck/reject <-> pre_desugar_validation.rs:
  untouched; the new/Series construction exercises existing paths.

Test-surface ripple, all verified:
- workspace module-count pins 4 -> 5 (e2e.rs, workspace_pin.rs),
  with a `series` membership assertion added — the kernel set grew
  by one deliberate module.
- two mono-identity tests (typeclass_22b3.rs) now skip `prelude`
  and `series`: series is the first auto-imported real-bodied
  consumer of the prelude polyfns `lt`/`compare` over Int, so mono
  legitimately materialises `lt__Int`/`compare__Int` into prelude
  for every workspace. That is a concrete call site, not gratuitous
  mutation; the tests' real invariant (mono leaves work-free modules
  untouched) still holds for every other module.
- five IR snapshots refreshed; the deltas are strictly additive
  (zero IR deletions) — the same series-driven Int specialisations.

Verified: full `cargo test --workspace` green; SMA binary built via
`ail build --alloc=rc` prints `3.0 / 5.33333 / 5.66667 / 5.33333`.

refs #61
This commit is contained in:
2026-06-02 13:09:17 +02:00
parent 6b7ee45e49
commit 35a9bc67b1
13 changed files with 572 additions and 11 deletions
+5 -4
View File
@@ -942,10 +942,10 @@ fn workspace_lists_imported_modules() {
let modules = v["modules"].as_array().expect("modules must be array");
// the loader auto-injects every built-in kernel-tier module
// (`prelude` and, since raw-buf.4, the first real-payload kernel
// extension `raw_buf`), so the count is the user's two modules plus
// the two built-ins.
assert_eq!(modules.len(), 4, "expected 4 modules: {stdout}");
// (`prelude`; `raw_buf` since raw-buf.4; and `series`, the first pure
// AILang library extension, since feat-series-step1), so the count is
// the user's two modules plus the three built-ins.
assert_eq!(modules.len(), 5, "expected 5 modules: {stdout}");
let names: Vec<&str> = modules
.iter()
@@ -955,6 +955,7 @@ fn workspace_lists_imported_modules() {
assert!(names.contains(&"ws_lib"), "ws_lib missing: {names:?}");
assert!(names.contains(&"prelude"), "prelude missing: {names:?}");
assert!(names.contains(&"raw_buf"), "raw_buf missing: {names:?}");
assert!(names.contains(&"series"), "series missing: {names:?}");
}
/// Guards Iter 5b: `ail check examples/ws_main.ail.json --json` must
+93
View File
@@ -31,6 +31,8 @@ declare i64 @llvm.fptosi.sat.i64.f64(double)
@ail_prelude_float_le_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_float_le_adapter, ptr null }
@ail_prelude_float_gt_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_float_gt_adapter, ptr null }
@ail_prelude_float_ge_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_float_ge_adapter, ptr null }
@ail_prelude_compare__Int_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_compare__Int_adapter, ptr null }
@ail_prelude_lt__Int_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_lt__Int_adapter, ptr null }
define i8 @ail_hello_main() {
entry:
%v1 = getelementptr inbounds i8, ptr getelementptr inbounds (<{ i64, [15 x i8] }>, ptr @.str_hello_str_0, i32 0, i32 0), i64 8
@@ -117,6 +119,45 @@ entry:
ret i1 %r
}
define ptr @ail_prelude_compare__Int(i64 %arg_x, i64 %arg_y) alwaysinline {
entry:
%v1 = icmp slt i64 %arg_x, %arg_y
br i1 %v1, label %cmp_lt_2, label %cmp_after_lt_2
cmp_lt_2:
%v3 = call ptr @ailang_rc_alloc(i64 8)
store i64 0, ptr %v3, align 8
ret ptr %v3
cmp_after_lt_2:
%v4 = icmp eq i64 %arg_x, %arg_y
br i1 %v4, label %cmp_eq_2, label %cmp_gt_2
cmp_eq_2:
%v5 = call ptr @ailang_rc_alloc(i64 8)
store i64 1, ptr %v5, align 8
ret ptr %v5
cmp_gt_2:
%v6 = call ptr @ailang_rc_alloc(i64 8)
store i64 2, ptr %v6, align 8
ret ptr %v6
}
define ptr @ail_prelude_compare__Int_adapter(ptr %_env, i64 %a0, i64 %a1) {
entry:
%r = call ptr @ail_prelude_compare__Int(i64 %a0, i64 %a1)
ret ptr %r
}
define i1 @ail_prelude_lt__Int(i64 %arg_x, i64 %arg_y) alwaysinline {
entry:
%v1 = icmp slt i64 %arg_x, %arg_y
ret i1 %v1
}
define i1 @ail_prelude_lt__Int_adapter(ptr %_env, i64 %a0, i64 %a1) {
entry:
%r = call i1 @ail_prelude_lt__Int(i64 %a0, i64 %a1)
ret i1 %r
}
define void @drop_prelude_Ordering(ptr %p) {
entry:
%is_null = icmp eq ptr %p, null
@@ -191,6 +232,58 @@ ret:
ret void
}
define void @drop_series_Series__6ddcf58f(ptr %p) {
entry:
%is_null = icmp eq ptr %p, null
br i1 %is_null, label %ret, label %live
live:
%tag = load i64, ptr %p, align 8
switch i64 %tag, label %dflt [
i64 0, label %arm_0
]
arm_0:
%a0 = getelementptr inbounds i8, ptr %p, i64 8
%v1 = load ptr, ptr %a0, align 8
call void @drop_raw_buf_RawBuf(ptr %v1)
br label %join
dflt:
unreachable
join:
call void @ailang_rc_dec(ptr %p)
br label %ret
ret:
ret void
}
define void @partial_drop_series_Series__6ddcf58f(ptr %p, i64 %mask) {
entry:
%is_null = icmp eq ptr %p, null
br i1 %is_null, label %ret, label %live
live:
%tag = load i64, ptr %p, align 8
switch i64 %tag, label %dflt [
i64 0, label %arm_0
]
arm_0:
%b0 = and i64 %mask, 1
%s1 = icmp ne i64 %b0, 0
br i1 %s1, label %after_0_0, label %do_0_0
do_0_0:
%a2 = getelementptr inbounds i8, ptr %p, i64 8
%v3 = load ptr, ptr %a2, align 8
call void @drop_raw_buf_RawBuf(ptr %v3)
br label %after_0_0
after_0_0:
br label %join
dflt:
unreachable
join:
call void @ailang_rc_dec(ptr %p)
br label %ret
ret:
ret void
}
define i32 @main() {
call i8 @ail_hello_main()
+93
View File
@@ -32,6 +32,8 @@ declare i64 @llvm.fptosi.sat.i64.f64(double)
@ail_prelude_float_ge_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_float_ge_adapter, ptr null }
@ail_prelude_show__Int_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_show__Int_adapter, ptr null }
@ail_prelude_print__Int_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_print__Int_adapter, ptr null }
@ail_prelude_compare__Int_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_compare__Int_adapter, ptr null }
@ail_prelude_lt__Int_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_lt__Int_adapter, ptr null }
define i64 @ail_list_sum_list(ptr %arg_xs) {
entry:
%v1 = load i64, ptr %arg_xs, align 8
@@ -254,6 +256,45 @@ entry:
ret i8 %r
}
define ptr @ail_prelude_compare__Int(i64 %arg_x, i64 %arg_y) alwaysinline {
entry:
%v1 = icmp slt i64 %arg_x, %arg_y
br i1 %v1, label %cmp_lt_2, label %cmp_after_lt_2
cmp_lt_2:
%v3 = call ptr @ailang_rc_alloc(i64 8)
store i64 0, ptr %v3, align 8
ret ptr %v3
cmp_after_lt_2:
%v4 = icmp eq i64 %arg_x, %arg_y
br i1 %v4, label %cmp_eq_2, label %cmp_gt_2
cmp_eq_2:
%v5 = call ptr @ailang_rc_alloc(i64 8)
store i64 1, ptr %v5, align 8
ret ptr %v5
cmp_gt_2:
%v6 = call ptr @ailang_rc_alloc(i64 8)
store i64 2, ptr %v6, align 8
ret ptr %v6
}
define ptr @ail_prelude_compare__Int_adapter(ptr %_env, i64 %a0, i64 %a1) {
entry:
%r = call ptr @ail_prelude_compare__Int(i64 %a0, i64 %a1)
ret ptr %r
}
define i1 @ail_prelude_lt__Int(i64 %arg_x, i64 %arg_y) alwaysinline {
entry:
%v1 = icmp slt i64 %arg_x, %arg_y
ret i1 %v1
}
define i1 @ail_prelude_lt__Int_adapter(ptr %_env, i64 %a0, i64 %a1) {
entry:
%r = call i1 @ail_prelude_lt__Int(i64 %a0, i64 %a1)
ret i1 %r
}
define void @drop_prelude_Ordering(ptr %p) {
entry:
%is_null = icmp eq ptr %p, null
@@ -328,6 +369,58 @@ ret:
ret void
}
define void @drop_series_Series__6ddcf58f(ptr %p) {
entry:
%is_null = icmp eq ptr %p, null
br i1 %is_null, label %ret, label %live
live:
%tag = load i64, ptr %p, align 8
switch i64 %tag, label %dflt [
i64 0, label %arm_0
]
arm_0:
%a0 = getelementptr inbounds i8, ptr %p, i64 8
%v1 = load ptr, ptr %a0, align 8
call void @drop_raw_buf_RawBuf(ptr %v1)
br label %join
dflt:
unreachable
join:
call void @ailang_rc_dec(ptr %p)
br label %ret
ret:
ret void
}
define void @partial_drop_series_Series__6ddcf58f(ptr %p, i64 %mask) {
entry:
%is_null = icmp eq ptr %p, null
br i1 %is_null, label %ret, label %live
live:
%tag = load i64, ptr %p, align 8
switch i64 %tag, label %dflt [
i64 0, label %arm_0
]
arm_0:
%b0 = and i64 %mask, 1
%s1 = icmp ne i64 %b0, 0
br i1 %s1, label %after_0_0, label %do_0_0
do_0_0:
%a2 = getelementptr inbounds i8, ptr %p, i64 8
%v3 = load ptr, ptr %a2, align 8
call void @drop_raw_buf_RawBuf(ptr %v3)
br label %after_0_0
after_0_0:
br label %join
dflt:
unreachable
join:
call void @ailang_rc_dec(ptr %p)
br label %ret
ret:
ret void
}
define i32 @main() {
call i8 @ail_list_main()
+65
View File
@@ -35,6 +35,7 @@ declare i64 @llvm.fptosi.sat.i64.f64(double)
@ail_prelude_gt__Int_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_gt__Int_adapter, ptr null }
@ail_prelude_show__Int_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_show__Int_adapter, ptr null }
@ail_prelude_print__Int_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_print__Int_adapter, ptr null }
@ail_prelude_lt__Int_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_lt__Int_adapter, ptr null }
define i64 @ail_max3_max(i64 %arg_a, i64 %arg_b) {
entry:
%v1 = call i1 @ail_prelude_gt__Int(i64 %arg_a, i64 %arg_b)
@@ -241,6 +242,18 @@ entry:
ret i8 %r
}
define i1 @ail_prelude_lt__Int(i64 %arg_x, i64 %arg_y) alwaysinline {
entry:
%v1 = icmp slt i64 %arg_x, %arg_y
ret i1 %v1
}
define i1 @ail_prelude_lt__Int_adapter(ptr %_env, i64 %a0, i64 %a1) {
entry:
%r = call i1 @ail_prelude_lt__Int(i64 %a0, i64 %a1)
ret i1 %r
}
define void @drop_prelude_Ordering(ptr %p) {
entry:
%is_null = icmp eq ptr %p, null
@@ -315,6 +328,58 @@ ret:
ret void
}
define void @drop_series_Series__6ddcf58f(ptr %p) {
entry:
%is_null = icmp eq ptr %p, null
br i1 %is_null, label %ret, label %live
live:
%tag = load i64, ptr %p, align 8
switch i64 %tag, label %dflt [
i64 0, label %arm_0
]
arm_0:
%a0 = getelementptr inbounds i8, ptr %p, i64 8
%v1 = load ptr, ptr %a0, align 8
call void @drop_raw_buf_RawBuf(ptr %v1)
br label %join
dflt:
unreachable
join:
call void @ailang_rc_dec(ptr %p)
br label %ret
ret:
ret void
}
define void @partial_drop_series_Series__6ddcf58f(ptr %p, i64 %mask) {
entry:
%is_null = icmp eq ptr %p, null
br i1 %is_null, label %ret, label %live
live:
%tag = load i64, ptr %p, align 8
switch i64 %tag, label %dflt [
i64 0, label %arm_0
]
arm_0:
%b0 = and i64 %mask, 1
%s1 = icmp ne i64 %b0, 0
br i1 %s1, label %after_0_0, label %do_0_0
do_0_0:
%a2 = getelementptr inbounds i8, ptr %p, i64 8
%v3 = load ptr, ptr %a2, align 8
call void @drop_raw_buf_RawBuf(ptr %v3)
br label %after_0_0
after_0_0:
br label %join
dflt:
unreachable
join:
call void @ailang_rc_dec(ptr %p)
br label %ret
ret:
ret void
}
define i32 @main() {
call i8 @ail_max3_main()
+93
View File
@@ -28,6 +28,8 @@ declare i64 @llvm.fptosi.sat.i64.f64(double)
@ail_prelude_float_le_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_float_le_adapter, ptr null }
@ail_prelude_float_gt_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_float_gt_adapter, ptr null }
@ail_prelude_float_ge_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_float_ge_adapter, ptr null }
@ail_prelude_compare__Int_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_compare__Int_adapter, ptr null }
@ail_prelude_lt__Int_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_lt__Int_adapter, ptr null }
@ail_prelude_eq__Int_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_eq__Int_adapter, ptr null }
@ail_prelude_show__Int_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_show__Int_adapter, ptr null }
@ail_prelude_print__Int_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_print__Int_adapter, ptr null }
@@ -105,6 +107,45 @@ entry:
ret i1 %r
}
define ptr @ail_prelude_compare__Int(i64 %arg_x, i64 %arg_y) alwaysinline {
entry:
%v1 = icmp slt i64 %arg_x, %arg_y
br i1 %v1, label %cmp_lt_2, label %cmp_after_lt_2
cmp_lt_2:
%v3 = call ptr @ailang_rc_alloc(i64 8)
store i64 0, ptr %v3, align 8
ret ptr %v3
cmp_after_lt_2:
%v4 = icmp eq i64 %arg_x, %arg_y
br i1 %v4, label %cmp_eq_2, label %cmp_gt_2
cmp_eq_2:
%v5 = call ptr @ailang_rc_alloc(i64 8)
store i64 1, ptr %v5, align 8
ret ptr %v5
cmp_gt_2:
%v6 = call ptr @ailang_rc_alloc(i64 8)
store i64 2, ptr %v6, align 8
ret ptr %v6
}
define ptr @ail_prelude_compare__Int_adapter(ptr %_env, i64 %a0, i64 %a1) {
entry:
%r = call ptr @ail_prelude_compare__Int(i64 %a0, i64 %a1)
ret ptr %r
}
define i1 @ail_prelude_lt__Int(i64 %arg_x, i64 %arg_y) alwaysinline {
entry:
%v1 = icmp slt i64 %arg_x, %arg_y
ret i1 %v1
}
define i1 @ail_prelude_lt__Int_adapter(ptr %_env, i64 %a0, i64 %a1) {
entry:
%r = call i1 @ail_prelude_lt__Int(i64 %a0, i64 %a1)
ret i1 %r
}
define i1 @ail_prelude_eq__Int(i64 %arg_x, i64 %arg_y) alwaysinline {
entry:
%v1 = icmp eq i64 %arg_x, %arg_y
@@ -219,6 +260,58 @@ ret:
ret void
}
define void @drop_series_Series__6ddcf58f(ptr %p) {
entry:
%is_null = icmp eq ptr %p, null
br i1 %is_null, label %ret, label %live
live:
%tag = load i64, ptr %p, align 8
switch i64 %tag, label %dflt [
i64 0, label %arm_0
]
arm_0:
%a0 = getelementptr inbounds i8, ptr %p, i64 8
%v1 = load ptr, ptr %a0, align 8
call void @drop_raw_buf_RawBuf(ptr %v1)
br label %join
dflt:
unreachable
join:
call void @ailang_rc_dec(ptr %p)
br label %ret
ret:
ret void
}
define void @partial_drop_series_Series__6ddcf58f(ptr %p, i64 %mask) {
entry:
%is_null = icmp eq ptr %p, null
br i1 %is_null, label %ret, label %live
live:
%tag = load i64, ptr %p, align 8
switch i64 %tag, label %dflt [
i64 0, label %arm_0
]
arm_0:
%b0 = and i64 %mask, 1
%s1 = icmp ne i64 %b0, 0
br i1 %s1, label %after_0_0, label %do_0_0
do_0_0:
%a2 = getelementptr inbounds i8, ptr %p, i64 8
%v3 = load ptr, ptr %a2, align 8
call void @drop_raw_buf_RawBuf(ptr %v3)
br label %after_0_0
after_0_0:
br label %join
dflt:
unreachable
join:
call void @ailang_rc_dec(ptr %p)
br label %ret
ret:
ret void
}
define i64 @ail_sum_sum(i64 %arg_n) {
entry:
%v1 = call i1 @ail_prelude_eq__Int(i64 %arg_n, i64 0)
+93
View File
@@ -28,6 +28,8 @@ declare i64 @llvm.fptosi.sat.i64.f64(double)
@ail_prelude_float_le_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_float_le_adapter, ptr null }
@ail_prelude_float_gt_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_float_gt_adapter, ptr null }
@ail_prelude_float_ge_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_float_ge_adapter, ptr null }
@ail_prelude_compare__Int_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_compare__Int_adapter, ptr null }
@ail_prelude_lt__Int_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_lt__Int_adapter, ptr null }
@ail_prelude_show__Int_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_show__Int_adapter, ptr null }
@ail_prelude_print__Int_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_print__Int_adapter, ptr null }
@ail_ws_lib_add_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_ws_lib_add_adapter, ptr null }
@@ -104,6 +106,45 @@ entry:
ret i1 %r
}
define ptr @ail_prelude_compare__Int(i64 %arg_x, i64 %arg_y) alwaysinline {
entry:
%v1 = icmp slt i64 %arg_x, %arg_y
br i1 %v1, label %cmp_lt_2, label %cmp_after_lt_2
cmp_lt_2:
%v3 = call ptr @ailang_rc_alloc(i64 8)
store i64 0, ptr %v3, align 8
ret ptr %v3
cmp_after_lt_2:
%v4 = icmp eq i64 %arg_x, %arg_y
br i1 %v4, label %cmp_eq_2, label %cmp_gt_2
cmp_eq_2:
%v5 = call ptr @ailang_rc_alloc(i64 8)
store i64 1, ptr %v5, align 8
ret ptr %v5
cmp_gt_2:
%v6 = call ptr @ailang_rc_alloc(i64 8)
store i64 2, ptr %v6, align 8
ret ptr %v6
}
define ptr @ail_prelude_compare__Int_adapter(ptr %_env, i64 %a0, i64 %a1) {
entry:
%r = call ptr @ail_prelude_compare__Int(i64 %a0, i64 %a1)
ret ptr %r
}
define i1 @ail_prelude_lt__Int(i64 %arg_x, i64 %arg_y) alwaysinline {
entry:
%v1 = icmp slt i64 %arg_x, %arg_y
ret i1 %v1
}
define i1 @ail_prelude_lt__Int_adapter(ptr %_env, i64 %a0, i64 %a1) {
entry:
%r = call i1 @ail_prelude_lt__Int(i64 %a0, i64 %a1)
ret i1 %r
}
define ptr @ail_prelude_show__Int(i64 %arg_x) {
entry:
%v1 = call ptr @ailang_int_to_str(i64 %arg_x)
@@ -206,6 +247,58 @@ ret:
ret void
}
define void @drop_series_Series__6ddcf58f(ptr %p) {
entry:
%is_null = icmp eq ptr %p, null
br i1 %is_null, label %ret, label %live
live:
%tag = load i64, ptr %p, align 8
switch i64 %tag, label %dflt [
i64 0, label %arm_0
]
arm_0:
%a0 = getelementptr inbounds i8, ptr %p, i64 8
%v1 = load ptr, ptr %a0, align 8
call void @drop_raw_buf_RawBuf(ptr %v1)
br label %join
dflt:
unreachable
join:
call void @ailang_rc_dec(ptr %p)
br label %ret
ret:
ret void
}
define void @partial_drop_series_Series__6ddcf58f(ptr %p, i64 %mask) {
entry:
%is_null = icmp eq ptr %p, null
br i1 %is_null, label %ret, label %live
live:
%tag = load i64, ptr %p, align 8
switch i64 %tag, label %dflt [
i64 0, label %arm_0
]
arm_0:
%b0 = and i64 %mask, 1
%s1 = icmp ne i64 %b0, 0
br i1 %s1, label %after_0_0, label %do_0_0
do_0_0:
%a2 = getelementptr inbounds i8, ptr %p, i64 8
%v3 = load ptr, ptr %a2, align 8
call void @drop_raw_buf_RawBuf(ptr %v3)
br label %after_0_0
after_0_0:
br label %join
dflt:
unreachable
join:
call void @ailang_rc_dec(ptr %p)
br label %ret
ret:
ret void
}
define i64 @ail_ws_lib_add(i64 %arg_a, i64 %arg_b) {
entry:
%v1 = add i64 %arg_a, %arg_b
+25 -1
View File
@@ -30,7 +30,21 @@ fn monomorphise_workspace_is_identity_on_class_free_workspace() {
"module set must be identical"
);
// Per-module def list unchanged (compare by canonical-JSON hash).
//
// Exception: `prelude` is no longer hash-identical through mono for
// ANY entry since feat-series-step1. The auto-imported `series`
// kernel module is a real-bodied (non-intrinsic) consumer of the
// prelude polyfns `lt`/`compare` over `Int`, so the workspace-wide
// mono pass now materialises `lt__Int` / `compare__Int` into the
// prelude module regardless of the user entry. That is a concrete
// call site driven by an auto-imported library module, not gratuitous
// mutation — the test's real invariant (mono does not rewrite modules
// with no work to do) holds for every other module. `series` itself
// is excluded for the same reason: it may gain its own specialisations.
for (mname, m) in &ws.modules {
if mname == "prelude" || mname == "series" {
continue;
}
let before_m = &before.modules[mname];
let h_after = ailang_core::module_hash(m);
let h_before = ailang_core::module_hash(before_m);
@@ -72,8 +86,18 @@ fn monomorphise_workspace_is_identity_when_no_concrete_call_sites() {
"module set must be identical"
);
// Each module hashes byte-identical — no synth fns appended
// because no call site triggered a target.
// because no class-method call site triggered a target.
//
// Exception: `prelude` and `series` — see the sibling test
// `monomorphise_workspace_is_identity_on_class_free_workspace` for
// the rationale. The auto-imported, real-bodied `series` kernel
// module pins `lt__Int` / `compare__Int` specialisations into
// `prelude` for every workspace; that is a legitimate concrete call
// site, orthogonal to this test's typeclass-call-site invariant.
for (mname, m) in &ws_after.modules {
if mname == "prelude" || mname == "series" {
continue;
}
let before_m = &before.modules[mname];
let h_after = ailang_core::module_hash(m);
let h_before = ailang_core::module_hash(before_m);
+7 -5
View File
@@ -46,13 +46,15 @@ fn loads_example_workspace_happy_path() {
assert_eq!(ws.entry, "ws_main");
assert!(ws.modules.contains_key("ws_main"));
assert!(ws.modules.contains_key("ws_lib"));
// the loader auto-injects two built-in kernel-tier modules
// (`prelude` and `raw_buf`, the first real-payload kernel
// extension, raw-buf.4), so the count is the user's two modules
// plus those two.
assert_eq!(ws.modules.len(), 4);
// the loader auto-injects three built-in kernel-tier modules
// (`prelude`; `raw_buf`, the first real-payload kernel extension,
// raw-buf.4; and `series`, the first pure-AILang library extension,
// feat-series-step1), so the count is the user's two modules plus
// those three.
assert_eq!(ws.modules.len(), 5);
assert!(ws.modules.contains_key("prelude"));
assert!(ws.modules.contains_key("raw_buf"));
assert!(ws.modules.contains_key("series"));
}
#[test]
+2
View File
@@ -9,5 +9,7 @@
//! crate carries zero parser code.
mod raw_buf;
mod series;
pub use raw_buf::SOURCE as RAW_BUF_AIL;
pub use series::SOURCE as SERIES_AIL;
+18
View File
@@ -0,0 +1,18 @@
//! Series submodule — the Form-A source of the `series` kernel-tier
//! library-extension module (Series: a bounded ring buffer over
//! RawBuf with financial-style indexing, index 0 = newest). Parsed by
//! `ailang_surface::parse_series`.
//!
//! Unlike `raw_buf`, `series` is a PURE AILang library extension: its
//! five ops (new/push/at/len/total_count) carry real `.ail` bodies
//! built on the `RawBuf.*` intrinsics, so it adds NO intercept entries
//! and NO `(intrinsic)` markers. It is the first kernel-tier module
//! that depends on another kernel-tier module (`raw_buf`); cross-
//! kernel-module visibility comes from the kernel auto-import set the
//! loader derives from every `kernel: true` module.
/// Source-of-truth Form-A text for the `series` kernel module.
/// Declares the `Series` TypeDef (`param-in (a Int Float)`) and the
/// five real-bodied ops `new`/`push`/`at`/`len`/`total_count`, all
/// implemented over the `raw_buf` kernel module's `RawBuf.*` ops.
pub const SOURCE: &str = include_str!("source.ail");
@@ -0,0 +1,49 @@
(module series
(kernel)
(data Series (vars a)
(doc "Bounded ring buffer with financial-style indexing (index 0 = newest). Pure AILang library extension over RawBuf: the buffer holds the elements, the four Int fields are lookback (capacity), head (next write slot), count (live elements, saturates at lookback), and total (monotone count of all pushes ever). Element type restricted to {Int, Float} via param-in; Bool deferred (#61).")
(param-in (a Int Float))
(ctor S (con RawBuf a)
(con Int)
(con Int)
(con Int)
(con Int)))
(fn new
(doc "Allocate an empty Series of capacity `lookback`. The inner RawBuf is allocated via `(new RawBuf lookback)` with NO element type-arg — the element type is solved from the expected ctor-field type `(con RawBuf a)`, which the base-tier mono fix (b11a6d9) carries through to the caller's concrete type.")
(type (forall (vars a) (fn-type (params (own (con Int))) (ret (own (con Series a))))))
(params lookback)
(body
(term-ctor Series S
(new RawBuf lookback) lookback 0 0 0)))
(fn push
(doc "Append `v`, evicting the oldest element when full. own-in/own-out linear threading: mutates the buffer at head, advances head mod lookback, saturates count at lookback, increments total.")
(type (forall (vars a) (fn-type (params (own (con Series a)) (own a)) (ret (own (con Series a))))))
(params s v)
(body
(match s
(case (pat-ctor S buf lookback head count total)
(term-ctor Series S
(app RawBuf.set buf head v)
lookback
(app % (app + head 1) lookback)
(if (app lt count lookback) (app + count 1) count)
(app + total 1))))))
(fn at
(doc "Read the element `i` positions back from newest (i=0 is the most recent push). Financial indexing: slot = (head - 1 - i + 2*lookback) mod lookback. UB if i >= len; caller checks via len/total_count.")
(type (forall (vars a) (fn-type (params (borrow (con Series a)) (own (con Int))) (ret (own a)))))
(params s i)
(body
(match s
(case (pat-ctor S buf lookback head count total)
(app RawBuf.get buf
(app % (app + (app - (app - head 1) i) (app * lookback 2)) lookback))))))
(fn len
(doc "Number of live elements (saturates at lookback).")
(type (forall (vars a) (fn-type (params (borrow (con Series a))) (ret (own (con Int))))))
(params s)
(body (match s (case (pat-ctor S buf lookback head count total) count))))
(fn total_count
(doc "Monotone count of all pushes ever (does not saturate). Drives the SMA emit gate.")
(type (forall (vars a) (fn-type (params (borrow (con Series a))) (ret (own (con Int))))))
(params s)
(body (match s (case (pat-ctor S buf lookback head count total) total)))))
+2 -1
View File
@@ -37,7 +37,8 @@ pub mod parse;
pub mod print;
pub use loader::{
load_module, load_workspace, parse_prelude, parse_raw_buf, PRELUDE_AIL,
load_module, load_workspace, parse_prelude, parse_raw_buf, parse_series,
PRELUDE_AIL,
};
pub use parse::{parse, parse_term, ParseError};
pub use print::{print, term_to_form_a, type_to_form_a};
+27
View File
@@ -56,6 +56,21 @@ pub fn parse_raw_buf() -> Module {
.expect("ailang_kernel::RAW_BUF_AIL must parse as a Module")
}
/// feat-series-step1: parse the embedded `series` kernel-tier library-
/// extension module bytes into a `Module`. Mirror of [`parse_raw_buf`].
///
/// Source-of-truth: `ailang_kernel::SERIES_AIL`. Declares the `Series`
/// TypeDef with `param-in (a Int Float)` and the five real-bodied ops
/// `new`/`push`/`at`/`len`/`total_count`, built over the `raw_buf`
/// kernel module's `RawBuf.*` ops. Unlike raw_buf this is a PURE
/// AILang library extension — no intercepts, no `(intrinsic)` markers.
///
/// Panics on parse failure — build-time-validated by every drift run.
pub fn parse_series() -> Module {
crate::parse(ailang_kernel::SERIES_AIL)
.expect("ailang_kernel::SERIES_AIL must parse as a Module")
}
fn is_ail_source(path: &Path) -> bool {
path.extension().and_then(|s| s.to_str()) == Some("ail")
}
@@ -131,6 +146,18 @@ pub fn load_workspace(entry: &Path) -> Result<Workspace, WorkspaceLoadError> {
}
modules.insert("raw_buf".to_string(), parse_raw_buf());
// parse_series() injects the series kernel-tier library-extension
// module — Series, a bounded ring buffer over RawBuf. Pure AILang
// (no intercepts); its body references the raw_buf kernel module,
// which the kernel auto-import set below makes visible. Its source
// carries `(kernel)`, so the kernel-flag filter auto-imports it.
if modules.contains_key("series") {
return Err(WorkspaceLoadError::ReservedModuleName {
name: "series".to_string(),
});
}
modules.insert("series".to_string(), parse_series());
// Derive the implicit-imports list from `kernel: true` modules.
// Replaces the previous hardcoded `&["prelude"]` literal: any
// workspace-loaded module that carries the kernel flag is now