mm() a MsgBox alternative for debugging that shows not just the value but the source-code expression evaluated

Post your working scripts, libraries and tools.
bonobo
Posts: 76
Joined: 03 Sep 2023, 20:13

mm() a MsgBox alternative for debugging that shows not just the value but the source-code expression evaluated

Post by bonobo » 18 Nov 2023, 01:47

Instead of

Code: Select all

Msgbox "var = " var
you can just write:

Code: Select all

mm var
And mm() will print "var ⟹ {{whatever the value of var is}}"

Usage Examples:

Code: Select all

f := i=>i**i
mm f(3) ; prints: f(3) ⟹ 27
mm f(4) ; prints: f(4) ⟹ 256
mm f(3)+f(4) ; prints: f(3)+f(4) ⟹ 283
mm 3.14*4**2 ; prints: 3.14*4**2 ⟹ 50.240000000000002
mm StrSplit("1234") ; prints: StrSplit("1234") ⟹ ["1","2","3","4"]
mm StrSplit("1|2|3|4","|") ; prints: StrSplit("1|2|3|4","|") ⟹ ["1","2","3","4"]
mm A_AhkVersion, A_OSVersion ; prints: A_AhkVersion, A_OSVersion ⟹ "2.1-alpha.7","10.0.19045"
mm [A_AhkVersion, A_OSVersion] ; prints: [A_AhkVersion, A_OSVersion] ⟹ ["2.1-alpha.7","10.0.19045"]
mm A_Year, A_YWeek, A_WDay ; prints: A_Year, A_YWeek, A_WDay ⟹ "2023","202346","7"
mm [1,2,f(3)] ; prints: [1,2,f(3)] ⟹ [1,2,283]
Tooltip(mm(f(3)+f(4))) ; prints: f(3)+f(4) ⟹ 283 (and returning 283 to be fed as argument to Tooltip)

Here's the function.
1. Limitation: if there are 2 mm() calls on the same line, it's not possible for mm() to identify which is the callsite. This is because Error objects give the line number but not the column number of the callsite (compare getColumnNumber() in V8's stack trace API https://v8.dev/docs/stack-trace-api)
2. You need to have a serializer somewhere else in your script in order for mm to properly print arrays, objects, etc. By default it is assumed to be called "JSON.stringify" but you should change the value of SerializeFunc if it's not.
3. Instead of 'printing' via Msgbox, you can configure it to print via Tooltip, StdOut, OutputDebug, or a log file. Just check out the LogFunc variable in the function body.

Code: Select all

mm(args*) {
	static LogFunc := { MsgBox: MsgBox, Tooltip: Tooltip, StdOut: (x) => FileAppend(x, "*"), OutputDebug: OutputDebug, LogToFile: (x) => FileAppend(x, "tmp.log") }.MsgBox
		, SerializeFunc := { JSON: x => x is Primitive ? x : JSON.stringify(x) }.JSON
		, FormatFunc := { Terse: (info) => Format("{1} ⟹ {2}", info.expression, info.value), Verbose: (info) => SerializeFunc(info) }.Terse
		, ReadLineFromFile := (file, lineNo) => StrSplit(FileRead(file), "`n", "`r")[lineNo]
		, funcName := A_ThisFunc
		, FindExpressionInLine := ((line) => RegExMatch(line, "^\s*" funcName " (.+)$", &mo) ? mo[1] : RegExMatch(line, "\b" funcName "\(((?:[^()]+|(?>\((?1)\)))+)\)", &mo) ? Substr(mo[0], StrLen(funcName) + 2, -1) : "Expression not found; line context = " line)
		, Cache := Map()
	try throw Error(, -1)
	catch as err
	{
		valStr := SerializeFunc(args), args.length ? valStr := SubStr(valStr, 2, -1) : 0
		if not Cache.Has(fileLineKey := err.File "//" err.Line)
			Cache.Set(fileLineKey, Trim(ReadLineFromFile(err.File, err.Line)))
		LogFunc(FormatFunc({ expression: FindExpressionInLine(Cache[fileLineKey]), value: valStr, file: err.File, lineNo: err.Line, lineText: Cache[fileLineKey] }))
	}
	return args.length == 1 ? args[1] : args
}

User avatar
kunkel321
Posts: 1137
Joined: 30 Nov 2015, 21:19

Re: mm() a MsgBox alternative for debugging that shows not just the value but the source-code expression evaluated

