Hi,
I am considering Nim as programming language for its metaprogramming capabilities. As a test case for code generation, I would like to convert matrix expressions like A[i,j] to Avec[3], given` i = 1`,` j = 2`.
In Matlab this could be done using string manipulations, in Julia on the AST using the Expr type. The expressions then arebe manipulated like nested structs, deleting, adding or modifying elements.
Some helpful conversion in Julia include:
A = rand(2,2)
i = 1, j = 2
e = :(A[i, j]) # :(A[i, j]) quoting an expression -> Expr
dump(e) # Expr -> AST (for humans)
#=
Expr
head: Symbol ref
args: Array{Any}((3,))
1: Symbol A
2: Symbol i
3: Symbol j
=#
symbs = (e.head, e.args...) # (:ref, :A, :i, :j) Expr -> (symbols...)
Expr(:ref, :A, :i, :j) # :(A[i, j]) (symbols...) -> Expr
string(:(A[i, j])) # "A[i, j]" Expr -> String
Meta.parse("A[i, j]") # :(A[i, j]) String -> Expr
eval(:(A[i, j])) # a random number
I understand that Nim is a bit more verbose here. What is the best approach?I don't think a macro is necessary in your case, a proc can work just fine:
proc `[]`*[T](m: Matrix[T], i, j: int): T {.inline.} =
result = m.data[i * m.n + j]
Thanks for replying. I am an absolute beginner in Nim, so please bear with me. Looks like an ordinary numerical function, where is the code generation?
As use case, an algorithm for x=Ab (like gaussian elimination) and sparsity pattern for A and b, is converted to an array of simple expressions acting on a vector Avec. Thus, avoiding zero-ops. The array of expressions is evaluated many times with different values for Avec and b, hence amortizing the time for generation.
- // loading A
- A[1] = gn_C1 + 1/RC1;
- A[2] = - gn_C1 - 1/RC1; A[3] = -1; A[4] = - gn_C1 - 1/RC1; A[5] = gn_C1 + gn_C2n + 1/RC1 + 1/RC2N + 1/Rshort_antp_antn; A[6] = -1/Rshort_antp_antn; A[7] = - gn_C2n - 1/RC2N; A[8] = -1/Rshort_antp_antn; A[9] = gn_C2p + 1/RC2P + 1/Rshort_antp_antn; A[10] = - gn_C2p - 1/RC2P; A[11] = 1; A[12] = 1/Rnormp; A[13] = -1/Rnormp; A[14] = 1; A[15] = 1/Rnormn; A[16] = -1/Rnormn; A[17] = 1; A[18] = -1/Rnormp; A[19] = 1/RantP + 1/Rnormp + 1/Rshort; A[20] = -1/Rshort; A[21] = -1/RantP; A[22] = -1/Rnormn; A[23] = -1/Rshort; A[24] = 1/RantN + 1/Rnormn + 1/Rshort; A[25] = -1/RantN; A[26] = - gn_C2p - 1/RC2P; A[27] = -1/RantP; A[28] = gn_C2p + 1/RC2P + 1/RantP; A[29] = - gn_C2n - 1/RC2N; A[30] = -1/RantN; A[31] = gn_C2n + 1/RC2N + 1/RantN; A[32] = -1; A[33] = 1; A[34] = -rt_Lant; A[35] = 1; A[36] = 1;
From what I understand you are asking for the same thing as the code given does. This might be wrong though. The point of the given code is that in Nim, a[b, c] is a valid expression that parses to a call to a routine named [] with the arguments a, b, c. Here is how the given code is used in case it is what you're asking for:
type Matrix[T] = object
rows: int
data: seq[T]
proc `[]`*[T](m: Matrix[T], i, j: int): T {.inline.} =
return m.data[i * m.rows + j]
let matrix = Matrix[int](rows: 2, data: @[
1, 2,
3, 4])
assert matrix[0, 0] == 1
assert matrix[0, 1] == 2
assert matrix[1, 0] == 3
If you want AST substitution at compile time instead of an inline proc (which is not always a good idea), you can use template or macro. template s are analogous to quote macros in other languages while macro s are like regular procs that generate AST.
template `[]`*[T](m: Matrix[T], i, j: int): T =
m.data[i * m.rows + j]
# macro version
import macros
macro `[]`*[T](m: Matrix[T], i, j: int): T =
# quote version
result = quote do:
`m`.data[`i` * `m`.rows + `j`]
# procedural version
result = newTree(nnkBracketExpr,
newDotExpr(m, ident("data")),
infix(
infix(i, "*", newDotExpr(m, ident("rows"))),
"+",
j
)
)
This is a very nice use case, I made an example for you
import std/macros
type
Matrix[M, N: static[int]] = array[M * N, float32]
macro transposeImpl(M, N: static[int]; x, res: typed): untyped =
result = newNimNode(nnkStmtList)
for n in 0 ..< N * M:
let i = n div N
let j = n mod N
result.add nnkAsgn.newTree(nnkBracketExpr.newTree(res, newLit(n)), nnkBracketExpr.newTree(x, newLit(j * M + i)))
echo result.repr
proc transpose*[M, N: static[int]](x: Matrix[M, N]): Matrix[N, M] {.inline.} =
transposeImpl(M, N, x, result)
#proc transpose*[M, N: static[int]](x: Matrix[M, N]): Matrix[N, M] {.inline.} =
#for n in 0 ..< N * M:
#let i = n div N
#let j = n mod N
#result[n] = x[j * M + i]
when isMainModule:
let
a: Matrix[2, 3] = [1'f32, 2, 3, 4, 5, 6]
b = transpose(a)
echo b
As you can see it generates the following code:
result[0] = x[0]
result[1] = x[2]
result[2] = x[4]
result[3] = x[1]
result[4] = x[3]
result[5] = x[5]
with fusion astdsl, it becomes a little simpler to write (not by much, for this example though):
import fusion/astdsl
macro transposeImpl(M, N: static[int]; x, res: typed): untyped =
result = buildAst(stmtList):
#let typeSym = getTypeInst(res)
#let typeNode = getTypeImpl(typeSym)
#expectKind(typeNode[1], nnkInfix)
for i in 0 ..< M:
for j in 0 ..< N:
asgn(bracketExpr(res, newLit(i * N + j)),
bracketExpr(x, newLit(j * M + i)))
echo result.repr
A big THANK YOU to all of you guys!
Your replies give a lot of things to consider: quote and procedural version, even a dsl package.
Well worth to have a closer look at Nim.