Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support keyword arguments in CallJuliaFunctionWithCatch, and a few other improvements #1043

Merged
merged 3 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 48 additions & 9 deletions pkg/JuliaInterface/gap/JuliaInterface.gd
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,24 @@ DeclareGlobalFunction( "JuliaImportPackage" );


#! @Section Access to &Julia; objects
#! Not all &Julia; syntax features are supported in ⪆.
#! For important ones, the interface provides ⪆ functions or
#! helper functions written in &Julia; to use them in ⪆.
#! For example, <Ref Func="CallJuliaFunctionWithCatch"/> allows one to use
#! &Julia;'s try/catch statements.
#!
#! Here is a selection of other workarounds for &Julia; syntax features.
#! <List>
#! <Item>
#! &Julia;'s <C>RefValue</C> objects can be handled as follows.
#! If <C>x</C> is such an object then its value can be fetched with
#! <C>Julia.GAP.getindex( x )</C>,
#! a value <C>v</C> of the right type can be set with
#! <C>Julia.GAP.setindex( x, v )</C>,
#! and one can check with <C>Julia.GAP.isassigned( x )</C>
#! whether <C>x</C> has a value.
#! </Item>
#! </List>

## Internal
BindGlobal( "_JuliaFunctions", rec( ) );
Expand Down Expand Up @@ -299,6 +317,19 @@ DeclareGlobalFunction( "JuliaFunction" );
#! gap> Julia.Main.x;
#! 1
#! @EndExampleSession
#!
#! Note that not all &Julia; variables are directly visible in its
#! <C>Main</C> module.
#! For example, &Julia; variables from the interface to &GAP; are defined
#! in the &Julia; module <C>GAP</C> or its submodules.
#! It is safe to access this module as <C>Julia.GAP</C>.
#!
#! @BeginExampleSession
#! gap> Julia.GAP;
#! <Julia module GAP>
#! gap> Julia.GAP.prompt;
#! <Julia: prompt>
#! @EndExampleSession
DeclareGlobalVariable( "Julia" );

#! @Arguments name
Expand Down Expand Up @@ -331,11 +362,13 @@ DeclareGlobalFunction( "JuliaModule" );
#! @EndExampleSession
DeclareGlobalFunction( "JuliaTypeInfo" );

#! @Arguments juliafunc, arguments
#! @Arguments juliafunc, arguments[, arec]
#! @Returns a record.
#! @Description
#! The function calls the &Julia; function <A>juliafunc</A>
#! with arguments in the &GAP; list <A>arguments</A>,
#! with ordinary arguments in the &GAP; list <A>arguments</A>
#! and optionally with keyword arguments given by the component names (keys)
#! and values of the &GAP; record <A>arec</A>,
#! and returns a record with the components <C>ok</C> and <C>value</C>.
#! If no error occurred then <C>ok</C> has the value <K>true</K>,
#! and <C>value</C> is the value returned by <A>juliafunc</A>.
Expand All @@ -358,6 +391,15 @@ DeclareGlobalFunction( "JuliaTypeInfo" );
#! false
#! gap> res.value{ [ 1 .. Position( res.value, '(' )-1 ] };
#! "LinearAlgebra.SingularException"
#! gap> fun:= Julia.range;;
#! gap> CallJuliaFunctionWithCatch( fun, [ 2, 10 ], rec( step:= 2 ) );
#! rec( ok := true, value := <Julia: 2:2:10> )
#! gap> res:= CallJuliaFunctionWithCatch( fun, [ 2, 10 ],
#! > rec( step:= GAPToJulia( "a" ) ) );;
#! gap> res.ok;
#! false
#! gap> res.value{ [ 1 .. Position( res.value, '(' )-1 ] };
#! "MethodError"
#! @EndExampleSession
DeclareGlobalFunction( "CallJuliaFunctionWithCatch" );