Post by kunkel321 » 06 Dec 2023, 16:23

Thanks for sharing this! I'm finally getting a chance to try it out. You mentioned a serializer... What is that? Can you post the one you're using? I'm guessing that this is related to the error I'm seeing:

Code: Select all

Warning: This variable appears to never be assigned a value.

Specifically: local JSON

	021: Static LogFunc := { MsgBox: MsgBox, Tooltip: Tooltip, StdOut: (x) => FileAppend(x, "*"), OutputDebug: OutputDebug, LogToFile: (x) => FileAppend(x, "tmp.log") }.MsgBox
	021: {
▶	021: Return  x is Primitive ? x : JSON.stringify(x)
	021: }
	021: Static SerializeFunc := { JSON: x => x is Primitive ? x : JSON.stringify(x) }.JSON
ste(phen|ve) kunkel

bonobo
Posts: 76
Joined: 03 Sep 2023, 20:13

Re: mm() a MsgBox alternative for debugging that shows not just the value but the source-code expression evaluated

Post by bonobo » 07 Dec 2023, 13:20

kunkel321 wrote:
06 Dec 2023, 16:23
Thanks for sharing this! I'm finally getting a chance to try it out. You mentioned a serializer... What is that? Can you post the one you're using? I'm guessing that this is related to the error I'm seeing:
Hi @kunkel321 , it's just a function that can convert AHK objects (objects, arrays, map, etc) to a string representation. Any JSON serializer would do (e.g. check out the libraries mentioned in this thread: viewtopic.php?f=82&t=121613&p=539900)

If you use thqby's cppjson (viewtopic.php?style=19&t=100602), the 2nd line in the function body should look like

Code: Select all

, SerializeFunc := { JSON: x => x is Primitive ? x : JSON.stringify(x) }.JSON
If you use Geekdude's cjson (just out (viewtopic.php?f=83&t=123517)), it should look like

Code: Select all

, SerializeFunc := { JSON: x => x is Primitive ? x : JSON.Dump(x) }.JSON
Any JSON serializer will do.

(Actually, a better function to use would be just a generic object inspection function like Python's repr() or Node's util.inspect(), which can give you information about types etc, but I don't know if anyone's written such a library for ahk2 yet)

bonobo
Posts: 76
Joined: 03 Sep 2023, 20:13

Re: mm() a MsgBox alternative for debugging that shows not just the value but the source-code expression evaluated

Post by bonobo » 07 Dec 2023, 13:52

Actually' @AHK_user has a nice function (Object2Str) here that might be more suited for debugging purposes:

viewtopic.php?p=547378#p547378

However, this line in Object2Str

Code: Select all

