Skip to content

Commit eb144b6

Browse files
Track 2: 2D arrays + matrix multiplication
Adds the minimum primitives for small linear-algebra workloads on top of the existing substrate-typed dtype: arr_matmul(A, B) — standard matmul, A * B = C arr_transpose(M) — swap rows/cols arr_eye(n) — n×n identity arr_zeros_2d(r, c) — r×c zero matrix Matrices are array-of-arrays — no new Value variant, so they round-trip cleanly through dict/array indexing, generators, classes. Element math goes through f64 so matmul stays sensible when fed float-valued rows (autograd-style scalars in the next step). Tests: 9 cases — identity, non-square, row-vector dot, transpose involution, and a substrate-pipeline composition (Fibonacci weights × substrate-typed feature row). Sets up the substrate-aware autograd MVP, which needs matmul + transpose for backward passes through linear layers. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent a47bb89 commit eb144b6

3 files changed

Lines changed: 265 additions & 0 deletions

File tree

examples/tests/test_matmul.omc

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# Track 2: 2D arrays + matrix multiplication.
2+
#
3+
# Matrices in OMC are arrays of arrays. arr_matmul, arr_transpose,
4+
# arr_eye, and arr_zeros_2d give us the minimum primitives to assemble
5+
# small linear-algebra workloads on top of the substrate-typed dtype.
6+
7+
fn assert_eq(actual, expected, msg) {
8+
if actual != expected {
9+
test_record_failure(msg + ": expected " + to_string(expected) + " got " + to_string(actual));
10+
}
11+
}
12+
13+
fn assert_true(cond, msg) {
14+
if !cond { test_record_failure(msg); }
15+
}
16+
17+
fn approx_eq(a, b, tol) {
18+
h d = a - b;
19+
if d < 0.0 { d = 0.0 - d; }
20+
return d <= tol;
21+
}
22+
23+
# ---- arr_eye (identity matrix) ----
24+
25+
fn test_eye_3() {
26+
h I = arr_eye(3);
27+
assert_eq(arr_len(I), 3, "I has 3 rows");
28+
h r0 = arr_get(I, 0);
29+
h r1 = arr_get(I, 1);
30+
assert_eq(arr_len(r0), 3, "row has 3 cols");
31+
assert_eq(arr_get(r0, 0), 1, "I[0][0]=1");
32+
assert_eq(arr_get(r0, 1), 0, "I[0][1]=0");
33+
assert_eq(arr_get(r1, 1), 1, "I[1][1]=1");
34+
}
35+
36+
# ---- arr_zeros_2d ----
37+
38+
fn test_zeros_2d() {
39+
h Z = arr_zeros_2d(2, 3);
40+
assert_eq(arr_len(Z), 2, "Z has 2 rows");
41+
h r0 = arr_get(Z, 0);
42+
assert_eq(arr_len(r0), 3, "row has 3 cols");
43+
assert_eq(arr_get(r0, 0), 0, "all zeros");
44+
assert_eq(arr_get(r0, 2), 0, "all zeros");
45+
}
46+
47+
# ---- arr_transpose ----
48+
49+
fn test_transpose_2x3() {
50+
h A = [[1, 2, 3], [4, 5, 6]];
51+
h At = arr_transpose(A);
52+
assert_eq(arr_len(At), 3, "transpose has 3 rows (was 3 cols)");
53+
h r0 = arr_get(At, 0);
54+
assert_eq(arr_len(r0), 2, "row has 2 cols (was 2 rows)");
55+
assert_eq(arr_get(r0, 0), 1, "At[0][0]=A[0][0]=1");
56+
assert_eq(arr_get(r0, 1), 4, "At[0][1]=A[1][0]=4");
57+
h r2 = arr_get(At, 2);
58+
assert_eq(arr_get(r2, 1), 6, "At[2][1]=A[1][2]=6");
59+
}
60+
61+
fn test_transpose_twice_identity() {
62+
h A = [[1, 2], [3, 4], [5, 6]];
63+
h Att = arr_transpose(arr_transpose(A));
64+
h r0 = arr_get(Att, 0);
65+
h r2 = arr_get(Att, 2);
66+
assert_eq(arr_get(r0, 0), 1, "(A^T)^T = A");
67+
assert_eq(arr_get(r2, 1), 6, "(A^T)^T = A");
68+
}
69+
70+
# ---- arr_matmul ----
71+
72+
fn test_matmul_2x2_2x2() {
73+
# [[1,2],[3,4]] * [[5,6],[7,8]] = [[19,22],[43,50]]
74+
h A = [[1, 2], [3, 4]];
75+
h B = [[5, 6], [7, 8]];
76+
h C = arr_matmul(A, B);
77+
h r0 = arr_get(C, 0);
78+
h r1 = arr_get(C, 1);
79+
assert_true(approx_eq(arr_get(r0, 0), 19.0, 0.001), "C[0][0]=19");
80+
assert_true(approx_eq(arr_get(r0, 1), 22.0, 0.001), "C[0][1]=22");
81+
assert_true(approx_eq(arr_get(r1, 0), 43.0, 0.001), "C[1][0]=43");
82+
assert_true(approx_eq(arr_get(r1, 1), 50.0, 0.001), "C[1][1]=50");
83+
}
84+
85+
fn test_matmul_identity() {
86+
# A * I = A
87+
h A = [[2, 3], [4, 5]];
88+
h I = arr_eye(2);
89+
h C = arr_matmul(A, I);
90+
h r0 = arr_get(C, 0);
91+
h r1 = arr_get(C, 1);
92+
assert_true(approx_eq(arr_get(r0, 0), 2.0, 0.001), "AI[0][0]=2");
93+
assert_true(approx_eq(arr_get(r0, 1), 3.0, 0.001), "AI[0][1]=3");
94+
assert_true(approx_eq(arr_get(r1, 0), 4.0, 0.001), "AI[1][0]=4");
95+
assert_true(approx_eq(arr_get(r1, 1), 5.0, 0.001), "AI[1][1]=5");
96+
}
97+
98+
fn test_matmul_nonsquare() {
99+
# (2x3) * (3x2) = (2x2)
100+
h A = [[1, 2, 3], [4, 5, 6]];
101+
h B = [[7, 8], [9, 10], [11, 12]];
102+
h C = arr_matmul(A, B);
103+
assert_eq(arr_len(C), 2, "result has 2 rows");
104+
h r0 = arr_get(C, 0);
105+
assert_eq(arr_len(r0), 2, "result row has 2 cols");
106+
# row 0: 1*7+2*9+3*11 = 58 ; 1*8+2*10+3*12 = 64
107+
# row 1: 4*7+5*9+6*11 = 139; 4*8+5*10+6*12 = 154
108+
assert_true(approx_eq(arr_get(r0, 0), 58.0, 0.001), "C[0][0]=58");
109+
assert_true(approx_eq(arr_get(r0, 1), 64.0, 0.001), "C[0][1]=64");
110+
h r1 = arr_get(C, 1);
111+
assert_true(approx_eq(arr_get(r1, 0), 139.0, 0.001), "C[1][0]=139");
112+
assert_true(approx_eq(arr_get(r1, 1), 154.0, 0.001), "C[1][1]=154");
113+
}
114+
115+
fn test_matmul_row_vector() {
116+
# (1x3) * (3x1) = (1x1) — dot product
117+
h r = [[1, 2, 3]];
118+
h c = [[4], [5], [6]];
119+
h R = arr_matmul(r, c);
120+
h r0 = arr_get(R, 0);
121+
# 1*4 + 2*5 + 3*6 = 32
122+
assert_true(approx_eq(arr_get(r0, 0), 32.0, 0.001), "dot via 1x3*3x1");
123+
}
124+
125+
# ---- Pipeline: substrate-aware features into matmul ----
126+
127+
fn test_matmul_substrate_pipeline() {
128+
# Build feature vector via substrate-typed primitives,
129+
# then project through a Fibonacci-weight matrix.
130+
h feat = [[1, 2, 3, 5]]; # Fibonacci-like row
131+
# 4x2 weight matrix (Fibonacci entries — substrate-resonant)
132+
h W = [[1, 0], [1, 1], [2, 1], [3, 2]];
133+
h out = arr_matmul(feat, W);
134+
h r0 = arr_get(out, 0);
135+
# col 0: 1*1 + 2*1 + 3*2 + 5*3 = 1+2+6+15 = 24
136+
# col 1: 1*0 + 2*1 + 3*1 + 5*2 = 0+2+3+10 = 15
137+
assert_true(approx_eq(arr_get(r0, 0), 24.0, 0.001), "proj col 0 = 24");
138+
assert_true(approx_eq(arr_get(r0, 1), 15.0, 0.001), "proj col 1 = 15");
139+
}