Expand Down Expand Up @@ -461,19 +503,17 @@ DeclareGlobalFunction( "CallJuliaFunctionWithKeywordArguments" );
#! <List>
#! <Item>
#! <Ref Oper="CallFuncList" BookName="ref"/>,
#! delegating to <C>Julia.Core._apply</C>
#! (this yields the function call syntax in &GAP;,
#! delegating to &Julia;'s <C>func(args...)</C> syntax;
#! this yields the function call syntax in &GAP;,
#! it is installed also for objects in
#! <Ref Filt="IsJuliaWrapper" Label="for IsObject"/>,
#! </Item>
#! <Item>
#! access to and assignment of entries of arrays, via
#! <Ref Oper="\[\]" BookName="ref"/>,
#! <Ref Oper="\[\]\:\=" BookName="ref"/>,
#! <!-- <Ref Oper="MatElm" BookName="ref"/>, and
#! <Ref Oper="SetMatElm" BookName="ref"/>, -->
#! and the (up to &GAP; 4.11 undocumented) operations <C>MatElm</C> and
#! <C>SetMatElm</C>,
#! <Ref Oper="MatElm" BookName="ref"/>, and
#! <Ref Oper="SetMatElm" BookName="ref"/>,
#! delegating to
#! <C>Julia.Base.getindex</C> and
#! <C>Julia.Base.setindex</C>,
Expand Down Expand Up @@ -532,7 +572,6 @@ DeclareGlobalFunction( "CallJuliaFunctionWithKeywordArguments" );
#! gap> m + m;
#! <Julia: [2 4; 6 8]>
#! @EndExampleSession
#TODO: add the cross-references to MatElm, SetMatElm when they are documented

#! @InsertChunk JuliaHelpInGAP

Expand Down
13 changes: 10 additions & 3 deletions pkg/JuliaInterface/gap/calls.gi
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ InstallMethod( CallFuncList,
[ "IsJuliaObject", "IsList" ],
function( julia_obj, args )
args := GAPToJulia( _JL_Vector_Any, args, false );
return Julia.Core._apply( julia_obj, args );
return Julia.GAP._apply( julia_obj, args );
end );

