diff --git a/Kernel/Tools/WolframLanguageEvaluator.wl b/Kernel/Tools/WolframLanguageEvaluator.wl index 4cbfd64..6d7ba02 100644 --- a/Kernel/Tools/WolframLanguageEvaluator.wl +++ b/Kernel/Tools/WolframLanguageEvaluator.wl @@ -10,6 +10,7 @@ Needs[ "Wolfram`AgentTools`Graphics`" ]; Needs[ "Wolfram`AgentTools`Tools`" ]; Needs[ "Wolfram`Chatbook`" -> "cb`" ]; +Needs[ "CodeParser`" -> "cp`" ]; System`HoldCompleteForm; @@ -121,6 +122,7 @@ evaluateWolframLanguage[ args: KeyValuePattern[ "code" -> code_ ] ] := session = Lookup[ args, "session", Missing[ "session" ] ]; withSession[ session, + code, If[ TrueQ @ $clientSupportsUI && TrueQ @ $deployCloudNotebooks, evaluateWolframLanguageUI[ code, timeConstraint ], evaluateWolframLanguage[ code, timeConstraint ] @@ -555,10 +557,11 @@ initializePacletInLocalKernel // endDefinition; (*Sessions*) (* Each conversation gets an isolated, resumable evaluation session, keyed by an opaque ID supplied by the AI via the "session" parameter. Isolation comes from a per-session $Context: the session functions - run via useEvaluatorKernel and point the controlling kernel's $Context at "Sessions``", so the - user's code parses into that context (and, under the "Local" method, the resulting fully qualified - symbols then evaluate in that context inside the eval subkernel). State is persisted to disk so sessions - survive server restarts. + run via useEvaluatorKernel and point the eval kernel's $Context at "Sessions``", so the user's code + parses into that context. Under the in-process methods the eval kernel is this kernel. Under the "Local" + method the code is parsed here but evaluated in the eval subkernel, so syncParseContext also points this + kernel's parser at the subkernel's session state (see Local Parse Context). State is persisted to disk so + sessions survive server restarts. Line numbering is owned by the file-scoped $line: the authoritative per-session counter, persisted in the session payload and passed as the "Line" option. Under the in-process "Session" method that option @@ -735,9 +738,8 @@ restoreCloudSessionState // endDefinition; $currentSessionID and the file-scoped $line. Under in-process methods parsing, evaluation, and session state all live in this kernel. Under the "Local" method useEvaluatorKernel runs these in the persistent eval subkernel alongside the user's evaluations, whose $Line is seeded separately via - syncEvalKernelLine. (Note: under "Local", Chatbook parses code strings in the controlling kernel, so - parse-time binding of unqualified new symbols does not see the eval kernel's session contexts \[LongDash] - a known limitation of that method.) *) + syncEvalKernelLine, and whose context state syncParseContext mirrors into this kernel for parsing (see + Local Parse Context). *) (* ::**************************************************************************************************************:: *) (* ::Subsubsection::Closed:: *) @@ -1091,6 +1093,123 @@ resumeSessionInKernel[ path_String ] := (* :!CodeAnalysis::EndBlock:: *) resumeSessionInKernel // endDefinition; +(* ::**************************************************************************************************************:: *) +(* ::Subsection::Closed:: *) +(*Local Parse Context*) +(* Under the "Local" method the user's code is evaluated in the eval subkernel, but Chatbook parses the code + string in this kernel first, and the parse decides which symbol each short name refers to. Left to this + kernel's own context state, typed symbols would land in Global` (shared by every session) and names would + resolve against this kernel's packages instead of the session's. So before each evaluation + syncParseContext asks the eval kernel for the full name of the symbol that each symbol name in the code + refers to there, creates those symbols here (this kernel may not have loaded the session's packages), and + applies the session's $Context, $ContextPath, and $ContextAliases to this kernel, scoped to the call by + withSession. The parse here then finds each name where the eval kernel would. The in-process methods parse + where they evaluate, so this is a no-op for them. *) + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*syncParseContext*) +syncParseContext // beginDefinition; + +(* :!CodeAnalysis::BeginBlock:: *) +(* :!CodeAnalysis::Disable::SuspiciousSessionSymbol:: *) +syncParseContext[ code_String ] /; getEvaluatorMethod[ ] === "Local" := Enclose[ + Module[ { state }, + state = ConfirmMatch[ + (* Inject the literal names for the same reason as in saveSession. *) + With[ { names = codeSymbolNames @ code }, useEvaluatorKernel @ parseContextInKernel @ names ], + KeyValuePattern @ { + "$Context" -> _String, + "$ContextPath" -> { ___String }, + "$ContextAliases" -> _Association, + "Symbols" -> { ___String } + }, + "State" + ]; + (* Full names, so no aliases may apply; Quiet suppresses General::shdw for names that exist in several + contexts here. *) + Block[ { $ContextAliases = <| |> }, + Quiet @ Scan[ ToExpression[ #, InputForm, Hold ] &, state[ "Symbols" ] ] + ]; + $Context = state[ "$Context" ]; + $ContextPath = state[ "$ContextPath" ]; + $ContextAliases = state[ "$ContextAliases" ]; + ], + throwInternalFailure +]; +(* :!CodeAnalysis::EndBlock:: *) + +syncParseContext[ _String ] := Null; + +syncParseContext // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*syncParseContextSafe*) +(* Parse-context bookkeeping must never abort the user's evaluation (mirrors syncEvalKernelLineSafe). *) +syncParseContextSafe // beginDefinition; +syncParseContextSafe[ code_String ] := Quiet @ catchAlways @ syncParseContext @ code; +syncParseContextSafe[ _ ] := Null; +syncParseContextSafe // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*codeSymbolNames*) +(* The names of the symbols in a code string as written there (short, qualified, or relative such as `f). The + concrete parse also covers code with syntax errors. Named-character letters such as \[Alpha] are unescaped + so that the eval kernel accepts the names (see resolveSymbolName). *) +codeSymbolNames // beginDefinition; + +codeSymbolNames[ code_String ] := ( + (* We need to make sure CodeParser is loaded at runtime, since we might be running from an MX build *) + Needs[ "CodeParser`" -> None ]; + DeleteDuplicates @ StringReplace[ + Cases[ cp`CodeConcreteParse @ code, cp`LeafNode[ Symbol, name_String, _ ] :> name, Infinity ], + esc: ("\\[" ~~ LetterCharacter.. ~~ "]") :> unescapeLetter @ esc + ] +); + +codeSymbolNames // endDefinition; + + +unescapeLetter // beginDefinition; +unescapeLetter[ esc_String ] := + With[ { c = Quiet @ ToExpression[ "\"" <> esc <> "\"" ] }, If[ StringQ @ c && LetterQ @ c, c, esc ] ]; +unescapeLetter // endDefinition; + +(* ::**************************************************************************************************************:: *) +(* ::Subsubsection::Closed:: *) +(*parseContextInKernel*) +(* Runs in the eval kernel: its context state, plus the full name of the symbol that each given name refers to + there. *) +parseContextInKernel // beginDefinition; +(* :!CodeAnalysis::BeginBlock:: *) +(* :!CodeAnalysis::Disable::SuspiciousSessionSymbol:: *) +parseContextInKernel[ names: { ___String } ] := <| + "$Context" -> $Context, + "$ContextPath" -> $ContextPath, + "$ContextAliases" -> $ContextAliases, + "Symbols" -> Map[ resolveSymbolName, names ] +|>; +(* :!CodeAnalysis::EndBlock:: *) +parseContextInKernel // endDefinition; + + +(* The name is looked up exactly as the parser looks it up, which creates the symbol in $Context if it does not + exist yet (as the evaluation would anyway). Names that are not valid symbol names as written (e.g. other + escaped characters) are left out and resolved by the parse in the controlling kernel. *) +resolveSymbolName // beginDefinition; +resolveSymbolName[ name_String ] /; Internal`SymbolNameQ[ name, True ] := + ToExpression[ name, InputForm, fullSymbolName ]; +resolveSymbolName[ _String ] := Nothing; +resolveSymbolName // endDefinition; + + +fullSymbolName // beginDefinition; +fullSymbolName // Attributes = { HoldAllComplete }; +fullSymbolName[ symbol_Symbol ] := Context @ Unevaluated @ symbol <> SymbolName @ Unevaluated @ symbol; +fullSymbolName // endDefinition; + (* ::**************************************************************************************************************:: *) (* ::Subsection::Closed:: *) (*Session Cleanup*) @@ -1259,7 +1378,8 @@ saveSessionSafe // endDefinition; (* ::Subsubsection::Closed:: *) (*withSession*) (* Single choke point wrapping both eval paths: set up the session, evaluate, save, then append the - session info. HoldRest defers the evaluation until the session is active. Internal`InheritedBlock + session info. HoldRest defers the evaluation until the session is active; the code string is only needed + to set up parsing under the "Local" method (see Local Parse Context). Internal`InheritedBlock scopes $Context/$ContextPath/$ContextAliases to this call: the session context is active during the evaluation and the save, then restored to the kernel's neutral baseline afterward. Symbols created during the block (the user's definitions) persist in their session context, as do the session's @@ -1270,15 +1390,17 @@ saveSessionSafe // endDefinition; withSession // beginDefinition; withSession // Attributes = { HoldRest }; -withSession[ session_, eval_ ] := +withSession[ session_, code_, eval_ ] := Internal`InheritedBlock[ { $Context, $ContextPath, $ContextAliases }, Block[ { cb`$CloudSessionMX = None }, Module[ { id, result }, id = applySession @ session; (* applySession has set $line and the session $Context. For the "Local" method also push $line into the eval subkernel's $Line at a boundary (a continued session already tracks - it in lockstep); no-op for in-process methods. *) + it in lockstep), and point this kernel's parser at the subkernel's session state; both + are no-ops for in-process methods. *) If[ $sessionStatus =!= "continued", syncEvalKernelLineSafe @ $line ]; + syncParseContextSafe @ code; result = eval; saveSessionSafe @ id; appendSessionInfo[ result, id ] diff --git a/Tests/EvaluatorSessions.wlt b/Tests/EvaluatorSessions.wlt index 72d612d..7a953f0 100644 --- a/Tests/EvaluatorSessions.wlt +++ b/Tests/EvaluatorSessions.wlt @@ -743,6 +743,104 @@ VerificationTest[ TestID -> "SessionInfoStatusText-InKernelHasNoCloudNotice@@Tests/EvaluatorSessions.wlt:734,1-744,2" ] +(* ::**************************************************************************************************************:: *) +(* ::Section::Closed:: *) +(*Local Parse Context*) +(* Under the "Local" method Chatbook parses the code in this kernel but evaluates it in the eval subkernel, so + the parse is given the eval kernel's context state and the eval kernel's symbols for the code's names. *) + +(* Only symbols count (not strings, comments, operators such as \[Equal], or slots), as written: qualified and + relative names stay whole, and named-character letters are unescaped. Code with syntax errors works too. *) +VerificationTest[ + Wolfram`AgentTools`Tools`WolframLanguageEvaluator`Private`codeSymbolNames[ + "f[x_Integer] := \\[Alpha]1 + $v2 + ctx`g + a\\[Equal]b (* note *) + \"word\" + f[2x + `y] + #s & + h[" + ], + { "f", "x", "Integer", "\[Alpha]1", "$v2", "ctx`g", "a", "b", "`y", "h" }, + SameTest -> MatchQ, + TestID -> "CodeSymbolNames-GH#249@@Tests/EvaluatorSessions.wlt:754,1-761,2" +] + +(* The eval kernel reports its context state and the full name of the symbol each name refers to there: found on + $ContextPath first, then in $Context, where names that do not exist yet are created (as the parser would); + qualified and relative names are resolved too. Names that are not valid as written are left out. *) +VerificationTest[ + Module[ { result }, + result = Block[ { $Context = "ParseCtxTestA`", $ContextPath = { "ParseCtxTestB`", "System`" } }, + ParseCtxTestA`pctShared = 1; + ParseCtxTestB`pctShared = 2; + ParseCtxTestA`pctOnlyA = 3; + { + Wolfram`AgentTools`Tools`WolframLanguageEvaluator`Private`parseContextInKernel[ + { "pctShared", "pctOnlyA", "pctNew", "Plus", "`pctRel", "ParseCtxTestB`pctOther", "\\:03b2" } + ], + NameQ[ "ParseCtxTestA`pctNew" ] + } + ]; + Quiet @ Remove[ "ParseCtxTestA`*", "ParseCtxTestB`*" ]; + result + ], + { + <| + "$Context" -> "ParseCtxTestA`", + "$ContextPath" -> { "ParseCtxTestB`", "System`" }, + "$ContextAliases" -> _Association, + "Symbols" -> { + "ParseCtxTestB`pctShared", + "ParseCtxTestA`pctOnlyA", + "ParseCtxTestA`pctNew", + "System`Plus", + "ParseCtxTestA`pctRel", + "ParseCtxTestB`pctOther" + } + |>, + True + }, + SameTest -> MatchQ, + TestID -> "ParseContextInKernel-ResolvesNames-GH#249@@Tests/EvaluatorSessions.wlt:766,1-800,2" +] + +(* With the eval kernel stubbed: the symbols it reports are created here although this kernel never loaded + their package, and its context state is applied, so a following parse resolves short and aliased names to + the eval kernel's symbols and creates new names in the session context. *) +VerificationTest[ + Module[ { result }, + result = Internal`InheritedBlock[ { $Context, $ContextPath, $ContextAliases }, + Block[ + { + Wolfram`AgentTools`Common`$toolOptions = <| "WolframLanguageEvaluator" -> <| "Method" -> "Local" |> |>, + Wolfram`AgentTools`Common`useEvaluatorKernel = Function[ eval, eval, HoldAllComplete ], + Wolfram`AgentTools`Tools`WolframLanguageEvaluator`Private`parseContextInKernel = <| + "$Context" -> "Sessions`PctStubSess`", + "$ContextPath" -> { "PctStubPkg`", "Sessions`PctStubSess`", "System`" }, + "$ContextAliases" -> <| "pcs`" -> "PctStubPkg`" |>, + "Symbols" -> { "PctStubPkg`pctStubFn" } + |> & + }, + Wolfram`AgentTools`Tools`WolframLanguageEvaluator`Private`syncParseContext[ "pctStubFn[1] + pctStubNew" ]; + { $Context, ToExpression[ #, InputForm, Context ] & /@ { "pctStubFn", "pcs`pctStubFn", "pctStubNew" } } + ] + ]; + Quiet @ Remove[ "PctStubPkg`*", "Sessions`PctStubSess`*" ]; + result + ], + { "Sessions`PctStubSess`", { "PctStubPkg`", "PctStubPkg`", "Sessions`PctStubSess`" } }, + SameTest -> MatchQ, + TestID -> "SyncParseContext-AppliesEvalKernelState-GH#249@@Tests/EvaluatorSessions.wlt:805,1-829,2" +] + +(* In-process methods parse where they evaluate, so this kernel's context state is left alone. *) +VerificationTest[ + Internal`InheritedBlock[ { $Context, $ContextPath, $ContextAliases }, + Block[ { Wolfram`AgentTools`Common`$toolOptions = <| |> }, + Wolfram`AgentTools`Tools`WolframLanguageEvaluator`Private`syncParseContext[ "x + 1" ]; + { $Context, $ContextPath, $ContextAliases } + ] + ] === { $Context, $ContextPath, $ContextAliases }, + True, + SameTest -> MatchQ, + TestID -> "SyncParseContext-NoOpForInProcess-GH#249@@Tests/EvaluatorSessions.wlt:832,1-842,2" +] + (* ::**************************************************************************************************************:: *) (* ::Section::Closed:: *) (*Integration: end-to-end session behavior*) @@ -768,7 +866,7 @@ VerificationTest[ StringContainsQ[ extractToolText @ r3, "42" ] ], True, - TestID -> "Integration-SessionIsolation@@Tests/EvaluatorSessions.wlt:753,1-772,2" + TestID -> "Integration-SessionIsolation@@Tests/EvaluatorSessions.wlt:851,1-870,2" ] (* Re-passing the same session ID continues it: definitions persist and line numbers advance. *) @@ -790,7 +888,7 @@ VerificationTest[ StringContainsQ[ text, "6" ] && StringContainsQ[ text, "Out[2]" ] ], True, - TestID -> "Integration-ContinueSamePersistsAndAdvancesLine@@Tests/EvaluatorSessions.wlt:775,1-794,2" + TestID -> "Integration-ContinueSamePersistsAndAdvancesLine@@Tests/EvaluatorSessions.wlt:873,1-892,2" ] (* A session resumes from disk after its in-kernel symbols are gone (simulated server restart). *) @@ -814,7 +912,7 @@ VerificationTest[ StringContainsQ[ extractToolText @ r2, "99" ] ], True, - TestID -> "Integration-RestartResumeFromDisk@@Tests/EvaluatorSessions.wlt:797,1-818,2" + TestID -> "Integration-RestartResumeFromDisk@@Tests/EvaluatorSessions.wlt:895,1-916,2" ] (* Every result echoes the session ID with resume instructions. *) @@ -834,7 +932,7 @@ VerificationTest[ StringContainsQ[ extractToolText @ r, "session=\"AppendSession\"" ] ], True, - TestID -> "Integration-AppendsSessionInfo@@Tests/EvaluatorSessions.wlt:821,1-838,2" + TestID -> "Integration-AppendsSessionInfo@@Tests/EvaluatorSessions.wlt:919,1-936,2" ] (* A fresh session's first evaluation is labeled Out[1]. *) @@ -854,7 +952,7 @@ VerificationTest[ StringContainsQ[ extractToolText @ r, "Out[1]" ] ], True, - TestID -> "Integration-FreshSessionStartsAtLineOne@@Tests/EvaluatorSessions.wlt:841,1-858,2" + TestID -> "Integration-FreshSessionStartsAtLineOne@@Tests/EvaluatorSessions.wlt:939,1-956,2" ] (* Resuming a session continues its line numbering rather than resetting it: A reaches Out[2], B @@ -879,7 +977,7 @@ VerificationTest[ StringContainsQ[ extractToolText @ r, "Out[3]" ] ], True, - TestID -> "Integration-ResumeContinuesLineNumbering@@Tests/EvaluatorSessions.wlt:863,1-883,2" + TestID -> "Integration-ResumeContinuesLineNumbering@@Tests/EvaluatorSessions.wlt:961,1-981,2" ] (* An unknown / expired session ID starts a fresh session reusing that ID and says so. *) @@ -899,7 +997,7 @@ VerificationTest[ StringContainsQ[ text, "NeverSavedXyz" ] && StringContainsQ[ text, "No saved state" ] ], True, - TestID -> "Integration-UnknownIdReusedFresh@@Tests/EvaluatorSessions.wlt:886,1-903,2" + TestID -> "Integration-UnknownIdReusedFresh@@Tests/EvaluatorSessions.wlt:984,1-1001,2" ] (* Context-path changes made inside a session (e.g. by Get) survive continued calls: the continuing @@ -923,7 +1021,7 @@ VerificationTest[ StringContainsQ[ extractToolText @ r2, "{101, True}" ] ], True, - TestID -> "Integration-ContinuePreservesContextPath@@Tests/EvaluatorSessions.wlt:908,1-927,2" + TestID -> "Integration-ContinuePreservesContextPath@@Tests/EvaluatorSessions.wlt:1006,1-1025,2" ] (* Resuming a session saved by a different kernel process restores the saved state and warns that @@ -953,7 +1051,7 @@ VerificationTest[ ], { True, True }, SameTest -> MatchQ, - TestID -> "Integration-ResumeFromPreviousKernelWarns@@Tests/EvaluatorSessions.wlt:932,1-957,2" + TestID -> "Integration-ResumeFromPreviousKernelWarns@@Tests/EvaluatorSessions.wlt:1030,1-1055,2" ] (* Switching back to an earlier session within the same kernel process resumes silently: no @@ -978,7 +1076,7 @@ VerificationTest[ ], { True, False }, SameTest -> MatchQ, - TestID -> "Integration-SameKernelResumeHasNoWarning@@Tests/EvaluatorSessions.wlt:961,1-982,2" + TestID -> "Integration-SameKernelResumeHasNoWarning@@Tests/EvaluatorSessions.wlt:1059,1-1080,2" ] (* If the eval kernel loses its in-memory session state while the session is still current (e.g. the @@ -1009,7 +1107,108 @@ VerificationTest[ ], { True, True, False }, SameTest -> MatchQ, - TestID -> "Integration-ContinueFallsBackToFileWhenKernelStateLost@@Tests/EvaluatorSessions.wlt:987,1-1013,2" + TestID -> "Integration-ContinueFallsBackToFileWhenKernelStateLost@@Tests/EvaluatorSessions.wlt:1085,1-1111,2" +] + +(* ::**************************************************************************************************************:: *) +(* ::Section::Closed:: *) +(*Integration: Local method*) +(* The real tool under the "Local" method, which evaluates in a Chatbook sandbox subkernel but parses the code + in this kernel. Skipped when the sandbox kernel cannot be started. *) +$localMethodOptions = <| "WolframLanguageEvaluator" -> <| "Method" -> "Local" |> |>; + +$localSessionTest = conditionalTest @ MatchQ[ + Quiet @ Wolfram`Chatbook`WolframLanguageToolEvaluate[ "1 + 1", "Result", "Method" -> "Local" ], + (HoldForm|HoldCompleteForm)[ 2 ] +]; + +withLocalSessions // Attributes = { HoldFirst }; +withLocalSessions[ eval_ ] := + Module[ { root }, + root = FileNameJoin @ { $TemporaryDirectory, "AgentToolsLocalSession_" <> CreateUUID[ ] }; + WithCleanup[ + Block[ + { + Wolfram`AgentTools`Common`$rootPath = root, + Wolfram`AgentTools`Common`$clientSupportsUI = False, + Wolfram`AgentTools`Common`$toolOptions = $localMethodOptions, + Wolfram`AgentTools`Tools`WolframLanguageEvaluator`Private`$currentSessionID = None + }, + eval + ], + Quiet @ DeleteDirectory[ root, DeleteContents -> True ] + ] + ]; + +localToolText[ code_String, session_String ] := + extractToolText @ $DefaultMCPTools[ "WolframLanguageEvaluator" ][ <| "code" -> code, "session" -> session |> ]; + +(* Typed symbols are created in the session context, so another session does not see them, and names are not + resolved against the packages loaded in this kernel. *) +$localSessionTest @ VerificationTest[ + withLocalSessions @ { + localToolText[ "localIsoX = 1; {Context[localIsoX], Context[CellToString], Context[StartMCPServer]}", "LocalIsoA" ], + localToolText[ "{localIsoX, Context[localIsoX]}", "LocalIsoB" ] + }, + { + _? (StringContainsQ[ "{\"Sessions`LocalIsoA`\", \"Sessions`LocalIsoA`\", \"Sessions`LocalIsoA`\"}" ]), + _? (StringContainsQ[ "{localIsoX, \"Sessions`LocalIsoB`\"}" ]) + }, + SameTest -> MatchQ, + TestID -> "Integration-Local-TypedSymbolsInSessionContext-GH#249@@Tests/EvaluatorSessions.wlt:1148,21-1159,2" +] + +(* Later calls resolve names through the contexts and aliases the session added: a package loaded only in the + eval kernel and an alias given to Needs. *) +$localSessionTest @ VerificationTest[ + Module[ { pkg, text }, + pkg = FileNameJoin @ { $TemporaryDirectory, "LocalPkg249_" <> CreateUUID[ ] <> ".wl" }; + Export[ + pkg, + "BeginPackage[\"LocalPkg249`\"];\nlocalPkgFn::usage = \"\";\nBegin[\"`Private`\"];\n" <> + "localPkgFn[x_] := x + 1000;\nEnd[];\nEndPackage[];\n", + "Text" + ]; + text = Last @ withLocalSessions @ { + localToolText[ "Get[" <> ToString[ pkg, InputForm ] <> "]; Needs[\"Developer`\" -> \"dv`\"]", "LocalPkgSess" ], + localToolText[ "{localPkgFn[1], Context[localPkgFn], dv`PackedArrayQ[Range[3]]}", "LocalPkgSess" ] + }; + Quiet @ DeleteFile @ pkg; + { StringContainsQ[ text, "{1001, \"LocalPkg249`\", True}" ], StringFreeQ[ text, "::shdw" ] } + ], + { True, True }, + SameTest -> MatchQ, + TestID -> "Integration-Local-SessionContextPathAndAliases-GH#249@@Tests/EvaluatorSessions.wlt:1163,21-1182,2" +] + +(* Symbols created at run time and typed symbols are the same symbols, and functions defined in an earlier call + are not reported as undefined. *) +$localSessionTest @ VerificationTest[ + Module[ { t1, t2 }, + { t1, t2 } = withLocalSessions @ { + localToolText[ "LocalUpperDef[x_] := x + 1; ToExpression[\"localGenSym = 5\"]; LocalUpperDef[1]", "LocalWarnSess" ], + localToolText[ "{LocalUpperDef[2], localGenSym}", "LocalWarnSess" ] + }; + { StringContainsQ[ t2, "{3, 5}" ], StringFreeQ[ t1 <> t2, "Symbol::undefined" ] } + ], + { True, True }, + SameTest -> MatchQ, + TestID -> "Integration-Local-NoUndefinedSymbolWarnings-GH#249@@Tests/EvaluatorSessions.wlt:1186,21-1197,2" +] + +(* The session's context state is applied to this kernel only for the duration of the call. *) +$localSessionTest @ VerificationTest[ + Module[ { before }, + before = { $Context, $ContextPath, $ContextAliases }; + withLocalSessions @ { + localToolText[ "Needs[\"Developer`\" -> \"dv`\"]; 1", "LocalLeakSess" ], + localToolText[ "2", "LocalLeakSess" ] (* continued: the session state includes the alias *) + }; + before === { $Context, $ContextPath, $ContextAliases } + ], + True, + SameTest -> MatchQ, + TestID -> "Integration-Local-ParseContextScopedToCall-GH#249@@Tests/EvaluatorSessions.wlt:1200,21-1212,2" ] (* ::**************************************************************************************************************:: *) @@ -1052,7 +1251,7 @@ $cloudSessionTest @ VerificationTest[ ], { True, True, True, True }, SameTest -> MatchQ, - TestID -> "Integration-CloudSessionDefinitionsSurviveRestart@@Tests/EvaluatorSessions.wlt:1027,21-1056,2" + TestID -> "Integration-CloudSessionDefinitionsSurviveRestart@@Tests/EvaluatorSessions.wlt:1226,21-1255,2" ] (* :!CodeAnalysis::EndBlock:: *)