if !(Type(Var) ~="Map|Array|Object|String|Number|Integer"){
should be changed to

Code: Select all

if !(Type(Var) ~="Map|Array|Object|String|Number|Integer|Float"){
so floating point inputs don't throw.

Here's a complete example using Object2Str:

Code: Select all

f := i=>i**i
mm f(3)
mm f(4)
mm f(3)+f(4)
mm 3.14*4**2
mm StrSplit("1234")
mm StrSplit("1|2|3|4","|")
mm A_AhkVersion, A_OSVersion
mm [A_AhkVersion, A_OSVersion]
mm A_Year, A_YWeek, A_WDay
mm [1,2,f(3)]
Tooltip(mm(f(3)+f(4)))


mm(args*) {
    static LogFunc := { MsgBox: MsgBox, Tooltip: Tooltip, StdOut: (x) => FileAppend(x, "*"), OutputDebug: OutputDebug, LogToFile: (x) => FileAppend(x, "tmp.log") }.MsgBox
        , SerializeFunc := Object2Str
        , FormatFunc := { Terse: (info) => Format("{1} ⟹ {2}", info.expression, info.value), Verbose: (info) => SerializeFunc(info) }.Terse
        , ReadLineFromFile := (file, lineNo) => StrSplit(FileRead(file), "`n", "`r")[lineNo]
        , funcName := A_ThisFunc
        , FindExpressionInLine := ((line) => RegExMatch(line, "^\s*" funcName " (.+)$", &mo) ? mo[1] : RegExMatch(line, "\b" funcName "\(((?:[^()]+|(?>\((?1)\)))+)\)", &mo) ? Substr(mo[0], StrLen(funcName) + 2, -1) : "Expression not found; line context = " line)
        , Cache := Map()
    try throw Error(, -1)
    catch as err
    {
        valStr := SerializeFunc(args), args.length ? valStr := SubStr(valStr, 2, -1) : 0
        if not Cache.Has(fileLineKey := err.File "//" err.Line)
            Cache.Set(fileLineKey, Trim(ReadLineFromFile(err.File, err.Line)))
        LogFunc(FormatFunc({ expression: FindExpressionInLine(Cache[fileLineKey]), value: valStr, file: err.File, lineNo: err.Line, lineText: Cache[fileLineKey] }))
    }
    return args.length == 1 ? args[1] : args
    ; by AHK_user (https://www.autohotkey.com/boards/viewtopic.php?p=547378#p547378)
    Object2Str(Var){
        Output := ""
        if !(Type(Var) ~="Map|Array|Object|String|Number|Integer|Float"){
            throw Error("Object type not supported.", -1, Format("<Object at 0x{:p}>", Type(Var)))
        }
        if (Type(Var)="Array"){
            Output .= "["
            For Index, Value in Var{
                Output .= ((Index=1) ? "" : ",") Object2Str(Value)
            }
            Output .= "]"
        } else if (Type(Var)="Map"){
            Output .= "Map("
            For Key , Value in Var {
                Output .= ((A_index=1) ? "" : ",") Key "," Object2Str(Value)
            }
            Output .= ")"
        } else if (Type(Var)="Object"){
            Output .= "{"
            For Key , Value in Var.Ownprops() {
                Output .= ((A_index=1) ? "" : ",") Key ":" Object2Str(Value)
            }
            Output .= "}"
        } else if (Type(Var)="String"){

            ; Quotes := InStr(Var,"'") ? '"' : "'"
            ; MsgBox(Var "`n" Quotes )
            Output := IsNumber(Var) ? Var : InStr(Var,"'") ? '"' Var '"' : "'" StrReplace(Var,"'","``'") "'"
        } else {
            Output := Var
        }
        if (Type(Var) ~="Map|Array" and ObjOwnPropCount(Var)>0){
            Output .= "{"
            For Key , Value in Var.Ownprops() {
                Output .= ((A_index=1) ? "" : ",") Key ":" Object2Str(Value)
            }
            Output .= "}"
        }

        Return Output
    }
}



User avatar
kunkel321
Posts: 1137
Joined: 30 Nov 2015, 21:19

Re: mm() a MsgBox alternative for debugging that shows not just the value but the source-code expression evaluated

Post by kunkel321 » 07 Dec 2023, 14:56

Yep -- That works. Many thanks!
ste(phen|ve) kunkel

AHK_user
Posts: 515
Joined: 04 Dec 2015, 14:52
Location: Belgium

Re: mm() a MsgBox alternative for debugging that shows not just the value but the source-code expression evaluated

Post by AHK_user » 07 Dec 2023, 17:56

Cool script, I like it. :bravo:

I would not advise the use of my Object2Str function, a better function would be my Obj_Gui function.
Obj_Gui is a nice example how to display more complex variables.
viewtopic.php?f=83&t=103437&hilit=gui+object#p459683

:think: I will probably add this script to Obj_Gui, to also display the name of the variable.

Thanks a lot :D

bonobo
Posts: 76
Joined: 03 Sep 2023, 20:13

Re: mm() a MsgBox alternative for debugging that shows not just the value but the source-code expression evaluated

Post by bonobo » 08 Dec 2023, 16:42

kunkel321 wrote:
07 Dec 2023, 14:56
Yep -- That works. Many thanks!
Cheers!
AHK_user wrote:
07 Dec 2023, 17:56
I would not advise the use of my Object2Str function, a better function would be my Obj_Gui function.
Obj_Gui is a nice example how to display more complex variables.
viewtopic.php?f=83&t=103437&hilit=gui+object#p459683
Very nice. Thanks for the pointer to Obj_Gui!

geek
Posts: 1053
Joined: 02 Oct 2013, 22:13
Location: GeekDude
Contact:

Re: mm() a MsgBox alternative for debugging that shows not just the value but the source-code expression evaluated

Post by geek » 11 Dec 2023, 09:59

Interesting concept, though it's worth noting that it does not work with tools that pass the script code to AHK by StdIn instead of by a file (like CodeQuickTester, the wiki sandbox, and some other tools), and if you edit the file of a running script it will reflect the new version not the running version of the code.

Also, you can throw out the try/catch. Try and Catch are not needed to create an Error, and while they are normally used with Error they are technically unrelated. For example, you can write this that throws and catches a number:

Code: Select all

try throw 4
catch any as e {
	MsgBox e
}
and you can write this code that saves an Error to a variable without using try/catch:

Code: Select all

err := Error(, -1)
By adjusting your function to skip using the try/catch, it would look like this:

Code: Select all

mm(args*) {
	static LogFunc := { MsgBox: MsgBox, Tooltip: Tooltip, StdOut: (x) => FileAppend(x, "*"), OutputDebug: OutputDebug, LogToFile: (x) => FileAppend(x, "tmp.log") }.MsgBox
		, SerializeFunc := { JSON: x => x is Primitive ? x : JSON.stringify(x) }.JSON
		, FormatFunc := { Terse: (info) => Format("{1} ⟹ {2}", info.expression, info.value), Verbose: (info) => SerializeFunc(info) }.Terse
		, ReadLineFromFile := (file, lineNo) => StrSplit(FileRead(file), "`n", "`r")[lineNo]
		, funcName := A_ThisFunc
		, FindExpressionInLine := ((line) => RegExMatch(line, "^\s*" funcName " (.+)$", &mo) ? mo[1] : RegExMatch(line, "\b" funcName "\(((?:[^()]+|(?>\((?1)\)))+)\)", &mo) ? Substr(mo[0], StrLen(funcName) + 2, -1) : "Expression not found; line context = " line)
		, Cache := Map()
	err := Error(, -1)
	valStr := SerializeFunc(args), args.length ? valStr := SubStr(valStr, 2, -1) : 0
	if not Cache.Has(fileLineKey := err.File "//" err.Line)
		Cache.Set(fileLineKey, Trim(ReadLineFromFile(err.File, err.Line)))
	LogFunc(FormatFunc({ expression: FindExpressionInLine(Cache[fileLineKey]), value: valStr, file: err.File, lineNo: err.Line, lineText: Cache[fileLineKey] }))
	return args.length == 1 ? args[1] : args
}

geek
Posts: 1053
Joined: 02 Oct 2013, 22:13
Location: GeekDude
Contact:

Re: mm() a MsgBox alternative for debugging that shows not just the value but the source-code expression evaluated

Post by geek » 04 Jan 2024, 13:28

I just had a thought, about using the Error's stack trace to extract the expression text rather than trying to read it out of the file. It seems to work ok, but I did not think about this extensively to understand if it will work everywhere. But to the extent that it does work, it works without needing the file on disk to match the running script.

Code: Select all

mm(args*) {
	static LogFunc := { MsgBox: MsgBox, Tooltip: Tooltip, StdOut: (x) => FileAppend(x, "*"), OutputDebug: OutputDebug, LogToFile: (x) => FileAppend(x, "tmp.log") }.MsgBox
		, SerializeFunc := { JSON: x => x is Primitive ? x : JSON.stringify(x) }.JSON
		, FormatFunc := { Terse: (info) => Format("{1} ⟹ {2}", info.expression, info.value), Verbose: (info) => SerializeFunc(info) }.Terse
		, funcName := A_ThisFunc
		, FindExpressionInLine := ((line) => RegExMatch(line, "^\s*" funcName " (.+)$", &mo) ? mo[1] : RegExMatch(line, "\b" funcName "\(((?:[^()]+|(?>\((?1)\)))+)\)", &mo) ? Substr(mo[0], StrLen(funcName) + 2, -1) : "Expression not found; line context = " line)
		, Cache := Map()
	err := Error(, -1)
	valStr := SerializeFunc(args), args.length ? valStr := SubStr(valStr, 2, -1) : 0
	if not Cache.Has(fileLineKey := err.File "//" err.Line)
		Cache.Set(fileLineKey, err.Stack)
	LogFunc(FormatFunc({ expression: FindExpressionInLine(Cache[fileLineKey]), value: valStr, file: err.File, lineNo: err.Line, lineText: Cache[fileLineKey] }))
	return args.length == 1 ? args[1] : args
}

cgx5871
Posts: 317
Joined: 26 Jul 2018, 14:02

Re: mm() a MsgBox alternative for debugging that shows not just the value but the source-code expression evaluated

Post by cgx5871 » 08 Jan 2024, 09:14

@bonobo
It would be perfect if the variable names could be displayed in alignment
In this way, it can be displayed neatly and intuitively.

Post Reply

Return to “Scripts and Functions (v2)”