InstallMethod( CallFuncList,
Expand All @@ -22,14 +22,21 @@ InstallMethod( CallFuncList,
end );

InstallGlobalFunction( CallJuliaFunctionWithCatch,
function( julia_obj, args )
function( julia_obj, args, arec... )
local res;

args := GAPToJulia( _JL_Vector_Any, args, false );
if IsFunction( julia_obj ) then
julia_obj:= Julia.GAP.UnwrapJuliaFunc( julia_obj );
fi;
res:= Julia.GAP.call_with_catch( julia_obj, args );
if Length( arec ) = 0 then
res:= Julia.GAP.call_with_catch( julia_obj, args );
elif Length( arec ) = 1 and IsRecord( arec[1] ) then
arec := GAPToJulia( _JL_Dict_Any, arec[1], false );
res:= Julia.GAP.call_with_catch( julia_obj, args, arec );
else
Error( "usage: CallJuliaFunctionWithCatch( <julia_obj>, <args>, <arec>" );
fi;
if res[1] then
return rec( ok:= true, value:= res[2] );
else
Expand Down
2 changes: 2 additions & 0 deletions pkg/JuliaInterface/tst/utils.tst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ gap> res.ok;
false
gap> StartsWith( res.value, "DomainError" );
true
gap> CallJuliaFunctionWithCatch( Julia.Base.sqrt, [ 4 ], rec() );
rec( ok := true, value := <Julia: 2.0> )

##
gap> JuliaEvalString(fail);
Expand Down
37 changes: 12 additions & 25 deletions src/packages.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,12 @@ const DEFAULT_PKGDIR = Ref{String}()
const DOWNLOAD_HELPER = Ref{Downloads.Downloader}()

function init_packagemanager()
#TODO:
# As soon as PackageManager uses utils' Download function,
# we need not replace code from PackageManager anymore.
# (And the function should be renamed.)
res = load("PackageManager")
@assert res

global DEFAULT_PKGDIR[] = sysinfo["DEFAULT_PKGDIR"]
# This is used by `PKGMAN_DownloadURL` in GAP's PackageManager package.
global DOWNLOAD_HELPER[] = Downloads.Downloader(; grace=0.1)

# overwrite PKGMAN_DownloadURL
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you verify that PackageManager still uses Julia's download feature? I am asking because the weird error about "could not import Downloads" suggests to me that perhaps JuliaImportPackage("Downloads") returns false and so the PackageManager code is not being loaded...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this was also my understanding:
Whenever the warning occurs, the overwrite inside PackageManager does not happen, but up to now, we were anyhow overwriting the function afterwards, thus the code in PackageManager was actually never used.

With gap-packages/PackageManager/pull/134, the problem with the warning should disappear. It depends on the version of PackageManager used whether we can safely remove the code from GAP.jl.

replace_global!(:PKGMAN_DownloadURL, function(url)
try
buffer = Downloads.download(String(url), IOBuffer(), downloader=DOWNLOAD_HELPER[])
return GapObj(Dict{Symbol, Any}(:success => true, :result => String(take!(buffer))), recursive=true)
catch
return GapObj(Dict{Symbol, Any}(:success => false), recursive=true)
end
end)
# We need functions from PackageManager.
res = load("PackageManager")
@assert res

# Install a method (based on Julia's Downloads package) as the first choice
# for the `Download` function from GAP's utils package,
Expand Down Expand Up @@ -59,15 +46,15 @@ function init_packagemanager()
# put the new method in the first position
meths = Globals.Download_Methods
Wrappers.Add(meths, GapObj(r, recursive=true), 1)

# monkey patch PackageManager so that we can disable removal of
# package directories for debugging purposes
orig_PKGMAN_RemoveDir = Globals.PKGMAN_RemoveDir
replace_global!(:PKGMAN_RemoveDir, function(dir)
Globals.ValueOption(GapObj("debug")) == true && return
orig_PKGMAN_RemoveDir(dir)
end)
end

# monkey patch PackageManager so that we can disable removal of
# package directories for debugging purposes
orig_PKGMAN_RemoveDir = Globals.PKGMAN_RemoveDir
replace_global!(:PKGMAN_RemoveDir, function(dir)
Globals.ValueOption(GapObj("debug")) == true && return
orig_PKGMAN_RemoveDir(dir)
end)
end

"""
Expand Down
42 changes: 33 additions & 9 deletions src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ function _setglobal(M::Module, name::Symbol, val::Any)
end
end

# avoid the deprecated `Core._apply`
_apply(func, args) = func(args...)

"""
get_symbols_in_module(m::Module) :: Vector{Symbol}

Expand All @@ -30,32 +33,55 @@ function get_symbols_in_module(m::Module)
end

"""
call_with_catch(juliafunc, arguments)
call_with_catch(func, args::Vector)
call_with_catch(func, args::Vector, kwargs::Dict{Symbol,T}) where T

Return a tuple `(ok, val)`
where `ok` is either `true`, meaning that calling the function `juliafunc`
with `arguments` returns the value `val`,
where `ok` is either `true`, meaning that calling `func`
with arguments `args` (and optionally with keyword arguments given by
the keys and values of `kwargs`) returns the value `val`,
or `false`, meaning that the function call runs into an error;
in the latter case, `val` is set to the string of the error message.

This function is used on the GAP side.

# Examples
```jldoctest
julia> GAP.call_with_catch(sqrt, 2)
julia> GAP.call_with_catch(sqrt, [2])
(true, 1.4142135623730951)

julia> flag, res = GAP.call_with_catch(sqrt, -2);
julia> flag, res = GAP.call_with_catch(sqrt, [-2]);

julia> flag
false

julia> startswith(res, "DomainError")
true

julia> GAP.call_with_catch(range, [2, 10], Dict(:step => 2))
(true, 2:2:10)

julia> flag, res = GAP.call_with_catch(range, [2, 10], Dict(:step => "a"));

julia> flag
false

julia> startswith(res, "MethodError")
true
```
"""
function call_with_catch(juliafunc, arguments)
function call_with_catch(func, args)
try
res = Core._apply(juliafunc, arguments)
res = func(args...)
return (true, res)
catch e
return (false, string(e))
end
end

function call_with_catch(func, args::Vector, kwargs::Dict{Symbol,T}) where T
try
res = func(args...; [k => kwargs[k] for k in keys(kwargs)]...)
return (true, res)
catch e
return (false, string(e))
Expand All @@ -70,7 +96,6 @@ given by the keys and values of `kwargs`.

This function is used on the GAP side, in calls of Julia functions that
require keyword arguments.
Note that `jl_call` and `Core._apply` do not support keyword arguments.

# Examples
```jldoctest
Expand All @@ -79,7 +104,6 @@ julia> range(2, length = 5, step = 2)

julia> GAP.kwarg_wrapper(range, [2], Dict(:length => 5, :step => 2))
2:2:10

```
"""
function kwarg_wrapper(func, args::Vector{T1}, kwargs::Dict{Symbol,T2}) where {T1,T2}
Expand Down
Loading