From da5f633459ef9b924126ecc515631d1c3fdd85e0 Mon Sep 17 00:00:00 2001 From: Clonkk Date: Sun, 24 Mar 2024 10:37:00 +0100 Subject: [PATCH] improved init_phase and use ordered table in code eval so the order of the files included matters --- changelog.md | 29 +++++++-- examples/ex01_helloworld.nim | 23 ++++--- examples/ex02_sqrt.nim | 19 +++--- examples/ex03_sugar.nim | 25 ++++---- examples/ex04_dict.nim | 54 ++++++++-------- examples/ex05_module.nim | 105 ++++++++++++++++---------------- examples/ex11_external_deps.nim | 23 ++++--- nimjl/cores.nim | 5 +- nimjl/glucose.nim | 69 ++++++++++++--------- 9 files changed, 202 insertions(+), 150 deletions(-) diff --git a/changelog.md b/changelog.md index 59ca81b..b1f9a97 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,22 @@ Changelog for Nimjl. Date in format YYYY_MM_DD +Release v0.8.1 - 2024_03_23 +=========================== +* Repo moved to SciNim +* Added activate option in init to start-up Julia in a virtual environment +* Added Hook function at init to execute code : BEFORE jl_init() is called; AFTER jl_init() and Pkg.activate() is called but BEFORE Pkg.add calls and BEFORE the embedded code is passed through ``eval`` function. +* Order of update is now relevant + +Release v0.8.0 - 2023_09_30 +=========================== +* Clean-up some code +* Added more type in evaluation of Dict and Tuples +* Improve loading a Package at init when the Package is already present (speed up init phase) + +Release v0.7.6 - 2023_02_22 +=========================== +* Small CI change + Release v0.7.5 - 2022_07_07 =========================== * Fixed https://github.com/Clonkk/nimjl/issues/18 @@ -34,15 +51,15 @@ Release v0.7.1 - 2022_01_04 Release v0.7.0 - 2022_01_04 =========================== * Add Julia.useModule alias for jlUseModule -* Add Julia.includeFile (include is reserved keyword) alias for jlInclude +* Add Julia.includeFile (include is reserved keyword) alias for jlInclude * Add mechanism to embed julia files at compile-time and run the code at init for an easy way to distribute binary with Julia code contained * Add Pkg template to easily install new Julia package during init ; it is also compatible with the embedding stuff : * See ex09 ```nim - Julia.init: - Pkg: + Julia.init: + Pkg: add("LinearAlgebra") - Embed: + Embed: file("myfile.jl") ``` @@ -60,7 +77,7 @@ Release v0.6.1 - 2021_10_14 Release v0.6.0 - 2021_10_08 =========================== -* Add --gc:orc to CI +* Add --gc:orc to CI Release v0.5.9 - 2021_10_05 =========================== @@ -157,4 +174,4 @@ Release v0.4.0 - 2021_03_08 * Add ``toJlVal`` / ``to`` proc to make conversion betwen Julia types and Nim types "smooth" * Added Julia exception handler from Nim * Examples in the examples folder -* Tess suite and memory leak suite +* Test suite and memory leak suite diff --git a/examples/ex01_helloworld.nim b/examples/ex01_helloworld.nim index ec2122f..b7e09ed 100644 --- a/examples/ex01_helloworld.nim +++ b/examples/ex01_helloworld.nim @@ -1,15 +1,18 @@ import nimjl -Julia.init() # Initialize Julia VM. This should be done once in the lifetime of your program. +proc main() = + Julia.init() # Initialize Julia VM. This should be done once in the lifetime of your program. -# Calling Julia function from Nim will always return a JlValue -# This JlValue can be "nothing" -# Therefore, Julia function who do not return a value can be discarded -var res = Julia.println("Hello world") -echo res # nothing -# Check that res is actually nothing -if res == JlNothing: - echo "Julia.println returned nothing" + # Calling Julia function from Nim will always return a JlValue + # This JlValue can be "nothing" + # Therefore, Julia function who do not return a value can be discarded + var res = Julia.println("Hello world") + echo res # nothing + # Check that res is actually nothing + if res == JlNothing: + echo "Julia.println returned nothing" -discard Julia.println("This also works") + discard Julia.println("This also works") +when isMainModule: + main() diff --git a/examples/ex02_sqrt.nim b/examples/ex02_sqrt.nim index 43e0097..be7305b 100644 --- a/examples/ex02_sqrt.nim +++ b/examples/ex02_sqrt.nim @@ -1,13 +1,16 @@ import nimjl import std/math -jlVmInit() # Initialize Julia VM. This should be done once in the lifetime of your program. +proc main() = + jlVmInit() # Initialize Julia VM. This should be done once in the lifetime of your program. -var myval = 4.0'f64 -# Call Julia function "sqrt" and convert the result to a float -# This syntax also works to call a function directly from a Julia modfule -var res = JlBase.sqrt(myval).to(float64) -# Echo on JlValue calls println from Julia -echo res # 2.0 -doAssert res == sqrt(myval) + var myval = 4.0'f64 + # Call Julia function "sqrt" and convert the result to a float + # This syntax also works to call a function directly from a Julia modfule + var res = JlBase.sqrt(myval).to(float64) + # Echo on JlValue calls println from Julia + echo res # 2.0 + doAssert res == sqrt(myval) +when isMainModule: + main() diff --git a/examples/ex03_sugar.nim b/examples/ex03_sugar.nim index 878ea10..a474c5c 100644 --- a/examples/ex03_sugar.nim +++ b/examples/ex03_sugar.nim @@ -1,17 +1,20 @@ import nimjl import std/sequtils -Julia.init() -# Use Module handle -discard JlMain.println(@[1, 2, 3]) -discard Julia.println(toSeq(0..5)) +proc main = + Julia.init() + # Use Module handle + discard JlMain.println(@[1, 2, 3]) + discard Julia.println(toSeq(0..5)) -let arr = [1.0, 2.0, 3.0].toJlArray() -# You can now use echo to call println for you on Julia type ! -echo jltypeof(arr) -echo arr + let arr = [1.0, 2.0, 3.0].toJlArray() + # You can now use echo to call println for you on Julia type ! + echo jltypeof(arr) + echo arr -# You can also call proc from the value directly -echo arr.stride(1) -echo arr.strides() + # You can also call proc from the value directly + echo arr.stride(1) + echo arr.strides() +when isMainModule: + main() diff --git a/examples/ex04_dict.nim b/examples/ex04_dict.nim index b91072b..7dce15a 100644 --- a/examples/ex04_dict.nim +++ b/examples/ex04_dict.nim @@ -12,29 +12,31 @@ proc popMe[U, V](tab: var JlValue, key: U): V = # Therefore "pop!" function will not modify tab result = jlCall("pop!", tab, key).to(V) - -jlVmInit() # Initialize Julia VM. This should be done once in the lifetime of your program. - -var mytab: Table[int64, float64] = {1'i64: 0.90'f64, 2'i64: 0.80'f64, 3'i64: 0.70'f64}.toTable -block: - let key = 1 - var poppedValue = popMe(mytab, key) - # mytab has not been changed due to copy when passing value to Julia - doAssert key in mytab - doAssert poppedValue == mytab[key] - -block: - var myJlTab = toJlVal(mytab) # Convert myJlTab to JlValue - let key = 1 - var poppedValue = popMe[int64, float64](myJlTab, key) - doAssert not myJlTab.to(Table[int64, float64]).contains(key) # Value was removed from myJlTab - doAssert poppedValue == mytab[key] - -block: - # You can use [] on Julia dict as well - var jldict = toJlVal({"alpha": 1.1, "beta": 2.2}.toTable) - doAssert jldict["alpha"].to(float) == 1.1 - doAssert jldict["beta"].to(float) == 2.2 - jldict["alpha"] = 3.3 - doAssert jldict["alpha"].to(float) == 3.3 - +proc main() = + jlVmInit() # Initialize Julia VM. This should be done once in the lifetime of your program. + + var mytab: Table[int64, float64] = {1'i64: 0.90'f64, 2'i64: 0.80'f64, 3'i64: 0.70'f64}.toTable + block: + let key = 1 + var poppedValue = popMe(mytab, key) + # mytab has not been changed due to copy when passing value to Julia + doAssert key in mytab + doAssert poppedValue == mytab[key] + + block: + var myJlTab = toJlVal(mytab) # Convert myJlTab to JlValue + let key = 1 + var poppedValue = popMe[int64, float64](myJlTab, key) + doAssert not myJlTab.to(Table[int64, float64]).contains(key) # Value was removed from myJlTab + doAssert poppedValue == mytab[key] + + block: + # You can use [] on Julia dict as well + var jldict = toJlVal({"alpha": 1.1, "beta": 2.2}.toTable) + doAssert jldict["alpha"].to(float) == 1.1 + doAssert jldict["beta"].to(float) == 2.2 + jldict["alpha"] = 3.3 + doAssert jldict["alpha"].to(float) == 3.3 + +when isMainModule: + main() diff --git a/examples/ex05_module.nim b/examples/ex05_module.nim index f912b2f..968b5a3 100644 --- a/examples/ex05_module.nim +++ b/examples/ex05_module.nim @@ -7,63 +7,66 @@ type y: float z: string -Julia.init() # Initialize Julia VM. This should be done once in the lifetime of your program. -# Include Julia file -jlInclude("localmodule.jl") -# Use the module. If you're confused by the syntax, go and read through Julia's Manual where module usage is explained -jlUseModule(".nimjlExample") +proc main() = + Julia.init() # Initialize Julia VM. This should be done once in the lifetime of your program. + # Include Julia file + jlInclude("localmodule.jl") + # Use the module. If you're confused by the syntax, go and read through Julia's Manual where module usage is explained + jlUseModule(".nimjlExample") -block: # Tuple handling - # Look, you can pass Nim tuple to Julia - var mytup: MyTuple = (nimTupKey1: 1, nimTupKey2: 2) - # Convert Nim tuple to Julia tuple automatically - var res = Julia.customFunction(mytup) - # Convert Julia tuple to Nim tuple - var nimres = res.to(MyTuple) + block: # Tuple handling + # Look, you can pass Nim tuple to Julia + var mytup: MyTuple = (nimTupKey1: 1, nimTupKey2: 2) + # Convert Nim tuple to Julia tuple automatically + var res = Julia.customFunction(mytup) + # Convert Julia tuple to Nim tuple + var nimres = res.to(MyTuple) - echo myTup - echo nimres + echo myTup + echo nimres - doAssert myTup.nimTupKey1+1 == nimres.nimTupKey1 - doAssert myTup.nimTupKey2+1 == nimres.nimTupKey2 + doAssert myTup.nimTupKey1+1 == nimres.nimTupKey1 + doAssert myTup.nimTupKey2+1 == nimres.nimTupKey2 -block: # Object manipulation - # Call constructor - var foo = Julia.makeFoo() - # Access fields with dot syntax - # Calls getproperty in Julia side - echo foo.x - echo foo.y - echo foo.z - # Modify fields with .= syntax - # Calls setproperty! on Julia side - foo.x = 2 - foo.y = 3.14 - foo.z = "General Kenobi !" - # Yay this has been modified - echo foo + block: # Object manipulation + # Call constructor + var foo = Julia.makeFoo() + # Access fields with dot syntax + # Calls getproperty in Julia side + echo foo.x + echo foo.y + echo foo.z + # Modify fields with .= syntax + # Calls setproperty! on Julia side + foo.x = 2 + foo.y = 3.14 + foo.z = "General Kenobi !" + # Yay this has been modified + echo foo - # You can use dot syntax on Julia value to call proc as well - discard foo.applyToFoo() - echo foo + # You can use dot syntax on Julia value to call proc as well + discard foo.applyToFoo() + echo foo -block: - var foo = Foo(x: 12, y: 15, z: "This string comes from Nim") - # You can convert Nim to Julia object if : - # * fields have the same name and type (fieldName becomdes Julia symbol) - # * The Julia type have an empty constructor -- Nim needs to initialize the Julia variable before calling setproperty! providing a default empty constructor is the easiest way of doing it - var jlfoo = toJlVal(foo) - echo jlfoo - echo jltypeof(jlfoo) # This echo "Foo" -> Julia sees this as a Foo mutable struct type + block: + var foo = Foo(x: 12, y: 15, z: "This string comes from Nim") + # You can convert Nim to Julia object if : + # * fields have the same name and type (fieldName becomdes Julia symbol) + # * The Julia type have an empty constructor -- Nim needs to initialize the Julia variable before calling setproperty! providing a default empty constructor is the easiest way of doing it + var jlfoo = toJlVal(foo) + echo jlfoo + echo jltypeof(jlfoo) # This echo "Foo" -> Julia sees this as a Foo mutable struct type - discard jlfoo.applyToFoo() - # Object are copid during conversions so modifying jlfoo does not modify foo - # There is an exception to this for Array fields -- see ex_arrays for explanation - echo jlfoo - echo foo + discard jlfoo.applyToFoo() + # Object are copid during conversions so modifying jlfoo does not modify foo + # There is an exception to this for Array fields -- see ex_arrays for explanation + echo jlfoo + echo foo -block: - let res = Julia.returnDoubleValue() - echo res.myint - echo res.mystr + block: + let res = Julia.returnDoubleValue() + echo res.myint + echo res.mystr +when isMainModule: + main() diff --git a/examples/ex11_external_deps.nim b/examples/ex11_external_deps.nim index 5b746b1..4d32b82 100644 --- a/examples/ex11_external_deps.nim +++ b/examples/ex11_external_deps.nim @@ -1,9 +1,18 @@ import nimjl -## See https://pkgdocs.julialang.org/dev/api/#Pkg.add for more info -Julia.init(1): - Pkg: - add(name="Polynomials", version="3.0.0") - add(name="LinearAlgebra") - add("DSP") - add(name="Wavelets", version="0.9.4") +proc main() = + ## See https://pkgdocs.julialang.org/dev/api/#Pkg.add for more info + Julia.init(1): + Pkg: + add(name="Polynomials", version="3.0.0") + add(name="LinearAlgebra") + add("DSP") + + Julia.useModule("Pkg") + let jlpkg = Julia.getModule("Pkg") + discard jlpkg.status() + + Julia.exit() + +when isMainModule: + main() diff --git a/nimjl/cores.nim b/nimjl/cores.nim index f9194de..5b55f6b 100644 --- a/nimjl/cores.nim +++ b/nimjl/cores.nim @@ -82,13 +82,14 @@ proc jlVmExit*(exit_code: cint = 0.cint) = # discard jlEval(fmt"exit_save_sysimage({fpath})") ######################################### -var staticContents: Table[string, string] +var staticContents: OrderedTable[string, string] import std/logging proc loadJlRessources*() = for key, content in staticContents.pairs(): - info("> Nimjl loading Julia ressource: ", key, ".jl") + info("> Nimjl loading Julia ressource: ", key) + # debugEcho("> Nimjl loading Julia ressource: ", key) JlCode(content) # Init & Exit function diff --git a/nimjl/glucose.nim b/nimjl/glucose.nim index a834377..9093347 100644 --- a/nimjl/glucose.nim +++ b/nimjl/glucose.nim @@ -1,6 +1,6 @@ # This file is named glucose because it gives you sugar ;) # It contains most syntactic sugar to ease using Julia inside Nim -import std/[os, strutils, strformat, tables] +import std/[os, strutils, strformat, tables, paths] import ./types import ./cores import ./functions @@ -122,7 +122,9 @@ template add*(name: static string, url: static string = "", path: static string template init*(jl: type Julia, nthreads: int, body: untyped) = ## Init Julia VM - var packages :JlPkgs + var packages: JlPkgs + var pkgEnv {.inject.} : string = "" + template Pkg(innerbody: untyped) {.used.} = block: # Technically accessible but since the type are not exported, what are you going to do with it ? @@ -131,6 +133,9 @@ template init*(jl: type Julia, nthreads: int, body: untyped) = innerbody packages = jl_pkg_private_scope + template activate(env: string) {.used.} = + pkgEnv = string(expandTilde(Path(env))) + template Embed(innerbody: untyped) {.used.} = ## Emded Julia file explicitly of from a directory template file(filename: static[string]) = @@ -156,36 +161,42 @@ template init*(jl: type Julia, nthreads: int, body: untyped) = jl_init() # Module installation Julia.useModule("Pkg") - - let - jlExistingPkgStr = "Dict(x[2].name => string(x[2].version) for x in Pkg.dependencies())" - jlPkgsExisting = jlEval(jlExistingPkgStr) - installed = jlPkgsExisting.to(Table[string, string]) - - for pkgspec in packages: - if not checkJlPkgSpec(installed, pkgspec): - var exprs: seq[string] = @[jlExpr(":.", ":Pkg", "QuoteNode(:add)")] - for key, field in pkgspec.fieldPairs(): - let fname = ":" & key - if not isEmptyOrWhitespace(field): - exprs.add jlExpr(":kw", fname, field) - - let strexpr = jlExpr(":call", exprs) - var jlexpr = jlEval(strexpr) - # Will crash if version are invalid - discard jlTopLevelEval(jlexpr) - - for pkgspec in packages: - # TODO : handle precompilation ? - # Julia.precompile() - jlUsing(pkgspec.name) - - # Eval Julia code embedded - loadJlRessources() - else: raise newException(JlError, "Error Julia.init() has already been initialized") + if not pkgEnv.isEmptyOrWhitespace(): + debugEcho(&"\"Pkg.activate(\"{pkgEnv}\")\"") + discard jlEval(&"Pkg.activate(\"{pkgEnv}\")") + + when compiles(postJlInit()): + postJlInit() + + let + jlExistingPkgStr = "Dict(x[2].name => string(x[2].version) for x in Pkg.dependencies())" + jlPkgsExisting = jlEval(jlExistingPkgStr) + installed = jlPkgsExisting.to(Table[string, string]) + + for pkgspec in packages: + if not checkJlPkgSpec(installed, pkgspec): + var exprs: seq[string] = @[jlExpr(":.", ":Pkg", "QuoteNode(:add)")] + for key, field in pkgspec.fieldPairs(): + let fname = ":" & key + if not isEmptyOrWhitespace(field): + exprs.add jlExpr(":kw", fname, field) + + let strexpr = jlExpr(":call", exprs) + var jlexpr = jlEval(strexpr) + # Will crash if version are invalid + discard jlTopLevelEval(jlexpr) + + for pkgspec in packages: + # TODO : handle precompilation ? + # Julia.precompile() + jlUsing(pkgspec.name) + + # Eval Julia code embedded + loadJlRessources() + proc exit*(jl: type Julia, exitcode: int = 0) = ## Exit Julia VM jlVmExit(exitcode.cint)