├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── examples.zig └── src ├── alg.zig ├── fixed.zig ├── imaginary.zig ├── mat.zig └── math.zig /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache/ 2 | zig-out/ 3 | build/ 4 | build-*/ 5 | docgen_tmp/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Scott Redig 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alg 2 | 3 | alg.math does concise formulas for custom types. Similar to operator overloading in other languages. 4 | 5 | I'm making this for a private project which needs to do various types of algebraic math. Unless Alg takes on a life of its own, things will mostly be implemented whent I need them. Also I'm new to Zig and things are likely to not be idiomatic. Issues, comments, and requests are welcome, but will only be acted upon as I see fit. 6 | 7 | Example: 8 | 9 | ```zig 10 | var a = alg.mat.Matrix(f32, 2, 1).lit(.{ 11 | 1, 12 | 2, 13 | }); 14 | 15 | var b = alg.mat.Matrix(f32, 1, 2).lit(.{ 16 | 3, 4, 17 | }); 18 | 19 | var c: f32 = 5; 20 | 21 | var result = alg.math("a * b * c", .{ 22 | .a = a, 23 | .b = b, 24 | .c = c, 25 | }); 26 | 27 | try expectEqual(alg.mat.Matrix(f32, 2, 2).lit(.{ 28 | 15, 20, 29 | 30, 40, 30 | }), result); 31 | ``` 32 | 33 | Current limitations: 34 | 35 | - Only a limited number of operations implemented so far. 36 | - There is no order of operations. All chained operations must be the same. Use parethesis to determine order. Eg, "(a * b) + c". 37 | - Chained operations are always carried out left to right. This may be inefficient for some equations, and not standard for others (eg, raising to a power). 38 | 39 | All more complex types have a single underlying type, and all operations require the same underlying type between operands. Eg you can't add a matrix backed by floats with one backed by integers. 40 | 41 | Implemented: 42 | - Matrices: 43 | - Define matrix in terms of rows, and columns. 44 | - Addition between matrices of the same size. 45 | - Multiplication with compatible shaped matricies resulting in a third, possibly differently shapped, matrix. 46 | - Multiplication with scaler values, which multiplies each value in the matrix by the scaler. 47 | - Imaginary Numbers: 48 | - Define in terms of real and imaginary parts. 49 | - Addition and Multiplication with other imaginary numbers. 50 | 51 | Feature Wishlist: 52 | - Types: 53 | - Floats 54 | - Integers 55 | - Comptime float and integers 56 | - Vectory / Array 57 | - Matrix 58 | - Affine Matrix 59 | - Geometic Algebra 60 | - Maybe custom functions? 61 | - Imaginary Numbers 62 | - Quaternion 63 | - Operations 64 | - Add 65 | - multiply 66 | - dot 67 | - etc. 68 | - Built in values? 69 | - e 70 | - pi 71 | - Identity matrix? Is this useful? 72 | - Make parse errors actually useful. 73 | - Pairwise conversion of underlying type. -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.build.Builder) void { 4 | // Standard release options allow the person running `zig build` to select 5 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. 6 | const mode = b.standardReleaseOptions(); 7 | 8 | const lib = b.addStaticLibrary("alg", "src/alg.zig"); 9 | lib.setBuildMode(mode); 10 | lib.install(); 11 | 12 | const main_tests = b.addTest("src/alg.zig"); 13 | main_tests.setBuildMode(mode); 14 | 15 | const test_step = b.step("test", "Run library tests"); 16 | test_step.dependOn(&main_tests.step); 17 | } 18 | -------------------------------------------------------------------------------- /examples.zig: -------------------------------------------------------------------------------- 1 | const expectEqual = @import("std").testing.expectEqual; 2 | const alg = @import("src/alg.zig"); 3 | 4 | test "matrix and scaler multiplication" { 5 | var a = alg.mat.Matrix(f32, 2, 1).lit(.{ 6 | 1, 7 | 2, 8 | }); 9 | 10 | var b = alg.mat.Matrix(f32, 1, 2).lit(.{ 11 | 3, 4, 12 | }); 13 | 14 | var c: f32 = 5; 15 | 16 | var result = alg.math("a * b * c", .{ 17 | .a = a, 18 | .b = b, 19 | .c = c, 20 | }); 21 | 22 | try expectEqual(alg.mat.Matrix(f32, 2, 2).lit(.{ 23 | 15, 20, 24 | 30, 40, 25 | }), result); 26 | } 27 | -------------------------------------------------------------------------------- /src/alg.zig: -------------------------------------------------------------------------------- 1 | pub const math = @import("math.zig").math; 2 | pub const mat = @import("mat.zig"); 3 | pub const imaginary = @import("imaginary.zig"); 4 | pub const fixed = @import("fixed.zig"); 5 | 6 | test "root: run reference all files" { 7 | @import("std").testing.refAllDecls(@This()); 8 | } 9 | -------------------------------------------------------------------------------- /src/fixed.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const math = @import("math.zig").math; 3 | const shr = std.math.shr; 4 | const shl = std.math.shl; 5 | 6 | const expectEqual = @import("std").testing.expectEqual; 7 | 8 | pub const D64 = struct { 9 | v: i64, 10 | const offset = 32; 11 | const lowBits = 0xFFFFFFFF; 12 | 13 | pub const pi = lit("3.1415926535897932384626433"); 14 | 15 | pub fn lit(comptime str: [:0]const u8) D64 { 16 | if (str.len == 0) { 17 | @compileError("Empty fixed point literal."); 18 | } 19 | 20 | // Parse as a fixed 128 with 64 fractal bits, then shift 21 | // to D64. 22 | const parseOne = 1 << 64; 23 | comptime var r: i128 = 0; 24 | 25 | comptime var i: usize = 0; 26 | const negative = str[0] == '-'; 27 | if (negative) { 28 | i += 1; 29 | } 30 | 31 | parse: { 32 | inline while (true) { 33 | if (i >= str.len) { 34 | break :parse; 35 | } 36 | const c = str[i]; 37 | i += 1; 38 | if (c >= '0' and c <= '9') { 39 | r *= 10; 40 | r += parseOne * @intCast(i128, c - '0'); 41 | } else if (c == '.') { 42 | break; 43 | } else { 44 | @compileError("Not a valid fixed point literal: " ++ str); 45 | } 46 | } 47 | 48 | comptime var divisor: i128 = 1; 49 | inline while (true) { 50 | if (i >= str.len) { 51 | break :parse; 52 | } 53 | divisor *= 10; 54 | const c = str[i]; 55 | i += 1; 56 | if (c >= '0' and c <= '9') { 57 | r += @divFloor(parseOne, divisor) * @intCast(i128, c - '0'); 58 | } else { 59 | @compileError("Not a valid fixed point literal: " ++ str); 60 | } 61 | } 62 | } 63 | 64 | if (negative) { 65 | r *= -1; 66 | } 67 | return comptime D64{ .v = @intCast(i64, shr(i128, r, 32)) }; 68 | } 69 | 70 | pub fn add(self: D64, other: D64) D64 { 71 | // a*2^f + b*2^f = (a + b)*2^f 72 | return D64{ .v = self.v + other.v }; 73 | } 74 | 75 | pub fn sub(self: D64, other: D64) D64 { 76 | // a*2^f - b*2^f = (a - b)*2^f 77 | return D64{ .v = self.v - other.v }; 78 | } 79 | 80 | pub fn mul(self: D64, other: D64) D64 { 81 | // a*2^f * b*2^f = a*b*2^(2f) 82 | // (a*2^f * b*2^f) / 2^f = a*b*2^f 83 | // const selfLow = self.v & lowBits; 84 | // const selfHigh = shr(i64, self.v, 32); 85 | // const otherLow = other.v & lowBits; 86 | // const otherHigh = shr(i64, other.v, 32); 87 | 88 | // std.debug.print("--- {} * {}\n", .{ self.toF32(), other.toF32() }); 89 | // std.debug.print("--- {b} = {b} + {b}\n", .{ self.v, selfHigh, selfLow }); 90 | // std.debug.print("--- {b} = {b} + {b}\n", .{ other.v, otherHigh, otherLow }); 91 | // return D64{ .v = shr(i64, selfLow * otherLow, 32) + (selfLow * otherHigh) + (selfHigh * otherLow) + ((selfHigh * otherHigh) << 32) }; 92 | 93 | const rb = @intCast(i128, self.v) * @intCast(i128, other.v); 94 | 95 | return D64{ .v = @intCast(i64, shr(i128, rb, 32)) }; 96 | } 97 | 98 | pub fn div(self: D64, other: D64) D64 { 99 | // (a*2^f) / (b*2^f) = a/b 100 | // (a*2^f*2^f) / (b*2^f) = (a/b) * 2^f 101 | 102 | const sb = shl(i128, @intCast(i128, self.v), 32); 103 | const ob = @intCast(i128, other.v); 104 | 105 | const rb = @divFloor(sb, ob); 106 | 107 | return D64{ .v = @intCast(i64, rb) }; 108 | } 109 | 110 | pub fn mod(self: D64, other: D64) D64 { 111 | var sb = shl(i128, @intCast(i128, self.v), 32); 112 | if (self.v < 0) { 113 | sb |= 0xFFFFFFFF; 114 | } 115 | const ob = shl(i128, @intCast(i128, other.v), 32); 116 | const rb = @mod(sb, ob); 117 | return D64{ .v = @intCast(i64, shr(i128, rb, 32)) }; 118 | } 119 | 120 | pub fn sin(self: D64) D64 { 121 | const twoPi = comptime pi.mul(lit("2")); 122 | const piOverTwo = comptime pi.div(lit("2")); 123 | const partial = self.mod(twoPi).div(piOverTwo); // 0 to 4. 124 | 125 | if (partial.lessThan(lit("1"))) { 126 | return zeroToOneSin(partial); 127 | } 128 | if (partial.lessThan(lit("2"))) { 129 | return zeroToOneSin(lit("2").sub(partial)); 130 | } 131 | if (partial.lessThan(lit("3"))) { 132 | return lit("-1").mul(zeroToOneSin(partial.sub(lit("2")))); 133 | } 134 | return lit("-1").mul(zeroToOneSin(lit("4").sub(partial))); 135 | } 136 | 137 | pub fn cos(self: D64) D64 { 138 | const twoPi = comptime pi.mul(lit("2")); 139 | const piOverTwo = comptime pi.div(lit("2")); 140 | const partial = self.mod(twoPi).div(piOverTwo); // 0 to 4. 141 | 142 | if (partial.lessThan(lit("1"))) { 143 | return zeroToOneSin(lit("1").sub(partial)); 144 | } 145 | if (partial.lessThan(lit("2"))) { 146 | return lit("-1").mul(zeroToOneSin(partial.sub(lit("1")))); 147 | } 148 | if (partial.lessThan(lit("3"))) { 149 | return lit("-1").mul(zeroToOneSin(lit("3").sub(partial))); 150 | } 151 | return zeroToOneSin(partial.sub(lit("3"))); 152 | } 153 | 154 | fn zeroToOneSin(z: D64) D64 { 155 | // Is correct to within +-0.0075. A better Taylor series would yield better accuracy. 156 | // Equation source: https://www.nullhardware.com/blog/fixed-point-sine-and-cosine-for-embedded-systems/ 157 | const a = comptime math("four * ((three / pi) - (nine / sixteen))", .{ 158 | .four = lit("4"), 159 | .three = lit("3"), 160 | .pi = pi, 161 | .nine = lit("9"), 162 | .sixteen = lit("16"), 163 | }); 164 | const b = comptime math("(five / two) - (two * a)", .{ 165 | .two = lit("2"), 166 | .a = a, 167 | .five = lit("5"), 168 | }); 169 | const c = comptime math("a - (three / two)", .{ 170 | .a = a, 171 | .three = lit("3"), 172 | .two = lit("2"), 173 | }); 174 | const zSquared = math("z * z", .{ .z = z }); 175 | const zCubed = math("zSquared * z", .{ .zSquared = zSquared, .z = z }); 176 | const zFifth = math("zCubed * z", .{ .zCubed = zCubed, .z = z }); 177 | return math("(a * z) + (b * zCubed) + (c * zFifth)", .{ 178 | .a = a, 179 | .z = z, 180 | .b = b, 181 | .zCubed = zCubed, 182 | .c = c, 183 | .zFifth = zFifth, 184 | }); 185 | } 186 | 187 | pub fn floor(self: D64) D64 { 188 | return D64{ .v = self.v & (~@as(i64, lowBits)) }; 189 | } 190 | 191 | pub fn toF32(self: D64) f32 { 192 | return @intToFloat(f32, self.v) / (1 << 32); 193 | } 194 | 195 | pub fn lessThan(self: D64, other: D64) bool { 196 | return self.v < other.v; 197 | } 198 | 199 | pub fn sqrt(self: D64) D64 { 200 | var v: i128 = @intCast(i128, self.v); 201 | if (v < 0) { 202 | v *= -1; 203 | } 204 | 205 | var r: i128 = 0; 206 | var i: i128 = 1 << 63; 207 | 208 | while (true) { 209 | const testR = r | i; 210 | if (shr(i128, testR * testR, 32) <= v) { 211 | r = testR; 212 | } 213 | if (i == 1) { 214 | break; 215 | } 216 | 217 | i >>= 1; 218 | } 219 | 220 | return D64{ .v = @intCast(i64, r) }; 221 | } 222 | 223 | // const rb = @intCast(i128, self.v) * @intCast(i128, other.v); 224 | 225 | // return D64{ .v = @intCast(i64, shr(i128, rb, 32)) }; 226 | 227 | // var v = self.v; 228 | // if (v < 0) { 229 | // v *= -1; 230 | // if (v < 0) { 231 | // v += 1; 232 | // v *= -1; 233 | // } 234 | // } 235 | // var i: i64 = 1 << (32 + 15); 236 | // var r: D64 = D64{ .v = 0 }; 237 | // while (true) { 238 | // const testR = D64{ .v = r.v | i }; 239 | // if (testR.mul(testR).v <= v) { 240 | // r = testR; 241 | // } 242 | // if (i == 1) { 243 | // return r; 244 | // } 245 | // i >>= 1; 246 | // } 247 | // } 248 | }; 249 | 250 | test "lit" { 251 | const a = D64.lit("2"); 252 | try expectEqual(@as(f32, 2.0), a.toF32()); 253 | 254 | const b = D64.lit("-2"); 255 | try expectEqual(@as(f32, -2.0), b.toF32()); 256 | } 257 | 258 | test "mul" { 259 | const a = D64.lit("2"); 260 | try expectEqual(@as(f32, 4.0), math("a * a", .{ .a = a }).toF32()); 261 | 262 | const b = D64.lit("0.5"); 263 | try expectEqual(@as(f32, 0.25), math("b * b", .{ .b = b }).toF32()); 264 | 265 | const c = D64.lit("2.5"); 266 | try expectEqual(@as(f32, 6.25), math("c * c", .{ .c = c }).toF32()); 267 | 268 | const d = D64.lit("-2.5"); 269 | try expectEqual(@as(f32, 6.25), math("d * d", .{ .d = d }).toF32()); 270 | try expectEqual(@as(f32, -6.25), math("d * c", .{ .c = c, .d = d }).toF32()); 271 | try expectEqual(@as(f32, -6.25), math("c * d", .{ .c = c, .d = d }).toF32()); 272 | } 273 | 274 | test "add" { 275 | const a = D64.lit("2"); 276 | const b = D64.lit("0.5"); 277 | try expectEqual(@as(f32, 2.5), math("a + b", .{ .a = a, .b = b }).toF32()); 278 | } 279 | 280 | test "div" { 281 | const a = D64.lit("2"); 282 | const b = D64.lit("0.5"); 283 | try expectEqual(@as(f32, 4), math("a / b", .{ .a = a, .b = b }).toF32()); 284 | } 285 | 286 | test "mod" { 287 | const d = D64.lit; 288 | try expectEqual(d("1"), d("-5").mod(d("3"))); 289 | try expectEqual(d("2"), d("5").mod(d("3"))); 290 | try expectEqual( 291 | d("-5.5").sub(d("-5.5").div(d("3")).floor().mul(d("3"))), 292 | d("-5.5").mod(d("3")), 293 | ); 294 | // The literal seems to be the wrong part here, so exact comparison is slightly off? 295 | try std.testing.expectApproxEqAbs(d("0.5").toF32(), d("-5.5").mod(d("3")).toF32(), 0.0075); 296 | } 297 | 298 | test "zeroToOneSin" { 299 | var i: usize = 0; 300 | const pointOne = D64.lit("0.01"); 301 | var value = D64.lit("0"); 302 | while (i <= 100) : (i += 1) { 303 | const calculated = value.zeroToOneSin().toF32(); 304 | const groundTruth = std.math.sin(value.toF32() * std.math.pi / 2); 305 | try std.testing.expectApproxEqAbs(groundTruth, calculated, 0.0075); 306 | 307 | value = value.add(pointOne); 308 | } 309 | } 310 | 311 | test "sin and cos" { 312 | var i: usize = 0; 313 | const one = D64.lit("1"); 314 | // var value = D64.lit("0"); 315 | var value = D64.lit("-50"); 316 | while (i <= 100) : (i += 1) { 317 | const sinExpected = std.math.sin(value.toF32()); 318 | const sinActual = value.sin().toF32(); 319 | try std.testing.expectApproxEqAbs(sinExpected, sinActual, 0.0075); 320 | 321 | const cosExpected = std.math.cos(value.toF32()); 322 | const cosActual = value.cos().toF32(); 323 | try std.testing.expectApproxEqAbs(cosExpected, cosActual, 0.0075); 324 | 325 | value = value.add(one); 326 | } 327 | } 328 | 329 | test "floor" { 330 | try expectEqual(D64.lit("1"), D64.lit("1.5").floor()); 331 | try expectEqual(D64.lit("-1"), D64.lit("-0.5").floor()); 332 | } 333 | 334 | test "sqrt" { 335 | try expectEqual(D64.lit("2"), D64.lit("4").sqrt()); 336 | try expectEqual(D64.lit("2"), D64.lit("-4").sqrt()); // Not great, but better than nothing... 337 | try expectEqual(D64.lit("46340.9500011").toF32(), D64.lit("2147483647").sqrt().toF32()); 338 | } 339 | -------------------------------------------------------------------------------- /src/imaginary.zig: -------------------------------------------------------------------------------- 1 | const expectEqual = @import("std").testing.expectEqual; 2 | const Vector = @import("std").meta.Vector; 3 | 4 | const math = @import("math.zig").math; 5 | 6 | pub fn Img(comptime T: type) type { 7 | return struct { 8 | v: Vector(2, T), 9 | 10 | pub fn init(scalar: T, imaginary: T) Img(T) { 11 | return Img(T){ 12 | .v = [_]T{ scalar, imaginary }, 13 | }; 14 | } 15 | 16 | pub fn mul(self: Img(T), other: Img(T)) Img(T) { 17 | // I have no clue if this Vector version is faster than 18 | // just writing out the multiplcations. Good practice 19 | // with Vectors, though. 20 | const scalarOnly: Vector(2, i32) = [_]i32{ 0, 0 }; 21 | const imaginaryOnly: Vector(2, i32) = [_]i32{ 1, 1 }; 22 | const flipped: Vector(2, i32) = [_]i32{ 1, 0 }; 23 | const invertISquared: Vector(2, T) = [_]T{ -1, 1 }; 24 | 25 | var otherS = @shuffle(T, other.v, undefined, scalarOnly); 26 | var otherI = @shuffle(T, other.v, undefined, imaginaryOnly); 27 | var selfFlipped = @shuffle(T, self.v, undefined, flipped); 28 | 29 | return Img(T){ 30 | .v = (self.v * otherS) + (selfFlipped * otherI * invertISquared), 31 | }; 32 | } 33 | 34 | pub fn add(self: Img(T), other: Img(T)) Img(T) { 35 | return Img(T){ .v = self.v + other.v }; 36 | } 37 | }; 38 | } 39 | 40 | test "imaginary" { 41 | const T = Img(f32); 42 | try expectEqual(T.init(1, 1), math("a + b", .{ 43 | .a = T.init(0, 1), 44 | .b = T.init(1, 0), 45 | })); 46 | 47 | try expectEqual(T.init(-5, 10), math("a * b", .{ 48 | .a = T.init(1, 2), 49 | .b = T.init(3, 4), 50 | })); 51 | } 52 | -------------------------------------------------------------------------------- /src/mat.zig: -------------------------------------------------------------------------------- 1 | const expectEqual = @import("std").testing.expectEqual; 2 | const Vector = @import("std").meta.Vector; 3 | 4 | const math = @import("math.zig").math; 5 | 6 | pub fn Matrix(comptime T: type, rows: comptime_int, columns: comptime_int) type { 7 | switch (@typeInfo(T)) { 8 | .Int, .Float => {}, 9 | else => @compileError("Matrix only supports integers and floats"), 10 | } 11 | 12 | return struct { 13 | columnMajorValues: [columns * rows]T, 14 | 15 | const T = T; 16 | const rows = rows; 17 | const columns = columns; 18 | const Self = @This(); 19 | const SelfV = Vector(columns * rows, T); 20 | const isMatrixType = true; 21 | 22 | pub fn lit(v: anytype) @This() { 23 | const VType = @TypeOf(v); 24 | const vTypeInfo = @typeInfo(VType); 25 | if (vTypeInfo != .Struct) { 26 | @compileError("Expected tuple or struct argument, found " ++ @typeName(VType)); 27 | } 28 | const fieldsInfo = vTypeInfo.Struct.fields; 29 | 30 | if (fieldsInfo.len != rows * columns) { 31 | @compileError("Wrong size literal for matrix"); 32 | } 33 | 34 | var r: @This() = undefined; 35 | comptime var column: usize = 0; 36 | inline while (column < columns) : (column += 1) { 37 | comptime var row: usize = 0; 38 | inline while (row < rows) : (row += 1) { 39 | r.columnMajorValues[column * rows + row] = @field(v, fieldsInfo[row * columns + column].name); 40 | } 41 | } 42 | return r; 43 | } 44 | 45 | pub fn ident() Self { 46 | if (rows != columns) { 47 | @panic("ident is only valid for square matrices."); 48 | } 49 | const arr = comptime init: { 50 | var arrInit = [_]T{0} ** (columns * rows); 51 | var i: usize = 0; 52 | while (i < arrInit.len) : (i += columns + 1) { 53 | arrInit[i] = 1; 54 | } 55 | break :init arrInit; 56 | }; 57 | return Self{ 58 | .columnMajorValues = arr, 59 | }; 60 | } 61 | 62 | pub fn mul(lhs: anytype, rhs: anytype) Self.mulReturnType(@TypeOf(lhs), @TypeOf(rhs)) { 63 | const Lhs = @TypeOf(lhs); 64 | const Rhs = @TypeOf(rhs); 65 | if (Lhs == Self and Rhs == T) { 66 | return lhs.mulScale(rhs); 67 | } 68 | if (Lhs == T and Rhs == Self) { 69 | return rhs.mulScale(lhs); 70 | } 71 | var r: Self.mulReturnType(Lhs, Rhs) = undefined; 72 | 73 | var selfRows: [rows]Vector(columns, T) = undefined; 74 | // TODO: Maybe putting into a big vector and shuffling out values 75 | // would be faster? 76 | 77 | { 78 | var row: usize = 0; 79 | while (row < rows) : (row += 1) { 80 | var rowArr: [columns]T = undefined; 81 | var column: usize = 0; 82 | while (column < columns) : (column += 1) { 83 | rowArr[column] = lhs.columnMajorValues[column * rows + row]; 84 | } 85 | selfRows[row] = rowArr; 86 | } 87 | } 88 | 89 | var column: usize = 0; 90 | while (column < Rhs.columns) : (column += 1) { 91 | var columnVec: Vector(Rhs.rows, T) = rhs.columnMajorValues[column * Rhs.rows ..][0..Rhs.rows].*; 92 | var row: usize = 0; 93 | while (row < rows) : (row += 1) { 94 | r.columnMajorValues[column * rows + row] = @reduce(.Add, selfRows[row] * columnVec); 95 | } 96 | } 97 | 98 | return r; 99 | } 100 | 101 | pub fn mulReturnType(comptime LhsMaybe: type, comptime RhsMaybe: type) type { 102 | const Lhs = DepointerType(LhsMaybe); 103 | const Rhs = DepointerType(RhsMaybe); 104 | 105 | if ((Lhs == Self and Rhs == T) or (Lhs == T and Rhs == Self)) { 106 | return Self; 107 | } 108 | if (!@hasDecl(Rhs, "isMatrixType") or Lhs != Self) { 109 | return void; 110 | } 111 | if (T != Rhs.T) { 112 | return void; 113 | // @compileError("Matrix multiplcation value types must match"); 114 | } 115 | if (columns != Rhs.rows) { 116 | return void; 117 | // @compileError("Matrix multiplcation sizes incompatible."); 118 | } 119 | return Matrix(T, rows, Rhs.columns); 120 | } 121 | 122 | fn mulScale(self: Self, scaler: T) Self { 123 | var scalerVec = @splat(columns * rows, scaler); 124 | return Self{ 125 | .columnMajorValues = @as(SelfV, self.columnMajorValues) * scalerVec, 126 | }; 127 | } 128 | 129 | pub fn add(self: Self, other: Self) Self { 130 | return Self{ 131 | .columnMajorValues = @as(SelfV, self.columnMajorValues) + @as(SelfV, other.columnMajorValues), 132 | }; 133 | } 134 | 135 | pub fn sub(self: Self, other: Self) Self { 136 | return Self{ 137 | .columnMajorValues = @as(SelfV, self.columnMajorValues) - @as(SelfV, other.columnMajorValues), 138 | }; 139 | } 140 | }; 141 | } 142 | 143 | fn DepointerType(comptime T: type) type { 144 | switch (@typeInfo(T)) { 145 | .Pointer => |ptr| { 146 | switch (@typeInfo(ptr.child)) { 147 | .Struct, .Enum, .Union => return ptr.child, 148 | else => {}, 149 | } 150 | }, 151 | else => {}, 152 | } 153 | return T; 154 | } 155 | 156 | test "matrix multiplcation type" { 157 | const A = Matrix(f32, 5, 3); 158 | const B = Matrix(f32, 3, 4); 159 | const C = comptime A.mulReturnType(A, B); 160 | try expectEqual(5, C.rows); 161 | try expectEqual(4, C.columns); 162 | } 163 | 164 | test "matrix literal" { 165 | var a = Matrix(f32, 2, 2).lit(.{ 166 | 2, 3, 167 | 4, 5, 168 | }); 169 | try expectEqual(@as(f32, 2), a.columnMajorValues[0]); 170 | try expectEqual(@as(f32, 4), a.columnMajorValues[1]); 171 | try expectEqual(@as(f32, 3), a.columnMajorValues[2]); 172 | try expectEqual(@as(f32, 5), a.columnMajorValues[3]); 173 | } 174 | 175 | test "matrix multiplcation" { 176 | var a = Matrix(f32, 2, 3).lit(.{ 177 | 2, 3, 4, 178 | 5, 6, 7, 179 | }); 180 | 181 | var b = Matrix(f32, 3, 2).lit(.{ 182 | 8, 9, 183 | 10, 11, 184 | 12, 13, 185 | }); 186 | 187 | var c = math("a * b", .{ 188 | .a = a, 189 | .b = b, 190 | }); 191 | 192 | try expectEqual(Matrix(f32, 2, 2).lit(.{ 193 | 94, 103, 194 | 184, 202, 195 | }), c); 196 | 197 | try expectEqual(Matrix(f32, 2, 2).lit(.{ 198 | 94, 103, 199 | 184, 202, 200 | }), a.mul(b)); 201 | } 202 | 203 | test "matrix addition" { 204 | var a = Matrix(f32, 2, 3).lit(.{ 205 | 2, 3, 4, 206 | 5, 6, 7, 207 | }); 208 | 209 | var b = Matrix(f32, 2, 3).lit(.{ 210 | 8, 9, 10, 211 | 11, 12, 13, 212 | }); 213 | 214 | var c = math("a + b", .{ 215 | .a = a, 216 | .b = b, 217 | }); 218 | 219 | try expectEqual(Matrix(f32, 2, 3).lit(.{ 220 | 10, 12, 14, 221 | 16, 18, 20, 222 | }), c); 223 | 224 | c = math("a - b", .{ 225 | .a = a, 226 | .b = b, 227 | }); 228 | 229 | try expectEqual(Matrix(f32, 2, 3).lit(.{ 230 | -6, -6, -6, 231 | -6, -6, -6, 232 | }), c); 233 | } 234 | 235 | test "matrix scale" { 236 | var a = Matrix(f32, 2, 2).lit(.{ 237 | 1, 2, 238 | 3, 4, 239 | }); 240 | var b: f32 = 2; 241 | 242 | var c = math("a * b", .{ 243 | .a = a, 244 | .b = b, 245 | }); 246 | 247 | try expectEqual(Matrix(f32, 2, 2).lit(.{ 248 | 2, 4, 249 | 6, 8, 250 | }), c); 251 | 252 | var d = math("b * a", .{ 253 | .a = a, 254 | .b = b, 255 | }); 256 | 257 | try expectEqual(Matrix(f32, 2, 2).lit(.{ 258 | 2, 4, 259 | 6, 8, 260 | }), d); 261 | } 262 | 263 | test "identity matrix" { 264 | const T = Matrix(f32, 5, 5); 265 | 266 | var a = T.lit(.{ 267 | 1, 0, 0, 0, 0, 268 | 0, 1, 0, 0, 0, 269 | 0, 0, 1, 0, 0, 270 | 0, 0, 0, 1, 0, 271 | 0, 0, 0, 0, 1, 272 | }); 273 | 274 | var b = T.ident(); 275 | 276 | try expectEqual(a, b); 277 | } 278 | -------------------------------------------------------------------------------- /src/math.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const expectEqual = @import("std").testing.expectEqual; 3 | 4 | test "math" { 5 | // std.debug.print("\n{}\n", .{math("a", .{ .a = @as(i8, 7) })}); 6 | try expectEqual(@as(i8, 5), math("a", .{ .a = @as(i8, 5) })); 7 | try expectEqual(@as(i8, 15), math("a + a + a", .{ .a = @as(i8, 5) })); 8 | // std.debug.print("\n{}\n", .{math("a + b", .{ 9 | // .a = @as(i8, 7), 10 | // .b = @as(i8, 2), 11 | // })}); 12 | try expectEqual(15, math("a - b", .{ .a = 20, .b = 5 })); 13 | // std.debug.print("\n{}\n", .{math("a - b", .{ .a = 1000000, .b = 5 })}); 14 | // std.debug.print("\n{}\n", .{math("(-a) + a + (a+a+a", .{ .a = 10 })}); 15 | } 16 | 17 | // Using an allocator (even just a fixed buffer) doesn't work during comptime yet. 18 | // Instead just allocate something on the stack at the entry point, and work 19 | // from there. If we could use a real allocator, it'd be better for ast to 20 | // contain pointer to concrete types, instead of concrete types pointing to asts. 21 | // This would more efficiently pack memory. However this way is much simpler given 22 | // the constraints. 23 | const StupidAlloc = struct { 24 | asts: [100]Ast, 25 | index: isize, 26 | 27 | fn next(comptime self: *StupidAlloc) *Ast { 28 | var r = &self.asts[self.index]; 29 | self.index += 1; 30 | return r; 31 | } 32 | }; 33 | 34 | pub fn math(comptime eq: [:0]const u8, args: anytype) ReturnType(eq, @TypeOf(args)) { 35 | comptime var parser = comptime Parser.init(eq, @TypeOf(args)); 36 | comptime var root = parser.parse(); 37 | return root.eval(args); 38 | } 39 | 40 | fn ReturnType(comptime eq: [:0]const u8, argsType: type) type { 41 | comptime @setEvalBranchQuota(10000); 42 | comptime var parser = comptime Parser.init(eq, argsType); 43 | comptime var root = parser.parse(); 44 | return root.ReturnType(); 45 | } 46 | 47 | const BlockTerm = enum { 48 | eof, 49 | rParen, 50 | 51 | // fn name(self: BlockTerm) [:0]const u8 { 52 | // switch (self) { 53 | // .eof => "end of equation", 54 | // .closeParen => "close parentheses", 55 | // } 56 | // } 57 | }; 58 | 59 | const Parser = struct { 60 | alloc: StupidAlloc, 61 | argsType: type, 62 | tokenizer: Tokenizer, 63 | 64 | // Careful: StupidAlloc is large, and pointers to it's allocations will change 65 | // when it's moved. 66 | fn init(eq: [:0]const u8, comptime argsType: type) Parser { 67 | return Parser{ 68 | .alloc = StupidAlloc{ 69 | .asts = undefined, 70 | .index = 0, 71 | }, 72 | .argsType = argsType, 73 | .tokenizer = Tokenizer.init(eq), 74 | }; 75 | } 76 | 77 | fn parse(comptime self: *Parser) *Ast { 78 | // parse should only be called once, but it's easy to fix that not working, 79 | // so why not? 80 | self.tokenizer.index = 0; 81 | return self.parseStatement(BlockTerm.eof); 82 | } 83 | 84 | fn parseStatement(comptime self: *Parser, expectedBlockTerm: BlockTerm) *Ast { 85 | var lhs = switch (self.parseElement()) { 86 | .operand => |op| { 87 | var rhs = switch (self.parseElement()) { 88 | .operand => @compileError("Two operands in a row."), 89 | .value => |rhs| rhs, 90 | .blockTerm => { 91 | @compileError("Unexpected end of block"); 92 | }, 93 | }; 94 | switch (self.parseElement()) { 95 | .operand => @compileError("Operand after unary operation"), 96 | .value => @compileError("Value after unary operation"), 97 | .blockTerm => |blockTerm| { 98 | if (expectedBlockTerm == blockTerm) { 99 | return UnaryOp.init(&self.alloc, op, rhs); 100 | } 101 | @compileError("Incorrect termination of a statement."); 102 | }, 103 | } 104 | }, 105 | .value => |lhs| lhs, 106 | .blockTerm => { 107 | @compileError("Empty block"); 108 | }, 109 | }; 110 | var firstOp: ?Token.Tag = null; 111 | while (true) { 112 | var op = switch (self.parseElement()) { 113 | .operand => |op| op, 114 | .value => { 115 | @compileError("Multiple values in a row"); 116 | }, 117 | .blockTerm => |blockTerm| { 118 | if (expectedBlockTerm == blockTerm) { 119 | return lhs; 120 | } 121 | @compileError("Incorrect termination of a statement."); 122 | }, 123 | }; 124 | if (firstOp) |correctOp| { 125 | if (op != correctOp) { 126 | @compileError("Mismatching operations"); 127 | } 128 | } else { 129 | firstOp = op; 130 | } 131 | var rhs = switch (self.parseElement()) { 132 | .operand => @compileError("Unexpected double operand"), 133 | .value => |rhs| rhs, 134 | .blockTerm => @compileError("Unexpected block termination"), 135 | }; 136 | lhs = makeBinaryOp(&self.alloc, lhs, op, rhs); 137 | } 138 | } 139 | 140 | const Element = union(enum) { 141 | operand: Token.Tag, 142 | value: *Ast, 143 | blockTerm: BlockTerm, 144 | }; 145 | 146 | fn parseElement(comptime self: *Parser) Element { 147 | var token = self.tokenizer.next(); 148 | switch (token.tag) { 149 | .invalid => @compileError("Invalid equation"), 150 | 151 | .eof => { 152 | return Element{ .blockTerm = BlockTerm.eof }; 153 | }, 154 | .lParen => { 155 | return Element{ .value = self.parseStatement(BlockTerm.rParen) }; 156 | }, 157 | .rParen => { 158 | return Element{ .blockTerm = BlockTerm.rParen }; 159 | }, 160 | 161 | .plus, .minus, .asterisk, .slash => { 162 | return Element{ .operand = token.tag }; 163 | }, 164 | 165 | .identifier => { 166 | return Element{ .value = Identifier.init(&self.alloc, self.tokenizer.source(token), self.argsType) }; 167 | }, 168 | } 169 | } 170 | }; 171 | 172 | const Ast = union(enum) { 173 | identifier: Identifier, 174 | scalerBinaryOp: ScalerBinaryOp, 175 | fnBinaryOp: FnBinaryOp, 176 | unaryOp: UnaryOp, 177 | 178 | fn eval(comptime ast: *Ast, args: anytype) ast.ReturnType() { 179 | return switch (ast.*) { 180 | .identifier => |v| v.eval(args), 181 | .scalerBinaryOp => |v| v.eval(args), 182 | .fnBinaryOp => |v| v.eval(args), 183 | .unaryOp => |v| v.eval(args), 184 | }; 185 | } 186 | 187 | fn ReturnType(comptime ast: *Ast) type { 188 | return switch (ast.*) { 189 | .identifier => |v| v.ReturnType, 190 | .scalerBinaryOp => |v| v.ReturnType, 191 | .fnBinaryOp => |v| v.ReturnType, 192 | .unaryOp => |v| v.ReturnType, 193 | }; 194 | } 195 | }; 196 | 197 | const Identifier = struct { 198 | name: []const u8, 199 | ReturnType: type, 200 | 201 | fn init(comptime alloc: *StupidAlloc, name: []const u8, comptime argsType: type) *Ast { 202 | comptime var S = switch (@typeInfo(argsType)) { 203 | .Struct => |v| v, 204 | else => @compileError("math args must be a struct or touple."), 205 | }; 206 | for (S.fields) |field| { 207 | if (std.mem.eql(u8, name, field.name)) { 208 | var r = alloc.next(); 209 | r.* = Ast{ 210 | .identifier = Identifier{ 211 | .name = name, 212 | .ReturnType = field.field_type, 213 | }, 214 | }; 215 | return r; 216 | } 217 | } else { 218 | @compileError("Identifier in equation not found in passed info: " ++ name); 219 | } 220 | } 221 | 222 | fn eval(comptime ident: *const Identifier, args: anytype) ident.ReturnType { 223 | return @field(args, ident.name); 224 | } 225 | }; 226 | 227 | fn makeBinaryOp(comptime alloc: *StupidAlloc, lhs: *Ast, opToken: Token.Tag, rhs: *Ast) *Ast { 228 | const r = alloc.next(); 229 | const Lhs = lhs.ReturnType(); 230 | const Rhs = rhs.ReturnType(); 231 | 232 | if (isBuiltinScalar(Lhs) and Lhs == Rhs) { 233 | const op = switch (opToken) { 234 | .plus => ScalerBinaryOp.Op.addErr, 235 | .minus => ScalerBinaryOp.Op.subErr, 236 | .asterisk => ScalerBinaryOp.Op.mulErr, 237 | else => @compileError("Invalid binary operator for scaler value"), 238 | }; 239 | 240 | r.* = Ast{ 241 | .scalerBinaryOp = ScalerBinaryOp{ 242 | .lhs = lhs, 243 | .op = op, 244 | .rhs = rhs, 245 | .ReturnType = Lhs, 246 | }, 247 | }; 248 | return r; 249 | } 250 | 251 | const opName = switch (opToken) { 252 | .plus => "add", 253 | .minus => "sub", 254 | .asterisk => "mul", 255 | .slash => "div", 256 | else => @compileError("Invalid binary operator for method call"), 257 | }; 258 | 259 | if (CheckTypeForBinaryMethod(opName, Lhs, Lhs, Rhs)) |T| { 260 | r.* = Ast{ .fnBinaryOp = FnBinaryOp{ 261 | .CallType = Lhs, 262 | .lhs = lhs, 263 | .op = opName, 264 | .rhs = rhs, 265 | .ReturnType = T, 266 | } }; 267 | return r; 268 | } 269 | 270 | if (CheckTypeForBinaryMethod(opName, Rhs, Lhs, Rhs)) |T| { 271 | r.* = Ast{ .fnBinaryOp = FnBinaryOp{ 272 | .CallType = Rhs, 273 | .lhs = rhs, 274 | .op = opName, 275 | .rhs = lhs, 276 | .ReturnType = T, 277 | } }; 278 | return r; 279 | } 280 | @compileError(@typeName(Lhs) ++ " and " ++ @typeName(Rhs) ++ " are incompatible for " ++ opName ++ "."); 281 | } 282 | 283 | // If A has a method named opName, which takes B, return the return type. Otherwise null. 284 | fn CheckTypeForBinaryMethod(comptime opName: [:0]const u8, comptime C: type, comptime A: type, comptime B: type) ?type { 285 | if (isBuiltinScalar(C)) { 286 | return null; 287 | } 288 | if (!@hasDecl(C, opName)) { 289 | return null; 290 | } 291 | 292 | const declInfo = std.meta.declarationInfo(C, opName); 293 | const FnType = switch (declInfo.data) { 294 | .Type => return null, 295 | .Var => return null, 296 | .Fn => |f| f.fn_type, 297 | }; 298 | 299 | const fnTypeInfo = switch (@typeInfo(FnType)) { 300 | .Fn => |f| f, 301 | else => unreachable, 302 | }; 303 | 304 | if (fnTypeInfo.args.len == 2 and 305 | fnTypeInfo.args[0].arg_type == A and 306 | fnTypeInfo.args[1].arg_type == B) 307 | { 308 | return fnTypeInfo.return_type; 309 | } 310 | 311 | const fnName = opName ++ "ReturnType"; 312 | if (@hasDecl(C, fnName)) { 313 | const R = @field(C, fnName)(A, B); 314 | if (R != void) { 315 | return R; 316 | } 317 | } 318 | return null; 319 | } 320 | 321 | const ScalerBinaryOp = struct { 322 | lhs: *Ast, 323 | op: Op, 324 | rhs: *Ast, 325 | ReturnType: type, 326 | 327 | const Op = enum { 328 | addErr, 329 | subErr, 330 | mulErr, 331 | }; 332 | 333 | fn eval(comptime self: *const ScalerBinaryOp, args: anytype) self.ReturnType { 334 | const lhs = self.lhs.eval(args); 335 | const rhs = self.rhs.eval(args); 336 | 337 | return switch (self.op) { 338 | .addErr => lhs + rhs, 339 | .subErr => lhs - rhs, 340 | .mulErr => lhs * rhs, 341 | }; 342 | } 343 | }; 344 | 345 | const FnBinaryOp = struct { 346 | CallType: type, 347 | lhs: *Ast, 348 | op: [:0]const u8, 349 | rhs: *Ast, 350 | ReturnType: type, 351 | 352 | fn eval(comptime self: *const FnBinaryOp, args: anytype) self.ReturnType { 353 | const lhs = self.lhs.eval(args); 354 | const rhs = self.rhs.eval(args); 355 | 356 | return @field(self.CallType, self.op)(lhs, rhs); 357 | } 358 | }; 359 | 360 | const UnaryOp = struct { 361 | op: Op, 362 | rhs: *Ast, 363 | ReturnType: type, 364 | 365 | const Op = enum { 366 | negate, 367 | }; 368 | 369 | fn init(comptime alloc: *StupidAlloc, opToken: Token.Tag, rhs: *Ast) *Ast { 370 | var op = switch (opToken) { 371 | .minus => Op.negate, 372 | else => @compileError("Invalid unary operator"), 373 | }; 374 | var r = alloc.next(); 375 | r.* = Ast{ .unaryOp = UnaryOp{ 376 | .op = op, 377 | .rhs = rhs, 378 | .ReturnType = rhs.ReturnType(), 379 | } }; 380 | return r; 381 | } 382 | 383 | fn eval(comptime self: *const UnaryOp, args: anytype) self.ReturnType { 384 | return switch (self.op) { 385 | .negate => { 386 | return -self.rhs.eval(args); 387 | }, 388 | }; 389 | } 390 | }; 391 | 392 | // fn parseInt(comptime str: []const u8) comptime_int { 393 | // var r: comptime_int = 0; 394 | // // todo: non-base 10 integers? 395 | // for (str) |chr| { 396 | // switch (chr) { 397 | // '0' => r = r * 10, 398 | // '1' => r = r * 10 + 1, 399 | // '2' => r = r * 10 + 2, 400 | // '3' => r = r * 10 + 3, 401 | // '4' => r = r * 10 + 4, 402 | // '5' => r = r * 10 + 5, 403 | // '6' => r = r * 10 + 6, 404 | // '7' => r = r * 10 + 7, 405 | // '8' => r = r * 10 + 8, 406 | // '9' => r = r * 10 + 9, 407 | // else => @compileError("invalid integer"), 408 | // } 409 | // } 410 | // return r; 411 | // } 412 | 413 | // test "parse integer" { 414 | // try expectEqual(0, parseInt("0")); 415 | // try expectEqual(10, parseInt("010")); 416 | // try expectEqual(9876543210, parseInt("9876543210")); 417 | // } 418 | 419 | fn isBuiltinScalar(comptime v: type) bool { 420 | return switch (@typeInfo(v)) { 421 | .Int => true, 422 | .Float => true, 423 | .ComptimeFloat => true, 424 | .ComptimeInt => true, 425 | else => false, 426 | }; 427 | } 428 | 429 | // fn InvalidCombo(comptime a: type, comptime b: type, comptime op: []const u8) noreturn { 430 | // @compileError("Invalid combination of " ++ @typeName(a) ++ " and " ++ @typeName(b) ++ " for operation " ++ op ++ "."); 431 | // } 432 | 433 | // fn logStruct(s: anytype) void { 434 | // comptime var T = @TypeOf(s); 435 | // @compileLog(T); 436 | // @compileLog(s); 437 | // comptime var S = switch (@typeInfo(T)) { 438 | // .Struct => |v| v, 439 | // else => @compileError("log struct only takes structs."), 440 | // }; 441 | // inline for (S.fields) |field| { 442 | // @compileLog(field.name); 443 | // } 444 | // } 445 | 446 | const Token = struct { 447 | tag: Tag, 448 | start: usize, 449 | end: usize, 450 | 451 | const Tag = enum { 452 | invalid, 453 | eof, 454 | identifier, 455 | plus, 456 | minus, 457 | asterisk, 458 | slash, 459 | lParen, 460 | rParen, 461 | }; 462 | }; 463 | 464 | const Tokenizer = struct { 465 | buffer: [:0]const u8, 466 | index: usize, 467 | 468 | fn init(buffer: [:0]const u8) Tokenizer { 469 | return Tokenizer{ 470 | .buffer = buffer, 471 | .index = 0, 472 | }; 473 | } 474 | 475 | const State = enum { 476 | start, 477 | identifier, 478 | // plus, 479 | }; 480 | 481 | fn next(self: *Tokenizer) Token { 482 | var state = State.start; 483 | var r = Token{ 484 | .tag = .eof, 485 | .start = self.index, 486 | .end = undefined, 487 | }; 488 | 489 | outer: while (true) : (self.index += 1) { 490 | const c = if (self.index < self.buffer.len) self.buffer[self.index] else 0; 491 | switch (state) { 492 | .start => switch (c) { 493 | 0 => { 494 | break :outer; 495 | }, 496 | ' ' => { 497 | r.start = self.index + 1; 498 | }, 499 | 'a'...'z', 'A'...'Z', '_' => { 500 | state = .identifier; 501 | }, 502 | '+' => { 503 | r.tag = .plus; 504 | self.index += 1; 505 | break :outer; 506 | }, 507 | '-' => { 508 | r.tag = .minus; 509 | self.index += 1; 510 | break :outer; 511 | }, 512 | '*' => { 513 | r.tag = .asterisk; 514 | self.index += 1; 515 | break :outer; 516 | }, 517 | '/' => { 518 | r.tag = .slash; 519 | self.index += 1; 520 | break :outer; 521 | }, 522 | '(' => { 523 | r.tag = .lParen; 524 | self.index += 1; 525 | break :outer; 526 | }, 527 | ')' => { 528 | r.tag = .rParen; 529 | self.index += 1; 530 | break :outer; 531 | }, 532 | else => { 533 | r.tag = .invalid; 534 | break :outer; 535 | }, 536 | }, 537 | .identifier => switch (c) { 538 | 'a'...'z', 'A'...'Z', '_' => {}, 539 | else => { 540 | r.tag = .identifier; 541 | break :outer; 542 | }, 543 | }, 544 | } 545 | } 546 | r.end = self.index; 547 | return r; 548 | } 549 | 550 | fn source(self: *Tokenizer, token: Token) []const u8 { 551 | return self.buffer[token.start..token.end]; 552 | } 553 | }; 554 | --------------------------------------------------------------------------------