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

Allow lazily-evaluated code as argument to TEST #3490

Draft
wants to merge 18 commits into
base: development
Choose a base branch
from
Draft
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
6 changes: 4 additions & 2 deletions M2/Macaulay2/d/actors5.d
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,8 @@ examine(e:Expr):Expr := (
<< " restargs : " << desc.restargs << endl
<< " frameID : " << desc.frameID << endl
<< " framesize : " << desc.framesize << endl
<< " numparms : " << desc.numparms << endl;
<< " numparms : " << desc.numparms << endl
<< " operator : " << model.Operator.name << endl;
showFrames(f);
nullE)
is model:functionCode do (
Expand All @@ -470,7 +471,8 @@ examine(e:Expr):Expr := (
<< " restargs : " << desc.restargs << endl
<< " frameID : " << desc.frameID << endl
<< " framesize : " << desc.framesize << endl
<< " numparms : " << desc.numparms << endl;
<< " numparms : " << desc.numparms << endl
<< " operator : " << model.Operator.name << endl;
nullE)
is fn:CompiledFunction do (
stdIO
Expand Down
6 changes: 4 additions & 2 deletions M2/Macaulay2/d/binding.d
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ bumpPrecedence();
export RightArrowW := binaryright("->",arrowop); export RightArrowS := makeKeyword(RightArrowW);
export DoubleArrowS := makeKeyword(binaryright("=>"));
export GreaterGreaterS := makeKeyword(binaryright(">>"));
export TestS := special("TEST", thunkop, prec, prec);
bumpPrecedence();
whenW = token("when"); makeKeyword(whenW);
ofW = token("of"); makeKeyword(ofW);
Expand Down Expand Up @@ -311,7 +312,6 @@ bumpPrecedence();
export elapsedTimeS := special("elapsedTime",unaryop,precSpace,wide);
export elapsedTimingS := special("elapsedTiming",unaryop,precSpace,wide);
export shieldS := special("shield",unaryop,precSpace,wide);
export TestS := special("TEST",unaryop,precSpace,wide);
export throwS := special("throw",nunaryop,precSpace,wide);
export returnS := special("return",nunaryop,precSpace,wide);
export breakS := special("break",nunaryop,precSpace,wide);
Expand Down Expand Up @@ -846,7 +846,9 @@ export bind(e:ParseTree,dictionary:Dictionary):void := (
is a:Arrow do (
newdict := newLocalDictionary(dictionary);
a.desc = functionDescription(newdict.frameID,0,0,false);
bindParenParmList(a.lhs,newdict,a.desc);
when a.lhs
is dummy do nothing
else bindParenParmList(a.lhs,newdict,a.desc);
bind(a.rhs,newdict);
a.desc.framesize = newdict.framesize;
)
Expand Down
3 changes: 2 additions & 1 deletion M2/Macaulay2/d/convertr.d
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,8 @@ export convert0(e:ParseTree):Code := (
is p:Parentheses do convertParentheses(makeCodeSequence(p.contents, CommaW), p.left.word, pos)
is a:Adjacent do Code(adjacentCode(convert(a.lhs), convert(a.rhs), pos))
is a:Arrow do (
fc := functionCode(convert(a.rhs), a.desc, hash_t(0), pos);
fc := functionCode(convert(a.rhs), a.desc, hash_t(0), pos,
a.Operator.word);
fc.hash = hashFromAddress(Expr(fc));
Code(fc))
is b:Binary do (
Expand Down
37 changes: 29 additions & 8 deletions M2/Macaulay2/d/evaluate.d
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export nextS := setupvar("next", nullE);
export applyIteratorS := setupvar("applyIterator", nullE);
export joinIteratorsS := setupvar("joinIterators", nullE);

addTestS := setupvar("addTest", nullE); -- will be overwritten in testing.m2

handleError(c:Code,e:Expr):Expr;
eval(c:Code):Expr;
applyEE(f:Expr,e:Expr):Expr;
Expand Down Expand Up @@ -1442,7 +1444,11 @@ export evalraw(c:Code):Expr := (
is m:functionCode do (
fc := FunctionClosure(noRecycle(localFrame),m,hash_t(0));
fc.hash = hashFromAddress(Expr(fc));
return Expr(fc))
if m.Operator == TestS.symbol.word
then (
r := applyEE(getGlobalVariable(addTestS), Expr(fc));
when r is Error do r else nullE)
else Expr(fc))
is r:localMemoryReferenceCode do (
f := localFrame;
nd := r.nestingDepth;
Expand Down Expand Up @@ -1656,13 +1662,28 @@ breakFun(a:Code):Expr := (
when e is Error do e else Expr(Error(dummyPosition,breakMessage,e,false,dummyFrame)));
setupop(breakS,breakFun);

addTestS := setupvar("addTest", nullE); -- will be overwritten in testing.m2
testfun(c:Code):Expr := (
r := applyEE(
getGlobalVariable(addTestS),
seq(eval(c), locate(codePosition(c))));
when r is Error do r else nullE);
setupop(TestS, testfun);
addTestFromFile(e:Expr):Expr := (
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand why this is better. It's much more complicated than before.

Copy link
Member Author

Choose a reason for hiding this comment

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

The current behavior is to store a test's location in a hash table, which is easy to do at top level. But with this proposal, tests are becoming functions, and so unless we want all the tests that have been loaded from files to have testing.m2 as their location, we need to fake it somehow in the interpreter. This function was my attempt at doing that. I figured we should try to find out the ending position so that code will work.

when e
is filename:stringCell do (
x := get(filename.v);
when x
is err:errmsg do buildErrorPacket(err.message)
is s:stringCell do (
lastrow := 1;
lastcol := 0;
for i from 0 to length(s.v) - 2 do (
if s.v.i == '\n' then (
lastcol = 0;
lastrow = lastrow + 1)
else lastcol = lastcol + 1);
p := Position(filename.v, ushort(1), ushort(1), ushort(lastrow),
ushort(lastcol), ushort(1), ushort(1), loadDepth);
fc := functionCode(stringCode(s.v, p), dummyDesc, hash_t(0), p,
TestS.symbol.word);
fc.hash = hashFromAddress(Expr(fc));
eval(Code(fc))))
else WrongArgString());
setupfun("addTestFromFile", addTestFromFile);

assigntofun(lhs:Code,rhs:Code):Expr := (
left := eval(lhs);
Expand Down
3 changes: 2 additions & 1 deletion M2/Macaulay2/d/parse.d
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,8 @@ export functionCode := {+ -- this is called FunctionBody in the top-level
body:Code,
desc:functionDescription,
hash:hash_t,
position:Position
position:Position,
Operator:Word
};
export Code := (
-- when adding or removing classes of core here, also update debugging.dd
Expand Down
12 changes: 11 additions & 1 deletion M2/Macaulay2/d/parser.d
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,13 @@ export nbinaryop(lhs:ParseTree, token2:Token, file:TokenFile, prec:int, obeyline
export arrowop(lhs:ParseTree, token2:Token, file:TokenFile, prec:int, obeylines:bool):ParseTree := (
e := parse(file,token2.word.parse.binaryStrength,obeylines);
if e == errorTree then e else ParseTree(Arrow(lhs, token2, e, dummyDesc)));
-- "thunk" operator (e.g., TEST) for creating nullary functions
export thunkop(token:Token,file:TokenFile,prec:int,obeylines:bool):ParseTree:=(
ret := parse(file,max(prec,token.word.parse.unaryStrength),obeylines);
if ret == errorTree then ret
else accumulate(
ParseTree(Arrow(dummy(dummyPosition), token, ret, dummyDesc)),
file, prec, obeylines));
MatchPair := {left:string, right:string, next:(null or MatchPair)};

matchList := (null or MatchPair)(NULL);
Expand Down Expand Up @@ -518,7 +525,10 @@ export treePosition(e:ParseTree):Position := (
is s:Parentheses do combinePositionL(s.left.position, s.right.position)
is s:EmptyParentheses do combinePositionL(s.left.position, s.right.position)
is a:Adjacent do combinePositionM(treePosition(a.lhs), treePosition(a.rhs))
is a:Arrow do combinePositionL(treePosition(a.lhs), treePosition(a.rhs))
is a:Arrow do (
Copy link
Member

Choose a reason for hiding this comment

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

Treating TEST as -> is kludgy.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, it is a little strange. We need to call bind when things are still ParseTree objects in order to create a local dictionary, which is why I went this route. And Arrow is already set up for creating functions.

I also considered creating another member of ParseTree. Then we could avoid all the when lhs is dummy cases.

when a.lhs
is dummy do combinePositionL(a.Operator.position, treePosition(a.rhs))
else combinePositionL(treePosition(a.lhs), treePosition(a.rhs)))
is o:Unary do combinePositionL(o.Operator.position, treePosition(o.rhs))
is o:Binary do combinePositionC(treePosition(o.lhs), treePosition(o.rhs), o.Operator.position)
is o:Postfix do combinePositionR(treePosition(o.lhs), o.Operator.position)
Expand Down
2 changes: 1 addition & 1 deletion M2/Macaulay2/m2/examples.m2
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ isCapturable = (inputs, pkg, isTest) -> (
and not match("(capture|read|input|load|needs)\\b", inputs) -- these commands hide undesirable functions
and not match("([Cc]ommand|fork|schedule|thread|Task)", inputs) -- remove when threads work more predictably
and not match("(temporaryFileName)", inputs) -- this is sometimes bug prone
and not match("(addHook|export|newPackage)", inputs) -- these commands have permanent effects
and not match("(addHook|export|newPackage|TEST)", inputs) -- these commands have permanent effects
and not match("(installMethod|installAssignmentMethod)", inputs) -- same as above
and not match("(Global.*Hook|add.*Function|Echo|Print)", inputs) -- same as above
and not match("(importFrom|exportFrom)", inputs) -- currently capture tries to clear all symbols created, these break it
Expand Down
4 changes: 3 additions & 1 deletion M2/Macaulay2/m2/exports.m2
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,8 @@ export {
"TensorProduct",
"TeXmacs",
"Test",
"TestInput",
"TestClosure",
"TestFiles",
"Thing",
"Threads",
"Time",
Expand Down Expand Up @@ -1284,6 +1285,7 @@ export {
"numrows" => "numRows",
"res" => "resolution",
"sub" => "substitute",
"TestInput" => "TestClosure",
"threadVariable" => "threadLocal", -- TODO: eventually remove this?
-- unicode synonyms:
"ℚ" => "QQ",
Expand Down
3 changes: 2 additions & 1 deletion M2/Macaulay2/m2/packages.m2
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,8 @@ newPackage = method(
PackageImports => {},
Reload => false,
UseCachedExampleOutput => null,
Version => "0.0"
Version => "0.0",
TestFiles => {}
})
newPackage Sequence := opts -> x -> newPackage splice(nonnull x, opts) -- to allow null entries
newPackage String := opts -> pkgname -> (
Expand Down
67 changes: 34 additions & 33 deletions M2/Macaulay2/m2/testing.m2
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,30 @@ needs "code.m2"
needs "run.m2"

-----------------------------------------------------------------------------
-- TestInput
-- TestClosure
-----------------------------------------------------------------------------
TestInput = new SelfInitializingType of HashTable
TestInput.synonym = "test input"
TestClosure = new SelfInitializingType of FunctionClosure
TestClosure.synonym = "test closure"

code TestInput := code @@ locate
toString TestInput := T -> T#"code"
locate TestInput := T -> T#"location"
net TestInput := lookup(net, Function)
precedence TestInput := lookup(precedence, Function)
editMethod TestInput := editMethod @@ locate
capture TestInput := opt -> T -> capture(toString T, opt)
capture TestClosure := o -> t -> (
if instance(teststring := t(), String) then capture(teststring, o))

-----------------------------------------------------------------------------
-- TEST
-----------------------------------------------------------------------------

-- TEST is a keyword that takes an object as input and determines its
-- location. It then passes the object and its location to addTest.
-- TEST is a keyword that takes M2 code and turns it into a nullary function,
-- which it then passes to addTest

addTest = method()
addTest(String, FilePosition) := (str, loc) -> (
addTest FunctionClosure := f -> (
n := #currentPackage#"test inputs";
currentPackage#"test inputs"#n = TestInput {
"location" => loc,
"code" => str})
currentPackage#"test inputs"#n = TestClosure f)

-- the following is not called by TEST, but called directly when we want to
-- add a test from a file (used by loadTestDir)
addTest String := filename -> addTest(get filename,
new FilePosition from(filename, 1, 1))
addTest String := addTestFromFile

-- TODO: support test titles

-----------------------------------------------------------------------------
Expand Down Expand Up @@ -61,16 +56,16 @@ captureTestResult := (desc, teststring, pkg, usermode) -> (

loadTestDir := pkg -> (
-- TODO: prioritize reading the tests from topSrcdir | "Macaulay2/tests/normal" instead
testDir := pkg#"package prefix" |
replace("PKG", pkg#"pkgname", currentLayout#"packagetests");
pkg#"test directory loaded" =
if fileExists testDir then (
tmp := currentPackage;
currentPackage = pkg;
scan(sort select(readDirectory testDir, file -> match("\\.m2$", file)),
test -> addTest(testDir | test));
currentPackage = tmp;
true) else false)
testDir := pkg#"package prefix" | replace("PKG", pkg#"pkgname",
currentLayout#"packagetests");
pushvar(symbol currentPackage, pkg);
if fileExists testDir then scan(
sort select(readDirectory testDir, file -> match("\\.m2$", file)),
test -> addTest(testDir | test));
if pkg#?"auxiliary files" then scan((options pkg).TestFiles,
file -> addTest(pkg#"auxiliary files" | file));
popvar symbol currentPackage;
pkg#"test directory loaded" = true)

tests = method()
tests Package := pkg -> (
Expand Down Expand Up @@ -101,15 +96,21 @@ check(List, Package) := opts -> (L, pkg) -> (
--
errorList := for k in testKeys list (
if not inputs#?k then error(pkg, " has no test #", k);
teststring := inputs#k#"code";
desc := "check(" | toString k | ", " | format pkg#"pkgname" | ")";
ret := elapsedTime captureTestResult(desc, teststring, pkg, usermode);
if not ret then (k, temporaryFilenameCounter - 2) else continue);
elapsedTime (
try teststring := inputs#k() then (
if instance(teststring, String) then (
if captureTestResult(desc, teststring, pkg, usermode)
then continue
else (k, temporaryFilenameCounter - 2))
else (checkmsg("calling", desc); continue))
else (checkmsg("calling", desc); (k, null))));
outfile := errfile -> temporaryDirectory() | errfile | ".tmp";
if #errorList > 0 then (
if opts.Verbose then apply(errorList, (k, errfile) -> (
stderr << locate inputs#k << " error:" << endl;
printerr getErrors(outfile errfile)));
if errfile =!= null then (
stderr << locate inputs#k << " error:" << endl;
printerr getErrors(outfile errfile))));
error("test(s) #", demark(", ", toString \ first \ errorList), " of package ", toString pkg, " failed.")))

checkAllPackages = () -> (
Expand Down
13 changes: 7 additions & 6 deletions M2/Macaulay2/packages/JSON.m2
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
newPackage(
"JSON",
Headline => "JSON encoding and decoding",
Version => "0.2",
Date => "January 28, 2024",
Version => "0.3",
Date => "September 28, 2024",
Authors => {{
Name => "Doug Torrance",
Email => "[email protected]",
HomePage => "https://webwork.piedmont.edu/~dtorrance"}},
Keywords => {"System"},
PackageImports => {"Parsing"},
AuxiliaryFiles => true)
AuxiliaryFiles => true,
TestFiles => {"test-parse.m2", "test-encode.m2"})

---------------
-- ChangeLog --
---------------

-*

0.3 (2024-09-28, M2 1.24.11)
* use new TestFiles options instead of TEST for loading tests

0.2 (2024-01-24, M2 1.23)
* use single-string version of exportFrom
* use null coalescing operator in toJSON
Expand Down Expand Up @@ -357,6 +361,3 @@ for tst in sort select(tsts, f -> match("^y_", f)) do (
format json << ", " << toExternalString fromJSON json << ")" << endl)
close outfile
///

TEST get(currentPackage#"auxiliary files" | "test-parse.m2")
TEST get(currentPackage#"auxiliary files" | "test-encode.m2")
16 changes: 14 additions & 2 deletions M2/Macaulay2/packages/Macaulay2Doc/functions/check-doc.m2
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,23 @@ Node
If a test should be skipped when running @TO "check"@, e.g., it is
known to fail under certain circumstances, then the comment
@TT "-* no-check-flag *-"@ may be added to @TT "s"@.

Tests may also be defined using arbitrary Macaulay2 code. Tests created
in this way will be called as functions with no arguments, and so it
is important to write them as such. For example, variables should be
local (see @TO "local variables in a function"@) and all but the final
statement should end with a @TO symbol ;@.
Example
TEST (
x := 4;
assert(2 + 2 == x))
Caveat
When creating tests, try to ensure that they run relatively quickly.
SeeAlso
assert
beginDocumentation
check
tests
///

doc ///
Expand Down Expand Up @@ -79,8 +90,9 @@ Node
One provides tests using the @TO symbol TEST@ function following the @TO beginDocumentation@ call in the source
of the package.

Optionally, one can store all tests in a @TT "tests.m2"@ directory under the auxiliary subdirectory of
the package and load the file from the main package source file.
Optionally, one can store tests as files in the auxiliary subdirectory of
the package and include their filenames in the @TO TestFiles@ option
to @TO newPackage@.

For example, to run the tests for the @TO "LLLBases :: LLLBases"@ package (Lenstra-Lenstra-Lovasz bases), use:
CannedExample
Expand Down
4 changes: 4 additions & 0 deletions M2/Macaulay2/packages/Macaulay2Doc/functions/package-doc.m2
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ Node
[newPackage, PackageExports]
[newPackage, PackageImports]
[newPackage, Reload]
[newPackage, TestFiles]
Copy link
Member

@mahrud mahrud Sep 29, 2024

Choose a reason for hiding this comment

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

This is complicating the testing system rather than simplifying it, and I would personally rather not add yet another option to newPackage (e.g. should we also add DocumentationFiles next?)

Copy link
Member Author

Choose a reason for hiding this comment

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

Fair enough -- I wasn't that excited about this idea either.

Another possibility would be to change currentLayout#"packagetests" to point to a subdirectory of the auxiliary files directory (say test or tests) instead of the current behavior, where it's a subdirectory of the package's documentation directory. Then any files in that directory would automatically get loaded as tests, as currently happens for Core.

Copy link
Member

Choose a reason for hiding this comment

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

Polyhedra/tests might get in the way, but in principle that's fine with me.

[newPackage, UseCachedExampleOutput]
[newPackage, Version]
Name
Expand Down Expand Up @@ -338,6 +339,9 @@ Node
but a backup will be made and the user's settings for the surviving options will be retained.
Reload=>Boolean
whether to reload the package, if it has been loaded before
TestFiles=>List
of strings, names of auxiliary files to be run as part of the package's
test suite when calling @TO check@
Outputs
:Package
the new package
Expand Down
Loading
Loading