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 all commits
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
63 changes: 51 additions & 12 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[, kwargs]
#! @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>kwargs</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,20 +391,29 @@ 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" );

#! @Arguments juliafunc, arguments, arec
#! @Arguments juliafunc, arguments, kwargs
#! @Returns the result of the &Julia; function call.
#! @Description
#! The function calls the &Julia; function <A>juliafunc</A>
#! with ordinary arguments in the &GAP; list <A>arguments</A>
#! and keyword arguments given by the component names (keys) and values
#! of the record <A>arec</A>,
#! of the record <A>kwargs</A>,
#! and returns the function value.
#!
#! Note that the entries of <A>arguments</A> and the components of
#! <A>arec</A> are not implicitly converted to &Julia;.
#! <A>kwargs</A> are not implicitly converted to &Julia;.
#! @BeginExampleSession
#! gap> CallJuliaFunctionWithKeywordArguments( Julia.Base.round,
#! > [ GAPToJulia( Float( 1/3 ) ) ], rec( digits:= 5 ) );
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
17 changes: 12 additions & 5 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, kwargs... )
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( kwargs ) = 0 then
res:= Julia.GAP.call_with_catch( julia_obj, args );
elif Length( kwargs ) = 1 and IsRecord( kwargs[1] ) then
kwargs := GAPToJulia( _JL_Dict_Any, kwargs[1], false );
res:= Julia.GAP.call_with_catch( julia_obj, args, kwargs );
else
Error( "usage: CallJuliaFunctionWithCatch( <julia_obj>, <args>[, <kwargs>]" );
fi;
if res[1] then
return rec( ok:= true, value:= res[2] );
else
Expand All @@ -38,7 +45,7 @@ InstallGlobalFunction( CallJuliaFunctionWithCatch,
end );

InstallGlobalFunction( CallJuliaFunctionWithKeywordArguments,
{ julia_obj, args, arec } -> Julia.GAP.kwarg_wrapper( julia_obj,
{ julia_obj, args, kwargs } -> Julia.GAP.kwarg_wrapper( julia_obj,
# non-recursive conversions
GAPToJulia( _JL_Vector_Any, args, false ),
GAPToJulia( _JL_Dict_Any, arec, false ) ) );
GAPToJulia( _JL_Dict_Any, kwargs, false ) ) );
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
20 changes: 10 additions & 10 deletions src/packages.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ 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.
# As soon as GAP.jl can rely on a good enough version of PackageManager
# we need not replace `PKGMAN_DownloadURL` anymore.
# (And the function should be renamed.)
res = load("PackageManager")
@assert res
Expand Down Expand Up @@ -59,15 +59,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