Script execution made simple with SML/NJ ---------------------------------------- Submitted By: Dayanandan Natarajan Supervisor: Joe Wells School of Mathematical and Computer Sciences Heriot-Watt University Objective --------- Primary objective of this change is to give the programmers and developers the capability of running a SML program as a script. Also the capability to mute and unmute compiler messages. Programmers and developers can write a SML program and run the program like a script over the command prompt. Files modified -------------- smlnj/base/cm/main/cm-boot.sml smlnj/base/system/smlnj/internal/boot-env-fn.sml smlnj/base/compiler/TopLevel/interact/interact.sig smlnj/base/compiler/TopLevel/interact/interact.sml Change details -------------- 1. interact.sig & interact.sml A new function (useScriptFile) is added to Backend.Interact structure, which takes the file name and its content as a stream and process the stream by passing it to EvalLoop.evalStream. The compiler messages are muted and unmuted before the processing of the file. (Functions silenceCompiler, dummyfn and unsilenceCompiler will be explained later in this document) a) New function declaration is added to interact.sig, val useStream : TextIO.instream -> unit val useScriptFile : string * TextIO.instream -> unit (* Addded by DAYA *) val evalStream : TextIO.instream * Environment.environment -> Environment.environment b) New function definition is added to interact.sml, fun useScriptFile (fname, stream) = ( (EvalLoop.evalStream (fname, stream)) handle exn => ( EvalLoop.uncaughtExnMessage exn ) ) 2. cm-boot.sml The following changes were made in cm-boot.sml to recognise the new command line parameter passed from script. a) In function args, line added to recognise the new command-line parameter ‘--script’, and a new function ‘nextargscript’ is called to initiate the process of the file. fun args ([], _) = () | args ("-a" :: _, _) = nextarg autoload' | args ("-m" :: _, _) = nextarg make' | args (["-H"], _) = (help NONE; quit ()) | args ("-H" :: _ :: _, mk) = (help NONE; nextarg mk) | args (["-S"], _) = (showcur NONE; quit ()) | args ("-S" :: _ :: _, mk) = (showcur NONE; nextarg mk) | args (["-E"], _) = (show_envvars NONE; quit ()) | args ("-E" :: _ :: _, mk) = (show_envvars NONE; nextarg mk) | args ("--script" :: _, _) = (nextargscript ()) (* line added by DAYA *) | args ("@CMbuild" :: rest, _) = mlbuild rest | args (["@CMredump", heapfile], _) = redump_heap heapfile | args (f :: rest, mk) = (carg (String.substring (f, 0, 2) handle General.Subscript => "", f, mk, List.null rest); nextarg mk) and nextarg mk = let val l = SMLofNJ.getArgs () in SMLofNJ.shiftArgs (); args (l, mk) end (* nextargscript added by DAYA *) and nextargscript () = let val l = SMLofNJ.getArgs () in SMLofNJ.shiftArgs (); processFileScript (hd l); quit () end b) In function init(), the new function (useScriptFile) is added as one of the parameter passed, fun init (bootdir, de, er, useStream, useScriptFile, useFile, errorwrap, icm) = let c) In function procCmdLine (), new function processFileScript is added to process the script file, function will check for whether the file passed on is a script file starting with ‘#!’ thru another new function checkSharpbang, consumes the first line thru another new function eatuntilneline and pass the remaining content of the file to function useScriptFile. (* DAYA change starts here *) fun eatuntilnewline (instream : TextIO.instream): bool = let val c = TextIO.input1 instream in case TextIO.lookahead instream of SOME #"\n" => true | SOME c => eatuntilnewline instream | NONE => false end fun checkSharpbang (instream : TextIO.instream): bool = let val c = TextIO.input1 instream in case c of SOME #"#" => ( case TextIO.lookahead instream of SOME #"!" => eatuntilnewline instream | SOME c => false | NONE => false ) | SOME c => false | NONE => false end fun processFileScript (fname) = let val stream = TextIO.openIn fname val isscript = checkSharpbang stream in if (isscript) = false then ( Say.say [ "!* Script file doesn't start with #!. \n" ] ) else ( useScriptFile (fname, stream) ) end (* DAYA change ends here *) 3. boot-env-fn.sml In functor BootEnvF, cminit function declaration is amended to include the newly added function useScriptFile. functor BootEnvF (datatype envrequest = AUTOLOAD | BARE val architecture: string val cminit : string * DynamicEnv.env * envrequest * (TextIO.instream -> unit)(* useStream *) * (string * TextIO.instream -> unit) (* useScriptFile *) * (string -> unit) (* useFile *) * ((string -> unit) -> (string -> unit)) (* errorwrap *) * ({ manageImport: Ast.dec * EnvRef.envref -> unit, managePrint: Symbol.symbol * EnvRef.envref -> unit, getPending : unit -> Symbol.symbol list } -> unit) -> (unit -> unit) option val cmbmake: string * bool -> unit) :> BOOTENV = struct Writing a script ---------------- The script should start with ‘#!’ in the first line followed by the environment location, command-line parameters ‘-Ssml’ and ‘—script’, a new line and then followed by the SML code or program. Example script named ‘sample’, --------------beginning of the script------------- #!/usr/bin/env -Ssml –script ;(*-*-SML-*-*) val () = print "Hello World\n"; --------------end of the script--------------------- Running a script ---------------- The script ‘sample’ can be executed from Linux terminal or command prompt as a regular OS script as below provided it is given execution permission, $ ./sample Additional functions -------------------- a. The compiler messages can be muted/suppressed by invoking the silencecompiler function as below in the script, val _ = Backend.Mutecompiler.silenceCompiler (); b. The compiler messages can be unmuted by invoking the unsilencecompiler function as below in the script, val _ = Backend.Mutecompiler.unsilenceCompiler (); c. Whenever an error is encountered in compiling, by default only last 5 lines of the suppressed compiler messages are printed to the user and this limit can be pre-set in script as below, Backend.Mutecompiler.printlineLimit := 10; d. Whenever compiler messages are muted by calling silenceCompiler function, variable declarations are stashed with ‘#’ in suppressed compiler messages to save memory. To see the original content in case of debugging, this can be retrieved by amending the Control print parameters and restoring the print limits by increasing the string depth and calling the restorePrintingLimits function as below in the script, Control.Print.stringDepth := 999; val _ = Backend.Mutecompiler.restorePrintingLimits (); SML/NJ Version used ------------------- Our development and testing is based on Standard ML of New Jersey (32-bit) v110.99.3 on an Intel based macOS 10.13.16. Test Details ------------- Test Script #1 #!/usr/bin/env -Ssml --script ;(* SML code starts here *) val x = "Hello World x\n"; val () = print x; val y = "Hello World y\n"; val () = print y; val z = "Hello World z\n"; val () = print z; Test Result #1 $ ./exml07 Standard ML of New Jersey (32-bit) v110.99.3 [built: Mon Apr 10 18:03:19 2023] val x = "Hello World x\n" : string Hello World x val y = "Hello World y\n" : string Hello World y val z = "Hello World z\n" : string Hello World z $ Test Script #2 (sample script with forced error) #!/usr/bin/env -Ssml --script ;(*-*-SML-*-*) val x = "Hello World x\n"; val () = print x; val y = "Hello World y\n"; val () == print y; val z = "Hello World z\n"; val () = print z; Test Result #2 $ ./sample Standard ML of New Jersey (32-bit) v110.99.3 [built: Sat Apr 15 18:38:28 2023] val x = "Hello World x\n" : string Hello World x val y = "Hello World y\n" : string ./exml07:7.18-8.4 Error: syntax error: deleting SEMICOLON VAL uncaught exception Compile [Compile: "syntax error"] raised at: ../compiler/Parse/main/smlfile.sml:19.24-19.46 ../compiler/TopLevel/interact/evalloop.sml:45.54 ../compiler/TopLevel/interact/evalloop.sml:306.20-306.23