omnimcode-core/src/compiler.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,9 @@ impl Compiler {
251251
| "harmonic_dedupe"
252252
// polish round (arrays)
253253
| "arr_zip" | "arr_unique"
254+
// 2D array primitives (Track 2 — 2026-05-16)
255+
| "arr_matmul" | "arr_transpose"
256+
| "arr_eye" | "arr_zeros_2d"
254257
// introspection
255258
| "defined_functions"
256259
// test runner: get_failures returns array of strings

omnimcode-core/src/interpreter.rs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5594,6 +5594,129 @@ impl Interpreter {
55945594
Err("arr_him_vec: requires an array".to_string())
55955595
}
55965596
}
5597+
// ---- 2D array primitives (Track 2) ----------------------
5598+
//
5599+
// A "matrix" in OMC is an array of arrays, all inner arrays
5600+
// the same length. arr_matmul(A, B) does the standard
5601+
// multiplication: output[i][j] = sum_k A[i][k] * B[k][j].
5602+
// All values treated as f64 to keep results sensible for
5603+
// small floating workloads (autograd-style scalars).
5604+
"arr_matmul" => {
5605+
if args.len() < 2 {
5606+
return Err("arr_matmul requires (matrix_a, matrix_b)".to_string());
5607+
}
5608+
let a = self.eval_expr(&args[0])?;
5609+
let b = self.eval_expr(&args[1])?;
5610+
if let (Value::Array(am), Value::Array(bm)) = (a, b) {
5611+
let arows = am.items.borrow();
5612+
let brows = bm.items.borrow();
5613+
if arows.is_empty() || brows.is_empty() {
5614+
return Err("arr_matmul: empty matrix".to_string());
5615+
}
5616+
let a_rows = arows.len();
5617+
let a_cols = match &arows[0] {
5618+
Value::Array(r) => r.items.borrow().len(),
5619+
_ => return Err("arr_matmul: A rows must be arrays".to_string()),
5620+
};
5621+
let b_rows = brows.len();
5622+
let b_cols = match &brows[0] {
5623+
Value::Array(r) => r.items.borrow().len(),
5624+
_ => return Err("arr_matmul: B rows must be arrays".to_string()),
5625+
};
5626+
if a_cols != b_rows {
5627+
return Err(format!(
5628+
"arr_matmul: shape mismatch — A is {}x{}, B is {}x{}",
5629+
a_rows, a_cols, b_rows, b_cols
5630+
));
5631+
}
5632+
// Materialize as Vec<Vec<f64>> for the inner loop.
5633+
let a_mat: Vec<Vec<f64>> = arows.iter().map(|r| {
5634+
if let Value::Array(row) = r {
5635+
row.items.borrow().iter().map(|v| v.to_float()).collect()
5636+
} else { Vec::new() }
5637+
}).collect();
5638+
let b_mat: Vec<Vec<f64>> = brows.iter().map(|r| {
5639+
if let Value::Array(row) = r {
5640+
row.items.borrow().iter().map(|v| v.to_float()).collect()
5641+
} else { Vec::new() }
5642+
}).collect();
5643+
let mut out: Vec<Value> = Vec::with_capacity(a_rows);
5644+
for i in 0..a_rows {
5645+
let mut row: Vec<Value> = Vec::with_capacity(b_cols);
5646+
for j in 0..b_cols {
5647+
let mut s = 0.0f64;
5648+
for k in 0..a_cols {
5649+
s += a_mat[i][k] * b_mat[k][j];
5650+
}
5651+
row.push(Value::HFloat(s));
5652+
}
5653+
out.push(Value::Array(HArray::from_vec(row)));
5654+
}
5655+
Ok(Value::Array(HArray::from_vec(out)))
5656+
} else {
5657+
Err("arr_matmul: requires two 2D arrays".to_string())
5658+
}
5659+
}
5660+
"arr_transpose" => {
5661+
// Transpose a 2D array. Output[j][i] = input[i][j].
5662+
if args.is_empty() {
5663+
return Err("arr_transpose requires (matrix)".to_string());
5664+
}
5665+
let a = self.eval_expr(&args[0])?;
5666+
if let Value::Array(am) = a {
5667+
let rows = am.items.borrow();
5668+
if rows.is_empty() {
5669+
return Ok(Value::Array(HArray::from_vec(vec![])));
5670+
}
5671+
let n_cols = match &rows[0] {
5672+
Value::Array(r) => r.items.borrow().len(),
5673+
_ => return Err("arr_transpose: rows must be arrays".to_string()),
5674+
};
5675+
let mut out: Vec<Value> = Vec::with_capacity(n_cols);
5676+
for j in 0..n_cols {
5677+
let mut col: Vec<Value> = Vec::with_capacity(rows.len());
5678+
for row_v in rows.iter() {
5679+
if let Value::Array(row) = row_v {
5680+
col.push(row.items.borrow()[j].clone());
5681+
}
5682+
}
5683+
out.push(Value::Array(HArray::from_vec(col)));
5684+
}
5685+
Ok(Value::Array(HArray::from_vec(out)))
5686+
} else {
5687+
Err("arr_transpose: requires a 2D array".to_string())
5688+
}
5689+
}
5690+
"arr_eye" => {
5691+
// arr_eye(n) -> identity matrix (n x n) of ints.
5692+
if args.is_empty() {
5693+
return Err("arr_eye requires (n)".to_string());
5694+
}
5695+
let n = self.eval_expr(&args[0])?.to_int().max(0) as usize;
5696+
let mut rows: Vec<Value> = Vec::with_capacity(n);
5697+
for i in 0..n {
5698+
let mut row: Vec<Value> = Vec::with_capacity(n);
5699+
for j in 0..n {
5700+
row.push(Value::HInt(HInt::new(if i == j { 1 } else { 0 })));
5701+
}
5702+
rows.push(Value::Array(HArray::from_vec(row)));
5703+
}
5704+
Ok(Value::Array(HArray::from_vec(rows)))
5705+
}
5706+
"arr_zeros_2d" => {
5707+
// arr_zeros_2d(rows, cols) -> (rows x cols) zero matrix.
5708+
if args.len() < 2 {
5709+
return Err("arr_zeros_2d requires (rows, cols)".to_string());
5710+
}
5711+
let r = self.eval_expr(&args[0])?.to_int().max(0) as usize;
5712+
let c = self.eval_expr(&args[1])?.to_int().max(0) as usize;
5713+
let mut rows: Vec<Value> = Vec::with_capacity(r);
5714+
for _ in 0..r {
5715+
let row: Vec<Value> = (0..c).map(|_| Value::HInt(HInt::new(0))).collect();
5716+
rows.push(Value::Array(HArray::from_vec(row)));
5717+
}
5718+
Ok(Value::Array(HArray::from_vec(rows)))
5719+
}
55975720
// arr_fold_all(arr) -> new array with every element snapped
55985721
// to its nearest Fibonacci attractor. Vectorized fold.
55995722
// Substrate-canonical denoising / quantization primitive.

0 commit comments

Comments
 (0)