mirror of
				https://github.com/SquidDev-CC/CC-Tweaked
				synced 2025-10-31 05:33:00 +00:00 
			
		
		
		
	Cherry pick several changes back from 1.19.3
The main purpose of this is to backport the improved parse/runtime errors to older versions. I think they're sufficiently useful that we should try to make it as widely available as possible. We've been running them for a week now on SC3 and the released version and not seen any issues, so I think it's probably stable enough. This is a pretty lazy commit: I ended up copying the whole ROM over and then picking up a few other related changes along the way. - Trim spaces from file paths (b8fce1eecc) - Correctly format 12AM/PM with %I (9f48395596) - Fix http.request and htpt.websocketAsync not handling a few failure edge-cases correctly (3b42f22a4f). - Move the internal modules into the main package path, hidden under cc.internal (34a31abd9c). - Gather code coverage in Java instead of Lua (28a55349a9). - Make error messages in edit more obvious (8cfbfe7ceb). - Make mcfly's test methods global. This means we don't need to pass stub everywhere (7335a892b5). - Improve runtime and parse errors. This comes from numerous commits, but chieflya12b405acf, and5502412181. - Hide the internal redirect methods in multishell (33b6f38339). Note this does /not/ include the shebang changes (sorry Emma!). I've tried to avoid adding any user-controllable features, mostly because I don't know how to handle the versioning otherwise :).
This commit is contained in:
		| @@ -1,64 +0,0 @@ | ||||
| --- @module fs | ||||
|  | ||||
| --- Returns true if a path is mounted to the parent filesystem. | ||||
| -- | ||||
| -- The root filesystem "/" is considered a mount, along with disk folders and | ||||
| -- the rom folder. Other programs (such as network shares) can exstend this to | ||||
| -- make other mount types by correctly assigning their return value for getDrive. | ||||
| -- | ||||
| -- @tparam string path The path to check. | ||||
| -- @treturn boolean If the path is mounted, rather than a normal file/folder. | ||||
| -- @throws If the path does not exist. | ||||
| -- @see getDrive | ||||
| -- @since 1.87.0 | ||||
| function isDriveRoot(path) end | ||||
|  | ||||
| --[[- Provides completion for a file or directory name, suitable for use with | ||||
| @{_G.read}. | ||||
|  | ||||
| When a directory is a possible candidate for completion, two entries are | ||||
| included - one with a trailing slash (indicating that entries within this | ||||
| directory exist) and one without it (meaning this entry is an immediate | ||||
| completion candidate). `include_dirs` can be set to @{false} to only include | ||||
| those with a trailing slash. | ||||
|  | ||||
| @tparam[1] string path The path to complete. | ||||
| @tparam[1] string location The location where paths are resolved from. | ||||
| @tparam[1,opt=true] boolean include_files When @{false}, only directories will | ||||
| be included in the returned list. | ||||
| @tparam[1,opt=true] boolean include_dirs When @{false}, "raw" directories will | ||||
| not be included in the returned list. | ||||
|  | ||||
| @tparam[2] string path The path to complete. | ||||
| @tparam[2] string location The location where paths are resolved from. | ||||
| @tparam[2] { | ||||
|     include_dirs? = boolean, include_files? = boolean, | ||||
|     include_hidden? = boolean | ||||
| } options | ||||
| This table form is an expanded version of the previous syntax. The | ||||
| `include_files` and `include_dirs` arguments from above are passed in as fields. | ||||
|  | ||||
| This table also accepts the following options: | ||||
|  - `include_hidden`: Whether to include hidden files (those starting with `.`) | ||||
|    by default. They will still be shown when typing a `.`. | ||||
|  | ||||
| @treturn { string... } A list of possible completion candidates. | ||||
| @since 1.74 | ||||
| @changed 1.101.0 | ||||
| @usage Complete files in the root directory. | ||||
|  | ||||
|     read(nil, nil, function(str) | ||||
|         return fs.complete(str, "", true, false) | ||||
|     end) | ||||
|  | ||||
| @usage Complete files in the root directory, hiding hidden files by default. | ||||
|  | ||||
|     read(nil, nil, function(str) | ||||
|         return fs.complete(str, "", { | ||||
|             include_files = true, | ||||
|             include_dirs = false, | ||||
|             included_hidden = false, | ||||
|         }) | ||||
|     end) | ||||
| ]] | ||||
| function complete(path, location, include_files, include_dirs) end | ||||
| @@ -1,177 +0,0 @@ | ||||
| --- Make HTTP requests, sending and receiving data to a remote web server. | ||||
| -- | ||||
| -- @module http | ||||
| -- @since 1.1 | ||||
| -- @see local_ips To allow accessing servers running on your local network. | ||||
|  | ||||
| --- Asynchronously make a HTTP request to the given url. | ||||
| -- | ||||
| -- This returns immediately, a @{http_success} or @{http_failure} will be queued | ||||
| -- once the request has completed. | ||||
| -- | ||||
| -- @tparam      string url   The url to request | ||||
| -- @tparam[opt] string body  An optional string containing the body of the | ||||
| -- request. If specified, a `POST` request will be made instead. | ||||
| -- @tparam[opt] { [string] = string } headers Additional headers to send as part | ||||
| -- of this request. | ||||
| -- @tparam[opt] boolean binary Whether to make a binary HTTP request. If true, | ||||
| -- the body will not be UTF-8 encoded, and the received response will not be | ||||
| -- decoded. | ||||
| -- | ||||
| -- @tparam[2] { | ||||
| --   url = string, body? = string, headers? = { [string] = string }, | ||||
| --   binary? = boolean, method? = string, redirect? = boolean, | ||||
| -- } request Options for the request. | ||||
| -- | ||||
| -- This table form is an expanded version of the previous syntax. All arguments | ||||
| -- from above are passed in as fields instead (for instance, | ||||
| -- `http.request("https://example.com")` becomes `http.request { url = | ||||
| -- "https://example.com" }`). | ||||
| -- | ||||
| -- This table also accepts several additional options: | ||||
| -- | ||||
| --  - `method`: Which HTTP method to use, for instance `"PATCH"` or `"DELETE"`. | ||||
| --  - `redirect`: Whether to follow HTTP redirects. Defaults to true. | ||||
| -- | ||||
| -- @see http.get  For a synchronous way to make GET requests. | ||||
| -- @see http.post For a synchronous way to make POST requests. | ||||
| -- | ||||
| -- @changed 1.63 Added argument for headers. | ||||
| -- @changed 1.80pr1 Added argument for binary handles. | ||||
| -- @changed 1.80pr1.6 Added support for table argument. | ||||
| -- @changed 1.86.0 Added PATCH and TRACE methods. | ||||
| function request(...) end | ||||
|  | ||||
| --- Make a HTTP GET request to the given url. | ||||
| -- | ||||
| -- @tparam string url   The url to request | ||||
| -- @tparam[opt] { [string] = string } headers Additional headers to send as part | ||||
| -- of this request. | ||||
| -- @tparam[opt] boolean binary Whether to make a binary HTTP request. If true, | ||||
| -- the body will not be UTF-8 encoded, and the received response will not be | ||||
| -- decoded. | ||||
| -- | ||||
| -- @tparam[2] { | ||||
| --   url = string, headers? = { [string] = string }, | ||||
| --   binary? = boolean, method? = string, redirect? = boolean, | ||||
| -- } request Options for the request. See @{http.request} for details on how | ||||
| -- these options behave. | ||||
| -- | ||||
| -- @treturn Response The resulting http response, which can be read from. | ||||
| -- @treturn[2] nil When the http request failed, such as in the event of a 404 | ||||
| -- error or connection timeout. | ||||
| -- @treturn string A message detailing why the request failed. | ||||
| -- @treturn Response|nil The failing http response, if available. | ||||
| -- | ||||
| -- @changed 1.63 Added argument for headers. | ||||
| -- @changed 1.80pr1 Response handles are now returned on error if available. | ||||
| -- @changed 1.80pr1 Added argument for binary handles. | ||||
| -- @changed 1.80pr1.6 Added support for table argument. | ||||
| -- @changed 1.86.0 Added PATCH and TRACE methods. | ||||
| -- | ||||
| -- @usage Make a request to [example.tweaked.cc](https://example.tweaked.cc), | ||||
| -- and print the returned page. | ||||
| -- ```lua | ||||
| -- local request = http.get("https://example.tweaked.cc") | ||||
| -- print(request.readAll()) | ||||
| -- -- => HTTP is working! | ||||
| -- request.close() | ||||
| -- ``` | ||||
| function get(...) end | ||||
|  | ||||
| --- Make a HTTP POST request to the given url. | ||||
| -- | ||||
| -- @tparam string url   The url to request | ||||
| -- @tparam string body  The body of the POST request. | ||||
| -- @tparam[opt] { [string] = string } headers Additional headers to send as part | ||||
| -- of this request. | ||||
| -- @tparam[opt] boolean binary Whether to make a binary HTTP request. If true, | ||||
| -- the body will not be UTF-8 encoded, and the received response will not be | ||||
| -- decoded. | ||||
| -- | ||||
| -- @tparam[2] { | ||||
| --   url = string, body? = string, headers? = { [string] = string }, | ||||
| --   binary? = boolean, method? = string, redirect? = boolean, | ||||
| -- } request Options for the request. See @{http.request} for details on how | ||||
| -- these options behave. | ||||
| -- | ||||
| -- @treturn Response The resulting http response, which can be read from. | ||||
| -- @treturn[2] nil When the http request failed, such as in the event of a 404 | ||||
| -- error or connection timeout. | ||||
| -- @treturn string A message detailing why the request failed. | ||||
| -- @treturn Response|nil The failing http response, if available. | ||||
| -- | ||||
| -- @since 1.31 | ||||
| -- @changed 1.63 Added argument for headers. | ||||
| -- @changed 1.80pr1 Response handles are now returned on error if available. | ||||
| -- @changed 1.80pr1 Added argument for binary handles. | ||||
| -- @changed 1.80pr1.6 Added support for table argument. | ||||
| -- @changed 1.86.0 Added PATCH and TRACE methods. | ||||
| function post(...) end | ||||
|  | ||||
| --- Asynchronously determine whether a URL can be requested. | ||||
| -- | ||||
| -- If this returns `true`, one should also listen for @{http_check} which will | ||||
| -- container further information about whether the URL is allowed or not. | ||||
| -- | ||||
| -- @tparam string url The URL to check. | ||||
| -- @treturn true When this url is not invalid. This does not imply that it is | ||||
| -- allowed - see the comment above. | ||||
| -- @treturn[2] false When this url is invalid. | ||||
| -- @treturn string A reason why this URL is not valid (for instance, if it is | ||||
| -- malformed, or blocked). | ||||
| -- | ||||
| -- @see http.checkURL For a synchronous version. | ||||
| function checkURLAsync(url) end | ||||
|  | ||||
| --- Determine whether a URL can be requested. | ||||
| -- | ||||
| -- If this returns `true`, one should also listen for @{http_check} which will | ||||
| -- container further information about whether the URL is allowed or not. | ||||
| -- | ||||
| -- @tparam string url The URL to check. | ||||
| -- @treturn true When this url is valid and can be requested via @{http.request}. | ||||
| -- @treturn[2] false When this url is invalid. | ||||
| -- @treturn string A reason why this URL is not valid (for instance, if it is | ||||
| -- malformed, or blocked). | ||||
| -- | ||||
| -- @see http.checkURLAsync For an asynchronous version. | ||||
| -- | ||||
| -- @usage | ||||
| -- ```lua | ||||
| -- print(http.checkURL("https://example.tweaked.cc/")) | ||||
| -- -- => true | ||||
| -- print(http.checkURL("http://localhost/")) | ||||
| -- -- => false Domain not permitted | ||||
| -- print(http.checkURL("not a url")) | ||||
| -- -- => false URL malformed | ||||
| -- ``` | ||||
| function checkURL(url) end | ||||
|  | ||||
| --- Open a websocket. | ||||
| -- | ||||
| -- @tparam string url The websocket url to connect to. This should have the | ||||
| -- `ws://` or `wss://` protocol. | ||||
| -- @tparam[opt] { [string] = string } headers Additional headers to send as part | ||||
| -- of the initial websocket connection. | ||||
| -- | ||||
| -- @treturn Websocket The websocket connection. | ||||
| -- @treturn[2] false If the websocket connection failed. | ||||
| -- @treturn string An error message describing why the connection failed. | ||||
| -- @since 1.80pr1.1 | ||||
| -- @changed 1.80pr1.3 No longer asynchronous. | ||||
| -- @changed 1.95.3 Added User-Agent to default headers. | ||||
| function websocket(url, headers) end | ||||
|  | ||||
| --- Asynchronously open a websocket. | ||||
| -- | ||||
| -- This returns immediately, a @{websocket_success} or @{websocket_failure} | ||||
| -- will be queued once the request has completed. | ||||
| -- | ||||
| -- @tparam string url The websocket url to connect to. This should have the | ||||
| -- `ws://` or `wss://` protocol. | ||||
| -- @tparam[opt] { [string] = string } headers Additional headers to send as part | ||||
| -- of the initial websocket connection. | ||||
| -- @since 1.80pr1.3 | ||||
| -- @changed 1.95.3 Added User-Agent to default headers. | ||||
| function websocketAsync(url, headers) end | ||||
| @@ -7,7 +7,7 @@ parchment = "2021.08.08" | ||||
| parchmentMc = "1.16.5" | ||||
|  | ||||
| autoService = "1.0.1" | ||||
| cobalt = "0.5.12" | ||||
| cobalt = "0.6.0" | ||||
| jetbrainsAnnotations = "23.0.0" | ||||
| kotlin = "1.7.10" | ||||
| kotlin-coroutines = "1.6.0" | ||||
| @@ -23,7 +23,7 @@ checkstyle = "8.25" # There's a reason we're pinned on an ancient version, but I | ||||
| curseForgeGradle = "1.0.11" | ||||
| forgeGradle = "5.1.+" | ||||
| githubRelease = "2.2.12" | ||||
| illuaminate = "0.1.0-7-g2a5a89c" | ||||
| illuaminate = "0.1.0-20-g8c483a4" | ||||
| librarian = "1.+" | ||||
| minotaur = "2.+" | ||||
| mixinGradle = "0.7.+" | ||||
|   | ||||
| @@ -111,6 +111,6 @@ | ||||
|   (lint | ||||
|     (globals | ||||
|       :max sleep write | ||||
|       cct_test describe expect howlci fail it pending stub))) | ||||
|       cct_test describe expect howlci fail it pending stub before_each))) | ||||
|  | ||||
| (at /src/web/mount/expr_template.lua (lint (globals :max __expr__))) | ||||
|   | ||||
| @@ -89,7 +89,7 @@ final class LuaDateTime | ||||
|                             formatter.appendValue( ChronoField.HOUR_OF_DAY, 2 ); | ||||
|                             break; | ||||
|                         case 'I': | ||||
|                             formatter.appendValue( ChronoField.HOUR_OF_AMPM, 2 ); | ||||
|                             formatter.appendValue( ChronoField.CLOCK_HOUR_OF_AMPM, 2 ); | ||||
|                             break; | ||||
|                         case 'j': | ||||
|                             formatter.appendValue( ChronoField.DAY_OF_YEAR, 3 ); | ||||
|   | ||||
| @@ -5,6 +5,7 @@ | ||||
|  */ | ||||
| package dan200.computercraft.core.filesystem; | ||||
| 
 | ||||
| import com.google.common.base.Splitter; | ||||
| import com.google.common.io.ByteStreams; | ||||
| import dan200.computercraft.ComputerCraft; | ||||
| import dan200.computercraft.api.filesystem.IFileSystem; | ||||
| @@ -515,10 +516,11 @@ public class FileSystem | ||||
|         path = cleanName.toString(); | ||||
| 
 | ||||
|         // Collapse the string into its component parts, removing ..'s | ||||
|         String[] parts = path.split( "/" ); | ||||
|         Stack<String> outputParts = new Stack<>(); | ||||
|         for( String part : parts ) | ||||
|         ArrayDeque<String> outputParts = new ArrayDeque<>(); | ||||
|         for( String fullPart : Splitter.on( '/' ).split( path ) ) | ||||
|         { | ||||
|             String part = fullPart.trim(); | ||||
| 
 | ||||
|             if( part.isEmpty() || part.equals( "." ) || threeDotsPattern.matcher( part ).matches() ) | ||||
|             { | ||||
|                 // . is redundant | ||||
| @@ -529,32 +531,32 @@ public class FileSystem | ||||
|             if( part.equals( ".." ) ) | ||||
|             { | ||||
|                 // .. can cancel out the last folder entered | ||||
|                 if( !outputParts.empty() ) | ||||
|                 if( !outputParts.isEmpty() ) | ||||
|                 { | ||||
|                     String top = outputParts.peek(); | ||||
|                     String top = outputParts.peekLast(); | ||||
|                     if( !top.equals( ".." ) ) | ||||
|                     { | ||||
|                         outputParts.pop(); | ||||
|                         outputParts.removeLast(); | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         outputParts.push( ".." ); | ||||
|                         outputParts.addLast( ".." ); | ||||
|                     } | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     outputParts.push( ".." ); | ||||
|                     outputParts.addLast( ".." ); | ||||
|                 } | ||||
|             } | ||||
|             else if( part.length() >= 255 ) | ||||
|             { | ||||
|                 // If part length > 255 and it is the last part | ||||
|                 outputParts.push( part.substring( 0, 255 ) ); | ||||
|                 outputParts.addLast( part.substring( 0, 255 ).trim() ); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 // Anything else we add to the stack | ||||
|                 outputParts.push( part ); | ||||
|                 outputParts.addLast( part ); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|   | ||||
| @@ -3,16 +3,15 @@ | ||||
| -- Ideally we'd use require, but that is part of the shell, and so is not | ||||
| -- available to the BIOS or any APIs. All APIs load this using dofile, but that | ||||
| -- has not been defined at this point. | ||||
| local expect, field | ||||
| local expect | ||||
|  | ||||
| do | ||||
|     local h = fs.open("rom/modules/main/cc/expect.lua", "r") | ||||
|     local f, err = loadstring(h.readAll(), "@expect.lua") | ||||
|     local f, err = loadstring(h.readAll(), "@/rom/modules/main/cc/expect.lua") | ||||
|     h.close() | ||||
|  | ||||
|     if not f then error(err) end | ||||
|     local res = f() | ||||
|     expect, field = res.expect, res.field | ||||
|     expect = f().expect | ||||
| end | ||||
|  | ||||
| if _VERSION == "Lua 5.1" then | ||||
| @@ -468,7 +467,7 @@ function loadfile(filename, mode, env) | ||||
|     local file = fs.open(filename, "r") | ||||
|     if not file then return nil, "File not found" end | ||||
|  | ||||
|     local func, err = load(file.readAll(), "@" .. fs.getName(filename), mode, env) | ||||
|     local func, err = load(file.readAll(), "@/" .. fs.combine(filename), mode, env) | ||||
|     file.close() | ||||
|     return func, err | ||||
| end | ||||
| @@ -584,257 +583,28 @@ function os.reboot() | ||||
|     end | ||||
| end | ||||
|  | ||||
| -- Install the lua part of the HTTP api (if enabled) | ||||
| if http then | ||||
|     local nativeHTTPRequest = http.request | ||||
| local bAPIError = false | ||||
|  | ||||
|     local methods = { | ||||
|         GET = true, POST = true, HEAD = true, | ||||
|         OPTIONS = true, PUT = true, DELETE = true, | ||||
|         PATCH = true, TRACE = true, | ||||
|     } | ||||
| local function load_apis(dir) | ||||
|     if not fs.isDir(dir) then return end | ||||
|  | ||||
|     local function checkKey(options, key, ty, opt) | ||||
|         local value = options[key] | ||||
|         local valueTy = type(value) | ||||
|  | ||||
|         if (value ~= nil or not opt) and valueTy ~= ty then | ||||
|             error(("bad field '%s' (expected %s, got %s"):format(key, ty, valueTy), 4) | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     local function checkOptions(options, body) | ||||
|         checkKey(options, "url", "string") | ||||
|         if body == false then | ||||
|           checkKey(options, "body", "nil") | ||||
|         else | ||||
|           checkKey(options, "body", "string", not body) | ||||
|         end | ||||
|         checkKey(options, "headers", "table", true) | ||||
|         checkKey(options, "method", "string", true) | ||||
|         checkKey(options, "redirect", "boolean", true) | ||||
|  | ||||
|         if options.method and not methods[options.method] then | ||||
|             error("Unsupported HTTP method", 3) | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     local function wrapRequest(_url, ...) | ||||
|         local ok, err = nativeHTTPRequest(...) | ||||
|         if ok then | ||||
|             while true do | ||||
|                 local event, param1, param2, param3 = os.pullEvent() | ||||
|                 if event == "http_success" and param1 == _url then | ||||
|                     return param2 | ||||
|                 elseif event == "http_failure" and param1 == _url then | ||||
|                     return nil, param2, param3 | ||||
|                 end | ||||
|             end | ||||
|         end | ||||
|         return nil, err | ||||
|     end | ||||
|  | ||||
|     http.get = function(_url, _headers, _binary) | ||||
|         if type(_url) == "table" then | ||||
|             checkOptions(_url, false) | ||||
|             return wrapRequest(_url.url, _url) | ||||
|         end | ||||
|  | ||||
|         expect(1, _url, "string") | ||||
|         expect(2, _headers, "table", "nil") | ||||
|         expect(3, _binary, "boolean", "nil") | ||||
|         return wrapRequest(_url, _url, nil, _headers, _binary) | ||||
|     end | ||||
|  | ||||
|     http.post = function(_url, _post, _headers, _binary) | ||||
|         if type(_url) == "table" then | ||||
|             checkOptions(_url, true) | ||||
|             return wrapRequest(_url.url, _url) | ||||
|         end | ||||
|  | ||||
|         expect(1, _url, "string") | ||||
|         expect(2, _post, "string") | ||||
|         expect(3, _headers, "table", "nil") | ||||
|         expect(4, _binary, "boolean", "nil") | ||||
|         return wrapRequest(_url, _url, _post, _headers, _binary) | ||||
|     end | ||||
|  | ||||
|     http.request = function(_url, _post, _headers, _binary) | ||||
|         local url | ||||
|         if type(_url) == "table" then | ||||
|             checkOptions(_url) | ||||
|             url = _url.url | ||||
|         else | ||||
|             expect(1, _url, "string") | ||||
|             expect(2, _post, "string", "nil") | ||||
|             expect(3, _headers, "table", "nil") | ||||
|             expect(4, _binary, "boolean", "nil") | ||||
|             url = _url.url | ||||
|         end | ||||
|  | ||||
|         local ok, err = nativeHTTPRequest(_url, _post, _headers, _binary) | ||||
|         if not ok then | ||||
|             os.queueEvent("http_failure", url, err) | ||||
|         end | ||||
|         return ok, err | ||||
|     end | ||||
|  | ||||
|     local nativeCheckURL = http.checkURL | ||||
|     http.checkURLAsync = nativeCheckURL | ||||
|     http.checkURL = function(_url) | ||||
|         expect(1, _url, "string") | ||||
|         local ok, err = nativeCheckURL(_url) | ||||
|         if not ok then return ok, err end | ||||
|  | ||||
|         while true do | ||||
|             local _, url, ok, err = os.pullEvent("http_check") | ||||
|             if url == _url then return ok, err end | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     local nativeWebsocket = http.websocket | ||||
|     http.websocketAsync = nativeWebsocket | ||||
|     http.websocket = function(_url, _headers) | ||||
|         expect(1, _url, "string") | ||||
|         expect(2, _headers, "table", "nil") | ||||
|  | ||||
|         local ok, err = nativeWebsocket(_url, _headers) | ||||
|         if not ok then return ok, err end | ||||
|  | ||||
|         while true do | ||||
|             local event, url, param = os.pullEvent( ) | ||||
|             if event == "websocket_success" and url == _url then | ||||
|                 return param | ||||
|             elseif event == "websocket_failure" and url == _url then | ||||
|                 return false, param | ||||
|     for _, file in ipairs(fs.list(dir)) do | ||||
|         if file:sub(1, 1) ~= "." then | ||||
|             local path = fs.combine(dir, file) | ||||
|             if not fs.isDir(path) then | ||||
|                 if not os.loadAPI(path) then | ||||
|                     bAPIError = true | ||||
|                 end | ||||
|             end | ||||
|         end | ||||
|     end | ||||
|  | ||||
| -- Install the lua part of the FS api | ||||
| local tEmpty = {} | ||||
| function fs.complete(sPath, sLocation, bIncludeFiles, bIncludeDirs) | ||||
|     expect(1, sPath, "string") | ||||
|     expect(2, sLocation, "string") | ||||
|     local bIncludeHidden = nil | ||||
|     if type(bIncludeFiles) == "table" then | ||||
|         bIncludeDirs = field(bIncludeFiles, "include_dirs", "boolean", "nil") | ||||
|         bIncludeHidden = field(bIncludeFiles, "include_hidden", "boolean", "nil") | ||||
|         bIncludeFiles = field(bIncludeFiles, "include_files", "boolean", "nil") | ||||
|     else | ||||
|         expect(3, bIncludeFiles, "boolean", "nil") | ||||
|         expect(4, bIncludeDirs, "boolean", "nil") | ||||
|     end | ||||
|  | ||||
|     bIncludeHidden = bIncludeHidden ~= false | ||||
|     bIncludeFiles = bIncludeFiles ~= false | ||||
|     bIncludeDirs = bIncludeDirs ~= false | ||||
|     local sDir = sLocation | ||||
|     local nStart = 1 | ||||
|     local nSlash = string.find(sPath, "[/\\]", nStart) | ||||
|     if nSlash == 1 then | ||||
|         sDir = "" | ||||
|         nStart = 2 | ||||
|     end | ||||
|     local sName | ||||
|     while not sName do | ||||
|         local nSlash = string.find(sPath, "[/\\]", nStart) | ||||
|         if nSlash then | ||||
|             local sPart = string.sub(sPath, nStart, nSlash - 1) | ||||
|             sDir = fs.combine(sDir, sPart) | ||||
|             nStart = nSlash + 1 | ||||
|         else | ||||
|             sName = string.sub(sPath, nStart) | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     if fs.isDir(sDir) then | ||||
|         local tResults = {} | ||||
|         if bIncludeDirs and sPath == "" then | ||||
|             table.insert(tResults, ".") | ||||
|         end | ||||
|         if sDir ~= "" then | ||||
|             if sPath == "" then | ||||
|                 table.insert(tResults, bIncludeDirs and ".." or "../") | ||||
|             elseif sPath == "." then | ||||
|                 table.insert(tResults, bIncludeDirs and "." or "./") | ||||
|             end | ||||
|         end | ||||
|         local tFiles = fs.list(sDir) | ||||
|         for n = 1, #tFiles do | ||||
|             local sFile = tFiles[n] | ||||
|             if #sFile >= #sName and string.sub(sFile, 1, #sName) == sName and ( | ||||
|                 bIncludeHidden or sFile:sub(1, 1) ~= "." or sName:sub(1, 1) == "." | ||||
|             ) then | ||||
|                 local bIsDir = fs.isDir(fs.combine(sDir, sFile)) | ||||
|                 local sResult = string.sub(sFile, #sName + 1) | ||||
|                 if bIsDir then | ||||
|                     table.insert(tResults, sResult .. "/") | ||||
|                     if bIncludeDirs and #sResult > 0 then | ||||
|                         table.insert(tResults, sResult) | ||||
|                     end | ||||
|                 else | ||||
|                     if bIncludeFiles and #sResult > 0 then | ||||
|                         table.insert(tResults, sResult) | ||||
|                     end | ||||
|                 end | ||||
|             end | ||||
|         end | ||||
|         return tResults | ||||
|     end | ||||
|     return tEmpty | ||||
| end | ||||
|  | ||||
| function fs.isDriveRoot(sPath) | ||||
|     expect(1, sPath, "string") | ||||
|     -- Force the root directory to be a mount. | ||||
|     return fs.getDir(sPath) == ".." or fs.getDrive(sPath) ~= fs.getDrive(fs.getDir(sPath)) | ||||
| end | ||||
|  | ||||
| -- Load APIs | ||||
| local bAPIError = false | ||||
| local tApis = fs.list("rom/apis") | ||||
| for _, sFile in ipairs(tApis) do | ||||
|     if string.sub(sFile, 1, 1) ~= "." then | ||||
|         local sPath = fs.combine("rom/apis", sFile) | ||||
|         if not fs.isDir(sPath) then | ||||
|             if not os.loadAPI(sPath) then | ||||
|                 bAPIError = true | ||||
|             end | ||||
|         end | ||||
|     end | ||||
| end | ||||
|  | ||||
| if turtle and fs.isDir("rom/apis/turtle") then | ||||
|     -- Load turtle APIs | ||||
|     local tApis = fs.list("rom/apis/turtle") | ||||
|     for _, sFile in ipairs(tApis) do | ||||
|         if string.sub(sFile, 1, 1) ~= "." then | ||||
|             local sPath = fs.combine("rom/apis/turtle", sFile) | ||||
|             if not fs.isDir(sPath) then | ||||
|                 if not os.loadAPI(sPath) then | ||||
|                     bAPIError = true | ||||
|                 end | ||||
|             end | ||||
|         end | ||||
|     end | ||||
| end | ||||
|  | ||||
| if pocket and fs.isDir("rom/apis/pocket") then | ||||
|     -- Load pocket APIs | ||||
|     local tApis = fs.list("rom/apis/pocket") | ||||
|     for _, sFile in ipairs(tApis) do | ||||
|         if string.sub(sFile, 1, 1) ~= "." then | ||||
|             local sPath = fs.combine("rom/apis/pocket", sFile) | ||||
|             if not fs.isDir(sPath) then | ||||
|                 if not os.loadAPI(sPath) then | ||||
|                     bAPIError = true | ||||
|                 end | ||||
|             end | ||||
|         end | ||||
|     end | ||||
| end | ||||
| load_apis("rom/apis") | ||||
| if http then load_apis("rom/apis/http") end | ||||
| if turtle then load_apis("rom/apis/turtle") end | ||||
| if pocket then load_apis("rom/apis/pocket") end | ||||
|  | ||||
| if commands and fs.isDir("rom/apis/command") then | ||||
|     -- Load command APIs | ||||
| @@ -930,7 +700,7 @@ settings.define("motd.path", { | ||||
|  | ||||
| settings.define("lua.warn_against_use_of_local", { | ||||
|     default = true, | ||||
|     description = [[Print a message when input in the Lua REPL starts with the word 'local'. Local variables defined in the Lua REPL are be inaccessable on the next input.]], | ||||
|     description = [[Print a message when input in the Lua REPL starts with the word 'local'. Local variables defined in the Lua REPL are be inaccessible on the next input.]], | ||||
|     type = "boolean", | ||||
| }) | ||||
| settings.define("lua.function_args", { | ||||
|   | ||||
| @@ -269,7 +269,7 @@ end | ||||
| --- Combine a three-colour RGB value into one hexadecimal representation. | ||||
| -- | ||||
| -- @tparam number r The red channel, should be between 0 and 1. | ||||
| -- @tparam number g The green channel, should be between 0 and 1. | ||||
| -- @tparam number g The red channel, should be between 0 and 1. | ||||
| -- @tparam number b The blue channel, should be between 0 and 1. | ||||
| -- @treturn number The combined hexadecimal colour. | ||||
| -- @usage | ||||
| @@ -292,7 +292,7 @@ end | ||||
| -- | ||||
| -- @tparam number rgb The combined hexadecimal colour. | ||||
| -- @treturn number The red channel, will be between 0 and 1. | ||||
| -- @treturn number The green channel, will be between 0 and 1. | ||||
| -- @treturn number The red channel, will be between 0 and 1. | ||||
| -- @treturn number The blue channel, will be between 0 and 1. | ||||
| -- @usage | ||||
| -- ```lua | ||||
|   | ||||
							
								
								
									
										147
									
								
								src/main/resources/data/computercraft/lua/rom/apis/fs.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								src/main/resources/data/computercraft/lua/rom/apis/fs.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,147 @@ | ||||
| --- @module fs | ||||
|  | ||||
| local expect = dofile("rom/modules/main/cc/expect.lua") | ||||
| local expect, field = expect.expect, expect.field | ||||
|  | ||||
| local native = fs | ||||
|  | ||||
| local fs = _ENV | ||||
| for k, v in pairs(native) do fs[k] = v end | ||||
|  | ||||
| --[[- Provides completion for a file or directory name, suitable for use with | ||||
| @{_G.read}. | ||||
|  | ||||
| When a directory is a possible candidate for completion, two entries are | ||||
| included - one with a trailing slash (indicating that entries within this | ||||
| directory exist) and one without it (meaning this entry is an immediate | ||||
| completion candidate). `include_dirs` can be set to @{false} to only include | ||||
| those with a trailing slash. | ||||
|  | ||||
| @tparam[1] string path The path to complete. | ||||
| @tparam[1] string location The location where paths are resolved from. | ||||
| @tparam[1,opt=true] boolean include_files When @{false}, only directories will | ||||
| be included in the returned list. | ||||
| @tparam[1,opt=true] boolean include_dirs When @{false}, "raw" directories will | ||||
| not be included in the returned list. | ||||
|  | ||||
| @tparam[2] string path The path to complete. | ||||
| @tparam[2] string location The location where paths are resolved from. | ||||
| @tparam[2] { | ||||
|     include_dirs? = boolean, include_files? = boolean, | ||||
|     include_hidden? = boolean | ||||
| } options | ||||
| This table form is an expanded version of the previous syntax. The | ||||
| `include_files` and `include_dirs` arguments from above are passed in as fields. | ||||
|  | ||||
| This table also accepts the following options: | ||||
|  - `include_hidden`: Whether to include hidden files (those starting with `.`) | ||||
|    by default. They will still be shown when typing a `.`. | ||||
|  | ||||
| @treturn { string... } A list of possible completion candidates. | ||||
| @since 1.74 | ||||
| @changed 1.101.0 | ||||
| @usage Complete files in the root directory. | ||||
|  | ||||
|     read(nil, nil, function(str) | ||||
|         return fs.complete(str, "", true, false) | ||||
|     end) | ||||
|  | ||||
| @usage Complete files in the root directory, hiding hidden files by default. | ||||
|  | ||||
|     read(nil, nil, function(str) | ||||
|         return fs.complete(str, "", { | ||||
|             include_files = true, | ||||
|             include_dirs = false, | ||||
|             include_hidden = false, | ||||
|         }) | ||||
|     end) | ||||
| ]] | ||||
| function fs.complete(sPath, sLocation, bIncludeFiles, bIncludeDirs) | ||||
|     expect(1, sPath, "string") | ||||
|     expect(2, sLocation, "string") | ||||
|     local bIncludeHidden = nil | ||||
|     if type(bIncludeFiles) == "table" then | ||||
|         bIncludeDirs = field(bIncludeFiles, "include_dirs", "boolean", "nil") | ||||
|         bIncludeHidden = field(bIncludeFiles, "include_hidden", "boolean", "nil") | ||||
|         bIncludeFiles = field(bIncludeFiles, "include_files", "boolean", "nil") | ||||
|     else | ||||
|         expect(3, bIncludeFiles, "boolean", "nil") | ||||
|         expect(4, bIncludeDirs, "boolean", "nil") | ||||
|     end | ||||
|  | ||||
|     bIncludeHidden = bIncludeHidden ~= false | ||||
|     bIncludeFiles = bIncludeFiles ~= false | ||||
|     bIncludeDirs = bIncludeDirs ~= false | ||||
|     local sDir = sLocation | ||||
|     local nStart = 1 | ||||
|     local nSlash = string.find(sPath, "[/\\]", nStart) | ||||
|     if nSlash == 1 then | ||||
|         sDir = "" | ||||
|         nStart = 2 | ||||
|     end | ||||
|     local sName | ||||
|     while not sName do | ||||
|         local nSlash = string.find(sPath, "[/\\]", nStart) | ||||
|         if nSlash then | ||||
|             local sPart = string.sub(sPath, nStart, nSlash - 1) | ||||
|             sDir = fs.combine(sDir, sPart) | ||||
|             nStart = nSlash + 1 | ||||
|         else | ||||
|             sName = string.sub(sPath, nStart) | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     if fs.isDir(sDir) then | ||||
|         local tResults = {} | ||||
|         if bIncludeDirs and sPath == "" then | ||||
|             table.insert(tResults, ".") | ||||
|         end | ||||
|         if sDir ~= "" then | ||||
|             if sPath == "" then | ||||
|                 table.insert(tResults, bIncludeDirs and ".." or "../") | ||||
|             elseif sPath == "." then | ||||
|                 table.insert(tResults, bIncludeDirs and "." or "./") | ||||
|             end | ||||
|         end | ||||
|         local tFiles = fs.list(sDir) | ||||
|         for n = 1, #tFiles do | ||||
|             local sFile = tFiles[n] | ||||
|             if #sFile >= #sName and string.sub(sFile, 1, #sName) == sName and ( | ||||
|                 bIncludeHidden or sFile:sub(1, 1) ~= "." or sName:sub(1, 1) == "." | ||||
|             ) then | ||||
|                 local bIsDir = fs.isDir(fs.combine(sDir, sFile)) | ||||
|                 local sResult = string.sub(sFile, #sName + 1) | ||||
|                 if bIsDir then | ||||
|                     table.insert(tResults, sResult .. "/") | ||||
|                     if bIncludeDirs and #sResult > 0 then | ||||
|                         table.insert(tResults, sResult) | ||||
|                     end | ||||
|                 else | ||||
|                     if bIncludeFiles and #sResult > 0 then | ||||
|                         table.insert(tResults, sResult) | ||||
|                     end | ||||
|                 end | ||||
|             end | ||||
|         end | ||||
|         return tResults | ||||
|     end | ||||
|  | ||||
|     return {} | ||||
| end | ||||
|  | ||||
| --- Returns true if a path is mounted to the parent filesystem. | ||||
| -- | ||||
| -- The root filesystem "/" is considered a mount, along with disk folders and | ||||
| -- the rom folder. Other programs (such as network shares) can exstend this to | ||||
| -- make other mount types by correctly assigning their return value for getDrive. | ||||
| -- | ||||
| -- @tparam string path The path to check. | ||||
| -- @treturn boolean If the path is mounted, rather than a normal file/folder. | ||||
| -- @throws If the path does not exist. | ||||
| -- @see getDrive | ||||
| -- @since 1.87.0 | ||||
| function fs.isDriveRoot(sPath) | ||||
|     expect(1, sPath, "string") | ||||
|     -- Force the root directory to be a mount. | ||||
|     return fs.getDir(sPath) == ".." or fs.getDrive(sPath) ~= fs.getDrive(fs.getDir(sPath)) | ||||
| end | ||||
| @@ -16,7 +16,7 @@ function path() | ||||
|     return sPath | ||||
| end | ||||
|  | ||||
| --- Sets the colon-seperated list of directories where help files are searched | ||||
| --- Sets the colon-separated list of directories where help files are searched | ||||
| -- for to `newPath` | ||||
| -- | ||||
| -- @tparam string newPath The new path to use. | ||||
|   | ||||
							
								
								
									
										317
									
								
								src/main/resources/data/computercraft/lua/rom/apis/http/http.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										317
									
								
								src/main/resources/data/computercraft/lua/rom/apis/http/http.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,317 @@ | ||||
| --[[- Make HTTP requests, sending and receiving data to a remote web server. | ||||
|  | ||||
| @module http | ||||
| @since 1.1 | ||||
| @see local_ips To allow accessing servers running on your local network. | ||||
| ]] | ||||
|  | ||||
| local expect = dofile("rom/modules/main/cc/expect.lua").expect | ||||
|  | ||||
| local native = http | ||||
| local nativeHTTPRequest = http.request | ||||
|  | ||||
| local methods = { | ||||
|     GET = true, POST = true, HEAD = true, | ||||
|     OPTIONS = true, PUT = true, DELETE = true, | ||||
|     PATCH = true, TRACE = true, | ||||
| } | ||||
|  | ||||
| local function checkKey(options, key, ty, opt) | ||||
|     local value = options[key] | ||||
|     local valueTy = type(value) | ||||
|  | ||||
|     if (value ~= nil or not opt) and valueTy ~= ty then | ||||
|         error(("bad field '%s' (expected %s, got %s"):format(key, ty, valueTy), 4) | ||||
|     end | ||||
| end | ||||
|  | ||||
| local function checkOptions(options, body) | ||||
|     checkKey(options, "url", "string") | ||||
|     if body == false then | ||||
|         checkKey(options, "body", "nil") | ||||
|     else | ||||
|         checkKey(options, "body", "string", not body) | ||||
|     end | ||||
|     checkKey(options, "headers", "table", true) | ||||
|     checkKey(options, "method", "string", true) | ||||
|     checkKey(options, "redirect", "boolean", true) | ||||
|  | ||||
|     if options.method and not methods[options.method] then | ||||
|         error("Unsupported HTTP method", 3) | ||||
|     end | ||||
| end | ||||
|  | ||||
| local function wrapRequest(_url, ...) | ||||
|     local ok, err = nativeHTTPRequest(...) | ||||
|     if ok then | ||||
|         while true do | ||||
|             local event, param1, param2, param3 = os.pullEvent() | ||||
|             if event == "http_success" and param1 == _url then | ||||
|                 return param2 | ||||
|             elseif event == "http_failure" and param1 == _url then | ||||
|                 return nil, param2, param3 | ||||
|             end | ||||
|         end | ||||
|     end | ||||
|     return nil, err | ||||
| end | ||||
|  | ||||
| --[[- Make a HTTP GET request to the given url. | ||||
|  | ||||
| @tparam string url   The url to request | ||||
| @tparam[opt] { [string] = string } headers Additional headers to send as part | ||||
| of this request. | ||||
| @tparam[opt] boolean binary Whether to make a binary HTTP request. If true, | ||||
| the body will not be UTF-8 encoded, and the received response will not be | ||||
| decoded. | ||||
|  | ||||
| @tparam[2] { | ||||
|   url = string, headers? = { [string] = string }, | ||||
|   binary? = boolean, method? = string, redirect? = boolean, | ||||
| } request Options for the request. See @{http.request} for details on how | ||||
| these options behave. | ||||
|  | ||||
| @treturn Response The resulting http response, which can be read from. | ||||
| @treturn[2] nil When the http request failed, such as in the event of a 404 | ||||
| error or connection timeout. | ||||
| @treturn string A message detailing why the request failed. | ||||
| @treturn Response|nil The failing http response, if available. | ||||
|  | ||||
| @changed 1.63 Added argument for headers. | ||||
| @changed 1.80pr1 Response handles are now returned on error if available. | ||||
| @changed 1.80pr1 Added argument for binary handles. | ||||
| @changed 1.80pr1.6 Added support for table argument. | ||||
| @changed 1.86.0 Added PATCH and TRACE methods. | ||||
|  | ||||
| @usage Make a request to [example.tweaked.cc](https://example.tweaked.cc), | ||||
| and print the returned page. | ||||
|  | ||||
| ```lua | ||||
| local request = http.get("https://example.tweaked.cc") | ||||
| print(request.readAll()) | ||||
| -- => HTTP is working! | ||||
| request.close() | ||||
| ``` | ||||
| ]] | ||||
| function get(_url, _headers, _binary) | ||||
|     if type(_url) == "table" then | ||||
|         checkOptions(_url, false) | ||||
|         return wrapRequest(_url.url, _url) | ||||
|     end | ||||
|  | ||||
|     expect(1, _url, "string") | ||||
|     expect(2, _headers, "table", "nil") | ||||
|     expect(3, _binary, "boolean", "nil") | ||||
|     return wrapRequest(_url, _url, nil, _headers, _binary) | ||||
| end | ||||
|  | ||||
| --[[- Make a HTTP POST request to the given url. | ||||
|  | ||||
| @tparam string url   The url to request | ||||
| @tparam string body  The body of the POST request. | ||||
| @tparam[opt] { [string] = string } headers Additional headers to send as part | ||||
| of this request. | ||||
| @tparam[opt] boolean binary Whether to make a binary HTTP request. If true, | ||||
| the body will not be UTF-8 encoded, and the received response will not be | ||||
| decoded. | ||||
|  | ||||
| @tparam[2] { | ||||
|   url = string, body? = string, headers? = { [string] = string }, | ||||
|   binary? = boolean, method? = string, redirect? = boolean, | ||||
| } request Options for the request. See @{http.request} for details on how | ||||
| these options behave. | ||||
|  | ||||
| @treturn Response The resulting http response, which can be read from. | ||||
| @treturn[2] nil When the http request failed, such as in the event of a 404 | ||||
| error or connection timeout. | ||||
| @treturn string A message detailing why the request failed. | ||||
| @treturn Response|nil The failing http response, if available. | ||||
|  | ||||
| @since 1.31 | ||||
| @changed 1.63 Added argument for headers. | ||||
| @changed 1.80pr1 Response handles are now returned on error if available. | ||||
| @changed 1.80pr1 Added argument for binary handles. | ||||
| @changed 1.80pr1.6 Added support for table argument. | ||||
| @changed 1.86.0 Added PATCH and TRACE methods. | ||||
| ]] | ||||
| function post(_url, _post, _headers, _binary) | ||||
|     if type(_url) == "table" then | ||||
|         checkOptions(_url, true) | ||||
|         return wrapRequest(_url.url, _url) | ||||
|     end | ||||
|  | ||||
|     expect(1, _url, "string") | ||||
|     expect(2, _post, "string") | ||||
|     expect(3, _headers, "table", "nil") | ||||
|     expect(4, _binary, "boolean", "nil") | ||||
|     return wrapRequest(_url, _url, _post, _headers, _binary) | ||||
| end | ||||
|  | ||||
| --[[- Asynchronously make a HTTP request to the given url. | ||||
|  | ||||
| This returns immediately, a @{http_success} or @{http_failure} will be queued | ||||
| once the request has completed. | ||||
|  | ||||
| @tparam      string url   The url to request | ||||
| @tparam[opt] string body  An optional string containing the body of the | ||||
| request. If specified, a `POST` request will be made instead. | ||||
| @tparam[opt] { [string] = string } headers Additional headers to send as part | ||||
| of this request. | ||||
| @tparam[opt] boolean binary Whether to make a binary HTTP request. If true, | ||||
| the body will not be UTF-8 encoded, and the received response will not be | ||||
| decoded. | ||||
|  | ||||
| @tparam[2] { | ||||
|   url = string, body? = string, headers? = { [string] = string }, | ||||
|   binary? = boolean, method? = string, redirect? = boolean, | ||||
| } request Options for the request. | ||||
|  | ||||
| This table form is an expanded version of the previous syntax. All arguments | ||||
| from above are passed in as fields instead (for instance, | ||||
| `http.request("https://example.com")` becomes `http.request { url = | ||||
| "https://example.com" }`). | ||||
|  This table also accepts several additional options: | ||||
|  | ||||
|  - `method`: Which HTTP method to use, for instance `"PATCH"` or `"DELETE"`. | ||||
|  - `redirect`: Whether to follow HTTP redirects. Defaults to true. | ||||
|  | ||||
| @see http.get  For a synchronous way to make GET requests. | ||||
| @see http.post For a synchronous way to make POST requests. | ||||
|  | ||||
| @changed 1.63 Added argument for headers. | ||||
| @changed 1.80pr1 Added argument for binary handles. | ||||
| @changed 1.80pr1.6 Added support for table argument. | ||||
| @changed 1.86.0 Added PATCH and TRACE methods. | ||||
| ]] | ||||
| function request(_url, _post, _headers, _binary) | ||||
|     local url | ||||
|     if type(_url) == "table" then | ||||
|         checkOptions(_url) | ||||
|         url = _url.url | ||||
|     else | ||||
|         expect(1, _url, "string") | ||||
|         expect(2, _post, "string", "nil") | ||||
|         expect(3, _headers, "table", "nil") | ||||
|         expect(4, _binary, "boolean", "nil") | ||||
|         url = _url | ||||
|     end | ||||
|  | ||||
|     local ok, err = nativeHTTPRequest(_url, _post, _headers, _binary) | ||||
|     if not ok then | ||||
|         os.queueEvent("http_failure", url, err) | ||||
|     end | ||||
|  | ||||
|     -- Return true/false for legacy reasons. Undocumented, as it shouldn't be relied on. | ||||
|     return ok, err | ||||
| end | ||||
|  | ||||
| local nativeCheckURL = native.checkURL | ||||
|  | ||||
| --[[- Asynchronously determine whether a URL can be requested. | ||||
|  | ||||
| If this returns `true`, one should also listen for @{http_check} which will | ||||
| container further information about whether the URL is allowed or not. | ||||
|  | ||||
| @tparam string url The URL to check. | ||||
| @treturn true When this url is not invalid. This does not imply that it is | ||||
| allowed - see the comment above. | ||||
| @treturn[2] false When this url is invalid. | ||||
| @treturn string A reason why this URL is not valid (for instance, if it is | ||||
| malformed, or blocked). | ||||
|  | ||||
| @see http.checkURL For a synchronous version. | ||||
| ]] | ||||
| checkURLAsync = nativeCheckURL | ||||
|  | ||||
| --[[- Determine whether a URL can be requested. | ||||
|  | ||||
| If this returns `true`, one should also listen for @{http_check} which will | ||||
| container further information about whether the URL is allowed or not. | ||||
|  | ||||
| @tparam string url The URL to check. | ||||
| @treturn true When this url is valid and can be requested via @{http.request}. | ||||
| @treturn[2] false When this url is invalid. | ||||
| @treturn string A reason why this URL is not valid (for instance, if it is | ||||
| malformed, or blocked). | ||||
|  | ||||
| @see http.checkURLAsync For an asynchronous version. | ||||
|  | ||||
| @usage | ||||
| ```lua | ||||
| print(http.checkURL("https://example.tweaked.cc/")) | ||||
| -- => true | ||||
| print(http.checkURL("http://localhost/")) | ||||
| -- => false Domain not permitted | ||||
| print(http.checkURL("not a url")) | ||||
| -- => false URL malformed | ||||
| ``` | ||||
| ]] | ||||
| function checkURL(_url) | ||||
|     expect(1, _url, "string") | ||||
|     local ok, err = nativeCheckURL(_url) | ||||
|     if not ok then return ok, err end | ||||
|  | ||||
|     while true do | ||||
|         local _, url, ok, err = os.pullEvent("http_check") | ||||
|         if url == _url then return ok, err end | ||||
|     end | ||||
| end | ||||
|  | ||||
| local nativeWebsocket = native.websocket | ||||
|  | ||||
|  | ||||
| --[[- Asynchronously open a websocket. | ||||
|  | ||||
| This returns immediately, a @{websocket_success} or @{websocket_failure} | ||||
| will be queued once the request has completed. | ||||
|  | ||||
| @tparam string url The websocket url to connect to. This should have the | ||||
| `ws://` or `wss://` protocol. | ||||
| @tparam[opt] { [string] = string } headers Additional headers to send as part | ||||
| of the initial websocket connection. | ||||
| @since 1.80pr1.3 | ||||
| @changed 1.95.3 Added User-Agent to default headers. | ||||
| ]] | ||||
| function websocketAsync(url, headers) | ||||
|     expect(1, url, "string") | ||||
|     expect(2, headers, "table", "nil") | ||||
|  | ||||
|     local ok, err = nativeWebsocket(url, headers) | ||||
|     if not ok then | ||||
|         os.queueEvent("websocket_failure", url, err) | ||||
|     end | ||||
|  | ||||
|     -- Return true/false for legacy reasons. Undocumented, as it shouldn't be relied on. | ||||
|     return ok, err | ||||
| end | ||||
|  | ||||
| --[[- Open a websocket. | ||||
|  | ||||
| @tparam string url The websocket url to connect to. This should have the | ||||
| `ws://` or `wss://` protocol. | ||||
| @tparam[opt] { [string] = string } headers Additional headers to send as part | ||||
| of the initial websocket connection. | ||||
|  | ||||
| @treturn Websocket The websocket connection. | ||||
| @treturn[2] false If the websocket connection failed. | ||||
| @treturn string An error message describing why the connection failed. | ||||
| @since 1.80pr1.1 | ||||
| @changed 1.80pr1.3 No longer asynchronous. | ||||
| @changed 1.95.3 Added User-Agent to default headers. | ||||
| ]] | ||||
| function websocket(_url, _headers) | ||||
|     expect(1, _url, "string") | ||||
|     expect(2, _headers, "table", "nil") | ||||
|  | ||||
|     local ok, err = nativeWebsocket(_url, _headers) | ||||
|     if not ok then return ok, err end | ||||
|  | ||||
|     while true do | ||||
|         local event, url, param = os.pullEvent( ) | ||||
|         if event == "websocket_success" and url == _url then | ||||
|             return param | ||||
|         elseif event == "websocket_failure" and url == _url then | ||||
|             return false, param | ||||
|         end | ||||
|     end | ||||
| end | ||||
| @@ -1,6 +1,6 @@ | ||||
| --[[- A simple way to run several functions at once. | ||||
|  | ||||
| Functions are not actually executed simultaniously, but rather this API will | ||||
| Functions are not actually executed simultaneously, but rather this API will | ||||
| automatically switch between them whenever they yield (e.g. whenever they call | ||||
| @{coroutine.yield}, or functions that call that - such as @{os.pullEvent} - or | ||||
| functions that call that, etc - basically, anything that causes the function | ||||
|   | ||||
| @@ -17,12 +17,13 @@ You can list the names of all peripherals with the `peripherals` program, or the | ||||
| @{peripheral.getNames} function. | ||||
|  | ||||
| It's also possible to use peripherals which are further away from your computer | ||||
| through the use of @{modem|Wired Modems}. Place one modem against your computer, | ||||
| run Networking Cable to your peripheral, and then place another modem against | ||||
| that block. You can then right click the modem to use (or *attach*) the | ||||
| peripheral. This will print a peripheral name to chat, which can then be used | ||||
| just like a direction name to access the peripheral. You can click on the message | ||||
| to copy the name to your clipboard. | ||||
| through the use of @{modem|Wired Modems}. Place one modem against your computer | ||||
| (you may need to sneak and right click), run Networking Cable to your | ||||
| peripheral, and then place another modem against that block. You can then right | ||||
| click the modem to use (or *attach*) the peripheral. This will print a | ||||
| peripheral name to chat, which can then be used just like a direction name to | ||||
| access the peripheral. You can click on the message to copy the name to your | ||||
| clipboard. | ||||
|  | ||||
| ## Using peripherals | ||||
|  | ||||
|   | ||||
| @@ -218,9 +218,9 @@ channel. The message will be received by every device listening to rednet. | ||||
|  | ||||
| @param message The message to send. This should not contain coroutines or | ||||
| functions, as they will be converted to @{nil}. | ||||
| @tparam[opt] string protocol The "protocol" to send this message under. | ||||
| When using @{rednet.receive} one can filter to only receive messages sent | ||||
| under a particular protocol. | ||||
| @tparam[opt] string protocol The "protocol" to send this message under. When | ||||
| using @{rednet.receive} one can filter to only receive messages sent under a | ||||
| particular protocol. | ||||
| @see rednet.receive | ||||
| @changed 1.6 Added protocol parameter. | ||||
| @usage Broadcast the words "Hello, world!" to every computer using rednet. | ||||
| @@ -319,7 +319,7 @@ different, or if they only join a given network after "registering" themselves | ||||
| before doing so (eg while offline or part of a different network). | ||||
|  | ||||
| @tparam string protocol The protocol this computer provides. | ||||
| @tparam string hostname The name this protocol exposes for the given protocol. | ||||
| @tparam string hostname The name this computer exposes for the given protocol. | ||||
| @throws If trying to register a hostname which is reserved, or currently in use. | ||||
| @see rednet.unhost | ||||
| @see rednet.lookup | ||||
| @@ -464,6 +464,7 @@ function run() | ||||
|             if channel == id_as_channel() or channel == CHANNEL_BROADCAST then | ||||
|                 if type(message) == "table" and type(message.nMessageID) == "number" | ||||
|                     and message.nMessageID == message.nMessageID and not received_messages[message.nMessageID] | ||||
|                     and (type(message.nSender) == "nil" or (type(message.nSender) == "number" and message.nSender == message.nSender)) | ||||
|                     and ((message.nRecipient and message.nRecipient == os.getComputerID()) or channel == CHANNEL_BROADCAST) | ||||
|                     and isOpen(modem) | ||||
|                 then | ||||
|   | ||||
| @@ -157,7 +157,7 @@ function pagedPrint(text, free_lines) | ||||
|     -- Removed the redirector | ||||
|     term.redirect(oldTerm) | ||||
|  | ||||
|     -- Propogate errors | ||||
|     -- Propagate errors | ||||
|     if not ok then | ||||
|         error(err, 0) | ||||
|     end | ||||
|   | ||||
| @@ -6,7 +6,7 @@ end | ||||
|  | ||||
| --- The builtin turtle API, without any generated helper functions. | ||||
| -- | ||||
| -- @deprecated Historically this table behaved differently to the main turtle API, but this is no longer the base. You | ||||
| -- @deprecated Historically this table behaved differently to the main turtle API, but this is no longer the case. You | ||||
| -- should not need to use it. | ||||
| native = turtle.native or turtle | ||||
|  | ||||
|   | ||||
| @@ -4,15 +4,13 @@ | ||||
| * Better reporting of fatal computer timeouts in the server log. | ||||
| * Convert detail providers into a registry, allowing peripheral mods to read item/block details. | ||||
| * Redesign the metrics system. `/computercraft track` now allows computing aggregates (total, max, avg) on any metric, not just computer time. | ||||
| * File drag-and-drop now queues a `file_transfer` event on the computer. The | ||||
|   built-in shell or the `import` program must now be running to upload files. | ||||
| * File drag-and-drop now queues a `file_transfer` event on the computer. The built-in shell or the `import` program must now be running to upload files. | ||||
| * The `peripheral` now searches for remote peripherals using any peripheral with the `peripheral_hub` type, not just wired modems. | ||||
| * Add `include_hidden` option to `fs.complete`, which can be used to prevent hidden files showing up in autocomplete results. (IvoLeal72) | ||||
| * Add `include_hidden` option to `fs.complete`, which can be used to prevent hidden files showing up in autocomplete results. (IvoLeal72). | ||||
| * Add `shell.autocomplete_hidden` setting. (IvoLeal72) | ||||
| 
 | ||||
| Several bug fixes: | ||||
| * Prevent `edit`'s "Run" command scrolling the terminal output on smaller | ||||
|   screens. | ||||
| * Prevent `edit`'s "Run" command scrolling the terminal output on smaller screens. | ||||
| * Remove some non-determinism in computing item's `nbt` hash. | ||||
| * Don't set the `Origin` header on outgoing websocket requests. | ||||
| 
 | ||||
| @@ -173,8 +171,7 @@ And several bug fixes: | ||||
| * Prevent `parallel.*` from hanging when no arguments are given. | ||||
| * Prevent issue in rednet when the message ID is NaN. | ||||
| * Fix `help` program crashing when terminal changes width. | ||||
| * Ensure monitors are well-formed when placed, preventing graphical glitches | ||||
|   when using Carry On or Quark. | ||||
| * Ensure monitors are well-formed when placed, preventing graphical glitches when using Carry On or Quark. | ||||
| * Accept several more extensions in the websocket client. | ||||
| * Prevent `wget` crashing when given an invalid URL and no filename. | ||||
| * Correctly wrap string within `textutils.slowWrite`. | ||||
| @@ -201,7 +198,7 @@ And several bug fixes: | ||||
| 
 | ||||
| And several bug fixes: | ||||
| * Fix NPE when using a treasure disk when no treasure disks are available. | ||||
| * Prevent command computers discarding command ouput when certain game rules are off. | ||||
| * Prevent command computers discarding command output when certain game rules are off. | ||||
| * Fix turtles not updating peripherals when upgrades are unequipped (Ronan-H). | ||||
| * Fix computers not shutting down on fatal errors within the Lua VM. | ||||
| * Speakers now correctly stop playing when broken, and sound follows noisy turtles and pocket computers. | ||||
| @@ -211,32 +208,6 @@ And several bug fixes: | ||||
| * Correctly render the transparent background on pocket/normal computers. | ||||
| * Don't apply CraftTweaker actions twice on single-player worlds. | ||||
| 
 | ||||
| # New features in CC: Tweaked 1.97.0 | ||||
| 
 | ||||
| * Update several translations (Anavrins, Jummit, Naheulf). | ||||
| * Add button to view a computer's folder to `/computercraft dump`. | ||||
| * Allow cleaning dyed turtles in a cauldron. | ||||
| * Add scale subcommand to `monitor` program (MCJack123). | ||||
| * Add option to make `textutils.serialize` not write an indent (magiczocker10). | ||||
| * Allow comparing vectors using `==` (fatboychummy). | ||||
| * Improve HTTP error messages for SSL failures. | ||||
| * Allow `craft` program to craft unlimited items (fatboychummy). | ||||
| * Impose some limits on various command queues. | ||||
| * Add buttons to shutdown and terminate to computer GUIs. | ||||
| * Add program subcompletion to several programs (Wojbie). | ||||
| * Update the `help` program to accept and (partially) highlight markdown files. | ||||
| * Remove config option for the debug API. | ||||
| * Allow setting the subprotocol header for websockets. | ||||
| 
 | ||||
| And several bug fixes: | ||||
| * Fix NPE when using a treasure disk when no treasure disks are available. | ||||
| * Prevent command computers discarding command ouput when certain game rules are off. | ||||
| * Fix turtles not updating peripherals when upgrades are unequipped (Ronan-H). | ||||
| * Fix computers not shutting down on fatal errors within the Lua VM. | ||||
| * Speakers now correctly stop playing when broken, and sound follows noisy turtles and pocket computers. | ||||
| * Update the `wget` to be more resiliant in the face of user-errors. | ||||
| * Fix exiting `paint` typing "e" in the shell. | ||||
| 
 | ||||
| # New features in CC: Tweaked 1.96.0 | ||||
| 
 | ||||
| * Use lightGrey for folders within the "list" program. | ||||
| @@ -288,7 +259,7 @@ Several bug fixes: | ||||
| # New features in CC: Tweaked 1.95.0 | ||||
| 
 | ||||
| * Optimise the paint program's initial render. | ||||
| * Several documentation improvments (Gibbo3771, MCJack123). | ||||
| * Several documentation improvements (Gibbo3771, MCJack123). | ||||
| * `fs.combine` now accepts multiple arguments. | ||||
| * Add a setting (`bios.strict_globals`) to error when accidentally declaring a global. (Lupus590). | ||||
| * Add an improved help viewer which allows scrolling up and down (MCJack123). | ||||
| @@ -328,7 +299,7 @@ And several bug fixes: | ||||
| 
 | ||||
| * Update Swedish translations (Granddave). | ||||
| * Printers use item tags to check dyes. | ||||
| * HTTP rules may now be targetted for a specific port. | ||||
| * HTTP rules may now be targeted for a specific port. | ||||
| * Don't propagate adjacent redstone signals through computers. | ||||
| 
 | ||||
| And several bug fixes: | ||||
| @@ -352,7 +323,7 @@ And several bug fixes: | ||||
| # New features in CC: Tweaked 1.91.0 | ||||
| 
 | ||||
| * [Generic peripherals] Expose NBT hashes of items to inventory methods. | ||||
| * Bump Cobalt version | ||||
| * Bump Cobalt version: | ||||
|   * Optimise handling of string concatenation. | ||||
|   * Add string.{pack,unpack,packsize} (MCJack123) | ||||
| * Update to 1.16.2 | ||||
| @@ -448,7 +419,7 @@ And several bug fixes: | ||||
|   - Add `utf8` lib. | ||||
|   - Mirror Lua's behaviour of tail calls more closely. Native functions are no longer tail called, and tail calls are displayed in the stack trace. | ||||
|   - `table.unpack` now uses `__len` and `__index` metamethods. | ||||
|   - Parser errors now include the token where the error occured. | ||||
|   - Parser errors now include the token where the error occurred. | ||||
| * Add `textutils.unserializeJSON`. This can be used to decode standard JSON and stringified-NBT. | ||||
| * The `settings` API now allows "defining" settings. This allows settings to specify a default value and description. | ||||
| * Enable the motd on non-pocket computers. | ||||
| @@ -456,7 +427,7 @@ And several bug fixes: | ||||
| * Add Danish and Korean translations (ChristianLW, mindy15963) | ||||
| * Fire `mouse_up` events in the monitor program. | ||||
| * Allow specifying a timeout to `websocket.receive`. | ||||
| * Increase the maximimum limit for websocket messages. | ||||
| * Increase the maximum limit for websocket messages. | ||||
| * Optimise capacity checking of computer/disk folders. | ||||
| 
 | ||||
| And several bug fixes: | ||||
| @@ -468,13 +439,12 @@ And several bug fixes: | ||||
| 
 | ||||
| # New features in CC: Tweaked 1.86.2 | ||||
| 
 | ||||
| * Fix peripheral.getMethods returning an empty table | ||||
| * Update to Minecraft 1.15.2. This is currently alpha-quality and so is missing | ||||
|   missing features and may be unstable. | ||||
| * Fix peripheral.getMethods returning an empty table. | ||||
| * Update to Minecraft 1.15.2. This is currently alpha-quality and so is missing missing features and may be unstable. | ||||
| 
 | ||||
| # New features in CC: Tweaked 1.86.1 | ||||
| 
 | ||||
| * Add a help message to the Lua REPL's exit function | ||||
| * Add a help message to the Lua REPL's exit function. | ||||
| * Add more MOTD messages. (osmarks) | ||||
| * GPS requests are now made anonymously (osmarks) | ||||
| * Minor memory usage improvements to Cobalt VM. | ||||
|   | ||||
| @@ -1,2 +1,2 @@ | ||||
| io is a standard Lua5.1 API, reimplemented for CraftOS. Not all the features are availiable. | ||||
| io is a standard Lua5.1 API, reimplemented for CraftOS. Not all the features are available. | ||||
| Refer to http://www.lua.org/manual/5.1/ for more information. | ||||
|   | ||||
| @@ -7,5 +7,5 @@ To terminate a program stuck in a loop, hold Ctrl+T for 1 second. | ||||
| To quickly shutdown a computer, hold Ctrl+S for 1 second. | ||||
| To quickly reboot a computer, hold Ctrl+R for 1 second. | ||||
|  | ||||
| To learn about the programming APIs availiable, type "apis" or "help apis". | ||||
| To learn about the programming APIs available, type "apis" or "help apis". | ||||
| If you get stuck, visit the forums at http://www.computercraft.info/ for advice and tutorials. | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| turtle is an api availiable on Turtles, which controls their movement. | ||||
| turtle is an api available on Turtles, which controls their movement. | ||||
| Functions in the Turtle API: | ||||
| turtle.forward() | ||||
| turtle.back() | ||||
|   | ||||
| @@ -4,15 +4,13 @@ New features in CC: Tweaked 1.101.0 | ||||
| * Better reporting of fatal computer timeouts in the server log. | ||||
| * Convert detail providers into a registry, allowing peripheral mods to read item/block details. | ||||
| * Redesign the metrics system. `/computercraft track` now allows computing aggregates (total, max, avg) on any metric, not just computer time. | ||||
| * File drag-and-drop now queues a `file_transfer` event on the computer. The | ||||
|   built-in shell or the `import` program must now be running to upload files. | ||||
| * File drag-and-drop now queues a `file_transfer` event on the computer. The built-in shell or the `import` program must now be running to upload files. | ||||
| * The `peripheral` now searches for remote peripherals using any peripheral with the `peripheral_hub` type, not just wired modems. | ||||
| * Add `include_hidden` option to `fs.complete`, which can be used to prevent hidden files showing up in autocomplete results. (IvoLeal72) | ||||
| * Add `include_hidden` option to `fs.complete`, which can be used to prevent hidden files showing up in autocomplete results. (IvoLeal72). | ||||
| * Add `shell.autocomplete_hidden` setting. (IvoLeal72) | ||||
| 
 | ||||
| Several bug fixes: | ||||
| * Prevent `edit`'s "Run" command scrolling the terminal output on smaller | ||||
|   screens. | ||||
| * Prevent `edit`'s "Run" command scrolling the terminal output on smaller screens. | ||||
| * Remove some non-determinism in computing item's `nbt` hash. | ||||
| * Don't set the `Origin` header on outgoing websocket requests. | ||||
| 
 | ||||
|   | ||||
| @@ -0,0 +1,173 @@ | ||||
| --[[- A pretty-printer for Lua errors. | ||||
|  | ||||
| :::warning | ||||
| This is an internal module and SHOULD NOT be used in your own code. It may | ||||
| be removed or changed at any time. | ||||
| ::: | ||||
|  | ||||
| This consumes a list of messages and "annotations" and displays the error to the | ||||
| terminal. | ||||
|  | ||||
| @see cc.internal.syntax.errors For errors produced by the parser. | ||||
| @local | ||||
| ]] | ||||
|  | ||||
| local pretty = require "cc.pretty" | ||||
| local expect = require "cc.expect" | ||||
| local expect, field = expect.expect, expect.field | ||||
| local wrap = require "cc.strings".wrap | ||||
|  | ||||
| --- Write a message to the screen. | ||||
| -- @tparam cc.pretty.Doc|string msg The message to write. | ||||
| local function display(msg) | ||||
|     if type(msg) == "table" then pretty.print(msg) else print(msg) end | ||||
| end | ||||
|  | ||||
| -- Write a message to the screen, aligning to the current cursor position. | ||||
| -- @tparam cc.pretty.Doc|string msg The message to write. | ||||
| local function display_here(msg, preamble) | ||||
|     expect(1, msg, "string", "table") | ||||
|     local x = term.getCursorPos() | ||||
|     local width, height = term.getSize() | ||||
|     width = width - x + 1 | ||||
|  | ||||
|     local function newline() | ||||
|         local _, y = term.getCursorPos() | ||||
|         if y >= height then | ||||
|             term.scroll(1) | ||||
|         else | ||||
|             y = y + 1 | ||||
|         end | ||||
|  | ||||
|         preamble(y) | ||||
|         term.setCursorPos(x, y) | ||||
|     end | ||||
|  | ||||
|     if type(msg) == "string" then | ||||
|         local lines = wrap(msg, width) | ||||
|         term.write(lines[1]) | ||||
|         for i = 2, #lines do | ||||
|             newline() | ||||
|             term.write(lines[i]) | ||||
|         end | ||||
|     else | ||||
|         local def_colour = term.getTextColour() | ||||
|         local function display_impl(doc) | ||||
|             expect(1, doc, "table") | ||||
|             local kind = doc.tag | ||||
|             if kind == "nil" then return | ||||
|             elseif kind == "text" then | ||||
|                 -- TODO: cc.strings.wrap doesn't support a leading indent. We should | ||||
|                 -- fix that! | ||||
|                 -- Might also be nice to add a wrap_iter, which returns an iterator over | ||||
|                 -- start_pos, end_pos instead. | ||||
|  | ||||
|                 if doc.colour then term.setTextColour(doc.colour) end | ||||
|                 local x1 = term.getCursorPos() | ||||
|  | ||||
|                 local lines = wrap((" "):rep(x1 - x) .. doc.text, width) | ||||
|                 term.write(lines[1]:sub(x1 - x + 1)) | ||||
|                 for i = 2, #lines do | ||||
|                     newline() | ||||
|                     term.write(lines[i]) | ||||
|                 end | ||||
|  | ||||
|                 if doc.colour then term.setTextColour(def_colour) end | ||||
|             elseif kind == "concat" then | ||||
|                 for i = 1, doc.n do display_impl(doc[i]) end | ||||
|             else | ||||
|                 error("Unknown doc " .. kind) | ||||
|             end | ||||
|         end | ||||
|         display_impl(msg) | ||||
|     end | ||||
|     print() | ||||
| end | ||||
|  | ||||
| --- A list of colours we can use for error messages. | ||||
| local error_colours = { colours.red, colours.green, colours.magenta, colours.orange } | ||||
|  | ||||
| --- The accent line used to denote a block of code. | ||||
| local code_accent = pretty.text("\x95", colours.cyan) | ||||
|  | ||||
| --[[- | ||||
| @tparam { get_pos = function, get_line = function } context | ||||
|     The context where the error was reported. This effectively acts as a view | ||||
|     over the underlying source, exposing the following functions: | ||||
|     - `get_pos`: Get the line and column of an opaque position. | ||||
|     - `get_line`: Get the source code for an opaque position. | ||||
| @tparam table message The message to display, as produced by @{cc.internal.syntax.errors}. | ||||
| ]] | ||||
| return function(context, message) | ||||
|     expect(1, context, "table") | ||||
|     expect(2, message, "table") | ||||
|     field(context, "get_pos", "function") | ||||
|     field(context, "get_line", "function") | ||||
|  | ||||
|     if #message == 0 then error("Message is empty", 2) end | ||||
|  | ||||
|     local error_colour = 1 | ||||
|     local width = term.getSize() | ||||
|  | ||||
|     for msg_idx = 1, #message do | ||||
|         if msg_idx > 1 then print() end | ||||
|  | ||||
|         local msg = message[msg_idx] | ||||
|         if type(msg) == "table" and msg.tag == "annotate" then | ||||
|             local line, col = context.get_pos(msg.start_pos) | ||||
|             local end_line, end_col = context.get_pos(msg.end_pos) | ||||
|             local contents = context.get_line(msg.start_pos) | ||||
|  | ||||
|             -- Pick a starting column. We pick the left-most position which fits | ||||
|             -- in one of the following: | ||||
|             --  - 10 characters after the start column. | ||||
|             --  - 5 characters after the end column. | ||||
|             --  - The end of the line. | ||||
|             if line ~= end_line then end_col = #contents end | ||||
|             local start_col = math.max(1, math.min(col + 10, end_col + 5, #contents + 1) - width + 1) | ||||
|  | ||||
|             -- Pick a colour for this annotation. | ||||
|             local colour = colours.toBlit(error_colours[error_colour]) | ||||
|             error_colour = (error_colour % #error_colours) + 1 | ||||
|  | ||||
|             -- Print the line number and snippet of code. We display french | ||||
|             -- quotes on either side of the string if it is truncated. | ||||
|             local str_start, str_end = start_col, start_col + width - 2 | ||||
|             local prefix, suffix = "", "" | ||||
|             if start_col > 1 then | ||||
|                 str_start = str_start + 1 | ||||
|                 prefix = pretty.text("\xab", colours.grey) | ||||
|             end | ||||
|             if str_end < #contents then | ||||
|                 str_end = str_end - 1 | ||||
|                 suffix = pretty.text("\xbb", colours.grey) | ||||
|             end | ||||
|  | ||||
|             pretty.print(code_accent .. pretty.text("Line " .. line, colours.cyan)) | ||||
|             pretty.print(code_accent .. prefix .. pretty.text(contents:sub(str_start, str_end), colours.lightGrey) .. suffix) | ||||
|  | ||||
|             -- Print a line highlighting the region of text. | ||||
|             local _, y = term.getCursorPos() | ||||
|             pretty.write(code_accent) | ||||
|  | ||||
|             local indicator_end = end_col | ||||
|             if end_col > str_end then indicator_end = str_end end | ||||
|  | ||||
|             local indicator_len = indicator_end - col + 1 | ||||
|             term.setCursorPos(col - start_col + 2, y) | ||||
|             term.blit(("\x83"):rep(indicator_len), colour:rep(indicator_len), ("f"):rep(indicator_len)) | ||||
|             print() | ||||
|  | ||||
|             -- And then print the annotation's message, if present. | ||||
|             if msg.msg ~= "" then | ||||
|                 term.blit("\x95", colour, "f") | ||||
|                 display_here(msg.msg, function(y) | ||||
|                     term.setCursorPos(1, y) | ||||
|                     term.blit("\x95", colour, "f") | ||||
|                 end) | ||||
|             end | ||||
|         else | ||||
|             display(msg) | ||||
|         end | ||||
|     end | ||||
| end | ||||
| @@ -0,0 +1,120 @@ | ||||
| --[[- Internal tools for working with errors. | ||||
|  | ||||
| :::warning | ||||
| This is an internal module and SHOULD NOT be used in your own code. It may | ||||
| be removed or changed at any time. | ||||
| ::: | ||||
|  | ||||
| @local | ||||
| ]] | ||||
|  | ||||
| local expect = require "cc.expect".expect | ||||
| local error_printer = require "cc.internal.error_printer" | ||||
|  | ||||
| local function find_frame(thread, file, line) | ||||
|     -- Scan the first 16 frames for something interesting. | ||||
|     for offset = 0, 15 do | ||||
|         local frame = debug.getinfo(thread, offset, "Sl") | ||||
|         if not frame then break end | ||||
|  | ||||
|         if frame.short_src == file and frame.what ~= "C" and frame.currentline == line then | ||||
|             return frame | ||||
|         end | ||||
|     end | ||||
| end | ||||
|  | ||||
| --[[- Attempt to call the provided function `func` with the provided arguments. | ||||
|  | ||||
| @tparam function func The function to call. | ||||
| @param ... Arguments to this function. | ||||
|  | ||||
| @treturn[1] true If the function ran successfully. | ||||
|     @return[1] ... The return values of the function. | ||||
|  | ||||
| @treturn[2] false If the function failed. | ||||
| @return[2] The error message | ||||
| @treturn[2] coroutine The thread where the error occurred. | ||||
| ]] | ||||
| local function try(func, ...) | ||||
|     expect(1, func, "function") | ||||
|  | ||||
|     local co = coroutine.create(func) | ||||
|     local result = table.pack(coroutine.resume(co, ...)) | ||||
|  | ||||
|     while coroutine.status(co) ~= "dead" do | ||||
|         local event = table.pack(os.pullEventRaw(result[2])) | ||||
|         if result[2] == nil or event[1] == result[2] or event[1] == "terminate" then | ||||
|             result = table.pack(coroutine.resume(co, table.unpack(event, 1, event.n))) | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     if not result[1] then return false, result[2], co end | ||||
|     return table.unpack(result, 1, result.n) | ||||
| end | ||||
|  | ||||
| --[[- Report additional context about an error. | ||||
|  | ||||
| @param err The error to report. | ||||
| @tparam coroutine thread The coroutine where the error occurred. | ||||
| @tparam[opt] { [string] = string } source_map Map of chunk names to their contents. | ||||
| ]] | ||||
| local function report(err, thread, source_map) | ||||
|     expect(2, thread, "thread") | ||||
|     expect(3, source_map, "table", "nil") | ||||
|  | ||||
|     if type(err) ~= "string" then return end | ||||
|  | ||||
|     local file, line = err:match("^([^:]+):(%d+):") | ||||
|     if not file then return end | ||||
|     line = tonumber(line) | ||||
|  | ||||
|     local frame = find_frame(thread, file, line) | ||||
|     if not frame or not frame.currentcolumn then return end | ||||
|  | ||||
|     local column = frame.currentcolumn | ||||
|     local line_contents | ||||
|     if source_map and source_map[frame.source] then | ||||
|         -- File exists in the source map. | ||||
|         local pos, contents = 1, source_map[frame.source] | ||||
|         -- Try to remap our position. The interface for this only makes sense | ||||
|         -- for single line sources, but that's sufficient for where we need it | ||||
|         -- (the REPL). | ||||
|         if type(contents) == "table" then | ||||
|             column = column - contents.offset | ||||
|             contents = contents.contents | ||||
|         end | ||||
|  | ||||
|         for _ = 1, line - 1 do | ||||
|             local next_pos = contents:find("\n", pos) | ||||
|             if not next_pos then return end | ||||
|             pos = next_pos + 1 | ||||
|         end | ||||
|  | ||||
|         local end_pos = contents:find("\n", pos) | ||||
|         line_contents = contents:sub(pos, end_pos and end_pos - 1 or #contents) | ||||
|  | ||||
|     elseif frame.source:sub(1, 2) == "@/" then | ||||
|         -- Read the file from disk. | ||||
|         local handle = fs.open(frame.source:sub(3), "r") | ||||
|         if not handle then return end | ||||
|         for _ = 1, line - 1 do handle.readLine() end | ||||
|  | ||||
|         line_contents = handle.readLine() | ||||
|     end | ||||
|  | ||||
|     -- Could not determine the line. Bail. | ||||
|     if not line_contents or #line_contents == "" then return end | ||||
|  | ||||
|     error_printer({ | ||||
|         get_pos = function() return line, column end, | ||||
|         get_line = function() return line_contents end, | ||||
|     }, { | ||||
|         { tag = "annotate", start_pos = column, end_pos = column, msg = "" }, | ||||
|     }) | ||||
| end | ||||
|  | ||||
|  | ||||
| return { | ||||
|     try = try, | ||||
|     report = report, | ||||
| } | ||||
| @@ -1,8 +1,16 @@ | ||||
| -- Internal module for handling file uploads. This has NO stability guarantees, | ||||
| -- and so SHOULD NOT be relyed on in user code. | ||||
| --[[- Upload a list of files, as received by the @{event!file_transfer} event. | ||||
| 
 | ||||
| :::warning | ||||
| This is an internal module and SHOULD NOT be used in your own code. It may | ||||
| be removed or changed at any time. | ||||
| ::: | ||||
| 
 | ||||
| @local | ||||
| ]] | ||||
| 
 | ||||
| local completion = require "cc.completion" | ||||
| 
 | ||||
| --- @tparam { file_transfer.TransferredFile ...} files The files to upload. | ||||
| return function(files) | ||||
|     local overwrite = {} | ||||
|     for _, file in pairs(files) do | ||||
| @@ -0,0 +1,552 @@ | ||||
| --[[- The error messages reported by our lexer and parser. | ||||
|  | ||||
| :::warning | ||||
| This is an internal module and SHOULD NOT be used in your own code. It may | ||||
| be removed or changed at any time. | ||||
| ::: | ||||
|  | ||||
| This provides a list of factory methods which take source positions and produce | ||||
| appropriate error messages targeting that location. These error messages can | ||||
| then be displayed to the user via @{cc.internal.error_printer}. | ||||
|  | ||||
| @local | ||||
| ]] | ||||
|  | ||||
| local pretty = require "cc.pretty" | ||||
| local expect = require "cc.expect".expect | ||||
| local tokens = require "cc.internal.syntax.parser".tokens | ||||
|  | ||||
| local function annotate(start_pos, end_pos, msg) | ||||
|     if msg == nil and (type(end_pos) == "string" or type(end_pos) == "table" or type(end_pos) == "nil") then | ||||
|         end_pos, msg = start_pos, end_pos | ||||
|     end | ||||
|  | ||||
|     expect(1, start_pos, "number") | ||||
|     expect(2, end_pos, "number") | ||||
|     expect(3, msg, "string", "table", "nil") | ||||
|  | ||||
|     return { tag = "annotate", start_pos = start_pos, end_pos = end_pos, msg = msg or "" } | ||||
| end | ||||
|  | ||||
| --- Format a string as a non-highlighted block of code. | ||||
| -- | ||||
| -- @tparam string msg The code to format. | ||||
| -- @treturn cc.pretty.Doc The formatted code. | ||||
| local function code(msg) return pretty.text(msg, colours.lightGrey) end | ||||
|  | ||||
| --- Maps tokens to a more friendly version. | ||||
| local token_names = setmetatable({ | ||||
|     -- Specific tokens. | ||||
|     [tokens.IDENT] = "identifier", | ||||
|     [tokens.NUMBER] = "number", | ||||
|     [tokens.STRING] = "string", | ||||
|     [tokens.EOF] = "end of file", | ||||
|     -- Symbols and keywords | ||||
|     [tokens.ADD] = code("+"), | ||||
|     [tokens.AND] = code("and"), | ||||
|     [tokens.BREAK] = code("break"), | ||||
|     [tokens.CBRACE] = code("}"), | ||||
|     [tokens.COLON] = code(":"), | ||||
|     [tokens.COMMA] = code(","), | ||||
|     [tokens.CONCAT] = code(".."), | ||||
|     [tokens.CPAREN] = code(")"), | ||||
|     [tokens.CSQUARE] = code("]"), | ||||
|     [tokens.DIV] = code("/"), | ||||
|     [tokens.DO] = code("do"), | ||||
|     [tokens.DOT] = code("."), | ||||
|     [tokens.DOTS] = code("..."), | ||||
|     [tokens.ELSE] = code("else"), | ||||
|     [tokens.ELSEIF] = code("elseif"), | ||||
|     [tokens.END] = code("end"), | ||||
|     [tokens.EQ] = code("=="), | ||||
|     [tokens.EQUALS] = code("="), | ||||
|     [tokens.FALSE] = code("false"), | ||||
|     [tokens.FOR] = code("for"), | ||||
|     [tokens.FUNCTION] = code("function"), | ||||
|     [tokens.GE] = code(">="), | ||||
|     [tokens.GT] = code(">"), | ||||
|     [tokens.IF] = code("if"), | ||||
|     [tokens.IN] = code("in"), | ||||
|     [tokens.LE] = code("<="), | ||||
|     [tokens.LEN] = code("#"), | ||||
|     [tokens.LOCAL] = code("local"), | ||||
|     [tokens.LT] = code("<"), | ||||
|     [tokens.MOD] = code("%"), | ||||
|     [tokens.MUL] = code("*"), | ||||
|     [tokens.NE] = code("~="), | ||||
|     [tokens.NIL] = code("nil"), | ||||
|     [tokens.NOT] = code("not"), | ||||
|     [tokens.OBRACE] = code("{"), | ||||
|     [tokens.OPAREN] = code("("), | ||||
|     [tokens.OR] = code("or"), | ||||
|     [tokens.OSQUARE] = code("["), | ||||
|     [tokens.POW] = code("^"), | ||||
|     [tokens.REPEAT] = code("repeat"), | ||||
|     [tokens.RETURN] = code("return"), | ||||
|     [tokens.SEMICOLON] = code(";"), | ||||
|     [tokens.SUB] = code("-"), | ||||
|     [tokens.THEN] = code("then"), | ||||
|     [tokens.TRUE] = code("true"), | ||||
|     [tokens.UNTIL] = code("until"), | ||||
|     [tokens.WHILE] = code("while"), | ||||
| }, { __index = function(_, name) error("No such token " .. tostring(name), 2) end }) | ||||
|  | ||||
| local errors = {} | ||||
|  | ||||
| -------------------------------------------------------------------------------- | ||||
| -- Lexer errors | ||||
| -------------------------------------------------------------------------------- | ||||
|  | ||||
| --[[- A string which ends without a closing quote. | ||||
|  | ||||
| @tparam number start_pos The start position of the string. | ||||
| @tparam number end_pos The end position of the string. | ||||
| @tparam string quote The kind of quote (`"` or `'`). | ||||
| @return The resulting parse error. | ||||
| ]] | ||||
| function errors.unfinished_string(start_pos, end_pos, quote) | ||||
|     expect(1, start_pos, "number") | ||||
|     expect(2, end_pos, "number") | ||||
|     expect(3, quote, "string") | ||||
|  | ||||
|     return { | ||||
|         "This string is not finished. Are you missing a closing quote (" .. code(quote) .. ")?", | ||||
|         annotate(start_pos, "String started here."), | ||||
|         annotate(end_pos, "Expected a closing quote here."), | ||||
|     } | ||||
| end | ||||
|  | ||||
| --[[- A string which ends with an escape sequence (so a literal `"foo\`). This | ||||
| is slightly different from @{unfinished_string}, as we don't want to suggest | ||||
| adding a quote. | ||||
|  | ||||
| @tparam number start_pos The start position of the string. | ||||
| @tparam number end_pos The end position of the string. | ||||
| @tparam string quote The kind of quote (`"` or `'`). | ||||
| @return The resulting parse error. | ||||
| ]] | ||||
| function errors.unfinished_string_escape(start_pos, end_pos, quote) | ||||
|     expect(1, start_pos, "number") | ||||
|     expect(2, end_pos, "number") | ||||
|     expect(3, quote, "string") | ||||
|  | ||||
|     return { | ||||
|         "This string is not finished.", | ||||
|         annotate(start_pos, "String started here."), | ||||
|         annotate(end_pos, "An escape sequence was started here, but with nothing following it."), | ||||
|     } | ||||
| end | ||||
|  | ||||
| --[[- A long string was never finished. | ||||
|  | ||||
| @tparam number start_pos The start position of the long string delimiter. | ||||
| @tparam number end_pos The end position of the long string delimiter. | ||||
| @tparam number ;em The length of the long string delimiter, excluding the first `[`. | ||||
| @return The resulting parse error. | ||||
| ]] | ||||
| function errors.unfinished_long_string(start_pos, end_pos, len) | ||||
|     expect(1, start_pos, "number") | ||||
|     expect(2, end_pos, "number") | ||||
|     expect(3, len, "number") | ||||
|  | ||||
|     return { | ||||
|         "This string was never finished.", | ||||
|         annotate(start_pos, end_pos, "String was started here."), | ||||
|         "We expected a closing delimiter (" .. code("]" .. ("="):rep(len - 1) .. "]") .. ") somewhere after this string was started.", | ||||
|     } | ||||
| end | ||||
|  | ||||
| --[[- Malformed opening to a long string (i.e. `[=`). | ||||
|  | ||||
| @tparam number start_pos The start position of the long string delimiter. | ||||
| @tparam number end_pos The end position of the long string delimiter. | ||||
| @tparam number len The length of the long string delimiter, excluding the first `[`. | ||||
| @return The resulting parse error. | ||||
| ]] | ||||
| function errors.malformed_long_string(start_pos, end_pos, len) | ||||
|     expect(1, start_pos, "number") | ||||
|     expect(2, end_pos, "number") | ||||
|     expect(3, len, "number") | ||||
|  | ||||
|     return { | ||||
|         "Incorrect start of a long string.", | ||||
|         annotate(start_pos, end_pos), | ||||
|         "Tip: If you wanted to start a long string here, add an extra " .. code("[") .. " here.", | ||||
|     } | ||||
| end | ||||
|  | ||||
| --[[- Malformed nesting of a long string. | ||||
|  | ||||
| @tparam number start_pos The start position of the long string delimiter. | ||||
| @tparam number end_pos The end position of the long string delimiter. | ||||
| @return The resulting parse error. | ||||
| ]] | ||||
| function errors.nested_long_str(start_pos, end_pos) | ||||
|     expect(1, start_pos, "number") | ||||
|     expect(2, end_pos, "number") | ||||
|  | ||||
|     return { | ||||
|         code("[[") .. " cannot be nested inside another " .. code("[[ ... ]]"), | ||||
|         annotate(start_pos, end_pos), | ||||
|     } | ||||
| end | ||||
|  | ||||
| --[[- A malformed numeric literal. | ||||
|  | ||||
| @tparam number start_pos The start position of the number. | ||||
| @tparam number end_pos The end position of the number. | ||||
| @return The resulting parse error. | ||||
| ]] | ||||
| function errors.malformed_number(start_pos, end_pos) | ||||
|     expect(1, start_pos, "number") | ||||
|     expect(2, end_pos, "number") | ||||
|  | ||||
|     return { | ||||
|         "This isn't a valid number.", | ||||
|         annotate(start_pos, end_pos), | ||||
|         "Numbers must be in one of the following formats: " .. code("123") .. ", " | ||||
|         .. code("3.14") .. ", " .. code("23e35") .. ", " .. code("0x01AF") .. ".", | ||||
|     } | ||||
| end | ||||
|  | ||||
| --[[- A long comment was never finished. | ||||
|  | ||||
| @tparam number start_pos The start position of the long string delimiter. | ||||
| @tparam number end_pos The end position of the long string delimiter. | ||||
| @tparam number len The length of the long string delimiter, excluding the first `[`. | ||||
| @return The resulting parse error. | ||||
| ]] | ||||
| function errors.unfinished_long_comment(start_pos, end_pos, len) | ||||
|     expect(1, start_pos, "number") | ||||
|     expect(2, end_pos, "number") | ||||
|     expect(3, len, "number") | ||||
|  | ||||
|     return { | ||||
|         "This comment was never finished.", | ||||
|         annotate(start_pos, end_pos, "Comment was started here."), | ||||
|         "We expected a closing delimiter (" .. code("]" .. ("="):rep(len - 1) .. "]") .. ") somewhere after this comment was started.", | ||||
|     } | ||||
| end | ||||
|  | ||||
| --[[- `&&` was used instead of `and`. | ||||
|  | ||||
| @tparam number start_pos The start position of the token. | ||||
| @tparam number end_pos The end position of the token. | ||||
| @return The resulting parse error. | ||||
| ]] | ||||
| function errors.wrong_and(start_pos, end_pos) | ||||
|     expect(1, start_pos, "number") | ||||
|     expect(2, end_pos, "number") | ||||
|  | ||||
|     return { | ||||
|         "Unexpected character.", | ||||
|         annotate(start_pos, end_pos), | ||||
|         "Tip: Replace this with " .. code("and") .. " to check if both values are true.", | ||||
|     } | ||||
| end | ||||
|  | ||||
| --[[- `||` was used instead of `or`. | ||||
|  | ||||
| @tparam number start_pos The start position of the token. | ||||
| @tparam number end_pos The end position of the token. | ||||
| @return The resulting parse error. | ||||
| ]] | ||||
| function errors.wrong_or(start_pos, end_pos) | ||||
|     expect(1, start_pos, "number") | ||||
|     expect(2, end_pos, "number") | ||||
|  | ||||
|     return { | ||||
|         "Unexpected character.", | ||||
|         annotate(start_pos, end_pos), | ||||
|         "Tip: Replace this with " .. code("or") .. " to check if either value is true.", | ||||
|     } | ||||
| end | ||||
|  | ||||
| --[[- `!=` was used instead of `~=`. | ||||
|  | ||||
| @tparam number start_pos The start position of the token. | ||||
| @tparam number end_pos The end position of the token. | ||||
| @return The resulting parse error. | ||||
| ]] | ||||
| function errors.wrong_ne(start_pos, end_pos) | ||||
|     expect(1, start_pos, "number") | ||||
|     expect(2, end_pos, "number") | ||||
|  | ||||
|     return { | ||||
|         "Unexpected character.", | ||||
|         annotate(start_pos, end_pos), | ||||
|         "Tip: Replace this with " .. code("~=") .. " to check if two values are not equal.", | ||||
|     } | ||||
| end | ||||
|  | ||||
| --[[- An unexpected character was used. | ||||
|  | ||||
| @tparam number pos The position of this character. | ||||
| @return The resulting parse error. | ||||
| ]] | ||||
| function errors.unexpected_character(pos) | ||||
|     expect(1, pos, "number") | ||||
|     return { | ||||
|         "Unexpected character.", | ||||
|         annotate(pos, "This character isn't usable in Lua code."), | ||||
|     } | ||||
| end | ||||
|  | ||||
| -------------------------------------------------------------------------------- | ||||
| -- Expression parsing errors | ||||
| -------------------------------------------------------------------------------- | ||||
|  | ||||
| --[[- A fallback error when we expected an expression but received another token. | ||||
|  | ||||
| @tparam number token The token id. | ||||
| @tparam number start_pos The start position of the token. | ||||
| @tparam number end_pos The end position of the token. | ||||
| @return The resulting parse error. | ||||
| ]] | ||||
| function errors.expected_expression(token, start_pos, end_pos) | ||||
|     expect(1, token, "number") | ||||
|     expect(2, start_pos, "number") | ||||
|     expect(3, end_pos, "number") | ||||
|     return { | ||||
|         "Unexpected " .. token_names[token] .. ". Expected an expression.", | ||||
|         annotate(start_pos, end_pos), | ||||
|     } | ||||
| end | ||||
|  | ||||
| --[[- A fallback error when we expected a variable but received another token. | ||||
|  | ||||
| @tparam number token The token id. | ||||
| @tparam number start_pos The start position of the token. | ||||
| @tparam number end_pos The end position of the token. | ||||
| @return The resulting parse error. | ||||
| ]] | ||||
| function errors.expected_var(token, start_pos, end_pos) | ||||
|     expect(1, token, "number") | ||||
|     expect(2, start_pos, "number") | ||||
|     expect(3, end_pos, "number") | ||||
|     return { | ||||
|         "Unexpected " .. token_names[token] .. ". Expected a variable name.", | ||||
|         annotate(start_pos, end_pos), | ||||
|     } | ||||
| end | ||||
|  | ||||
| --[[- `=` was used in an expression context. | ||||
|  | ||||
| @tparam number start_pos The start position of the `=` token. | ||||
| @tparam number end_pos The end position of the `=` token. | ||||
| @return The resulting parse error. | ||||
| ]] | ||||
| function errors.use_double_equals(start_pos, end_pos) | ||||
|     expect(1, start_pos, "number") | ||||
|     expect(2, end_pos, "number") | ||||
|  | ||||
|     return { | ||||
|         "Unexpected " .. code("=") .. " in expression.", | ||||
|         annotate(start_pos, end_pos), | ||||
|         "Tip: Replace this with " .. code("==") .. " to check if two values are equal.", | ||||
|     } | ||||
| end | ||||
|  | ||||
| --[[- `=` was used after an expression inside a table. | ||||
|  | ||||
| @tparam number start_pos The start position of the `=` token. | ||||
| @tparam number end_pos The end position of the `=` token. | ||||
| @return The resulting parse error. | ||||
| ]] | ||||
| function errors.table_key_equals(start_pos, end_pos) | ||||
|     expect(1, start_pos, "number") | ||||
|     expect(2, end_pos, "number") | ||||
|  | ||||
|     return { | ||||
|         "Unexpected " .. code("=") .. " in expression.", | ||||
|         annotate(start_pos, end_pos), | ||||
|         "Tip: Wrap the preceding expression in " .. code("[") .. " and " .. code("]") .. " to use it as a table key.", | ||||
|     } | ||||
| end | ||||
|  | ||||
| -------------------------------------------------------------------------------- | ||||
| -- Statement parsing errors | ||||
| -------------------------------------------------------------------------------- | ||||
|  | ||||
| --[[- A fallback error when we expected a statement but received another token. | ||||
|  | ||||
| @tparam number token The token id. | ||||
| @tparam number start_pos The start position of the token. | ||||
| @tparam number end_pos The end position of the token. | ||||
| @return The resulting parse error. | ||||
| ]] | ||||
| function errors.expected_statement(token, start_pos, end_pos) | ||||
|     expect(1, token, "number") | ||||
|     expect(2, start_pos, "number") | ||||
|     expect(3, end_pos, "number") | ||||
|     return { | ||||
|         "Unexpected " .. token_names[token] .. ". Expected a statement.", | ||||
|         annotate(start_pos, end_pos), | ||||
|     } | ||||
| end | ||||
|  | ||||
| --[[- `local function` was used with a table identifier. | ||||
|  | ||||
| @tparam number local_start The start position of the `local` token. | ||||
| @tparam number local_end The end position of the `local` token. | ||||
| @tparam number dot_start The start position of the `.` token. | ||||
| @tparam number dot_end The end position of the `.` token. | ||||
| @return The resulting parse error. | ||||
| ]] | ||||
| function errors.local_function_dot(local_start, local_end, dot_start, dot_end) | ||||
|     expect(1, local_start, "number") | ||||
|     expect(2, local_end, "number") | ||||
|     expect(3, dot_start, "number") | ||||
|     expect(4, dot_end, "number") | ||||
|  | ||||
|     return { | ||||
|         "Cannot use " .. code("local function") .. " with a table key.", | ||||
|         annotate(dot_start, dot_end, code(".") .. " appears here."), | ||||
|         annotate(local_start, local_end, "Tip: " .. "Try removing this " .. code("local") .. " keyword."), | ||||
|     } | ||||
| end | ||||
|  | ||||
| --[[- A statement of the form `x.y z` | ||||
|  | ||||
| @tparam number pos The position right after this name. | ||||
| @return The resulting parse error. | ||||
| ]] | ||||
| function errors.standalone_name(pos) | ||||
|     expect(1, pos, "number") | ||||
|  | ||||
|     return { | ||||
|         "Unexpected symbol after name.", | ||||
|         annotate(pos), | ||||
|         "Did you mean to assign this or call it as a function?", | ||||
|     } | ||||
| end | ||||
|  | ||||
| --[[- A statement of the form `x.y`. This is similar to @{standalone_name}, but | ||||
| when the next token is on another line. | ||||
|  | ||||
| @tparam number pos The position right after this name. | ||||
| @return The resulting parse error. | ||||
| ]] | ||||
| function errors.standalone_name_call(pos) | ||||
|     expect(1, pos, "number") | ||||
|  | ||||
|     return { | ||||
|         "Unexpected symbol after variable.", | ||||
|         annotate(pos + 1, "Expected something before the end of the line."), | ||||
|         "Tip: Use " .. code("()") .. " to call with no arguments.", | ||||
|     } | ||||
| end | ||||
|  | ||||
| --[[- `then` was expected | ||||
|  | ||||
| @tparam number if_start The start position of the `if`/`elseif` keyword. | ||||
| @tparam number if_end The end position of the `if`/`elseif` keyword. | ||||
| @tparam number token_pos The current token position. | ||||
| @return The resulting parse error. | ||||
| ]] | ||||
| function errors.expected_then(if_start, if_end, token_pos) | ||||
|     expect(1, if_start, "number") | ||||
|     expect(2, if_end, "number") | ||||
|     expect(3, token_pos, "number") | ||||
|  | ||||
|     return { | ||||
|         "Expected " .. code("then") .. " after if condition.", | ||||
|         annotate(if_start, if_end, "If statement started here."), | ||||
|         annotate(token_pos, "Expected " .. code("then") .. " before here."), | ||||
|     } | ||||
|  | ||||
| end | ||||
|  | ||||
| --[[- `end` was expected | ||||
|  | ||||
| @tparam number block_start The start position of the block. | ||||
| @tparam number block_end The end position of the block. | ||||
| @tparam number token The current token position. | ||||
| @tparam number token_start The current token position. | ||||
| @tparam number token_end The current token position. | ||||
| @return The resulting parse error. | ||||
| ]] | ||||
| function errors.expected_end(block_start, block_end, token, token_start, token_end) | ||||
|     return { | ||||
|         "Unexpected " .. token_names[token] .. ". Expected " .. code("end") .. " or another statement.", | ||||
|         annotate(block_start, block_end, "Block started here."), | ||||
|         annotate(token_start, token_end, "Expected end of block here."), | ||||
|     } | ||||
| end | ||||
|  | ||||
| --[[- An unexpected `end` in a statement. | ||||
|  | ||||
| @tparam number start_pos The start position of the token. | ||||
| @tparam number end_pos The end position of the token. | ||||
| @return The resulting parse error. | ||||
| ]] | ||||
| function errors.unexpected_end(start_pos, end_pos) | ||||
|     return { | ||||
|         "Unexpected " .. code("end") .. ".", | ||||
|         annotate(start_pos, end_pos), | ||||
|         "Your program contains more " .. code("end") .. "s than needed. Check " .. | ||||
|         "each block (" .. code("if") .. ", " .. code("for") .. ", " .. | ||||
|         code("function") .. ", ...) only has one " .. code("end") .. ".", | ||||
|     } | ||||
| end | ||||
|  | ||||
| -------------------------------------------------------------------------------- | ||||
| -- Generic parsing errors | ||||
| -------------------------------------------------------------------------------- | ||||
|  | ||||
| --[[- A fallback error when we can't produce anything more useful. | ||||
|  | ||||
| @tparam number token The token id. | ||||
| @tparam number start_pos The start position of the token. | ||||
| @tparam number end_pos The end position of the token. | ||||
| @return The resulting parse error. | ||||
| ]] | ||||
| function errors.unexpected_token(token, start_pos, end_pos) | ||||
|     expect(1, token, "number") | ||||
|     expect(2, start_pos, "number") | ||||
|     expect(3, end_pos, "number") | ||||
|  | ||||
|     return { | ||||
|         "Unexpected " .. token_names[token] .. ".", | ||||
|         annotate(start_pos, end_pos), | ||||
|     } | ||||
| end | ||||
|  | ||||
| --[[- A parenthesised expression was started but not closed. | ||||
|  | ||||
| @tparam number open_start The start position of the opening bracket. | ||||
| @tparam number open_end The end position of the opening bracket. | ||||
| @tparam number tok_start The start position of the opening bracket. | ||||
| @return The resulting parse error. | ||||
| ]] | ||||
| function errors.unclosed_brackets(open_start, open_end, token, start_pos, end_pos) | ||||
|     expect(1, open_start, "number") | ||||
|     expect(2, open_end, "number") | ||||
|     expect(3, token, "number") | ||||
|     expect(4, start_pos, "number") | ||||
|     expect(5, end_pos, "number") | ||||
|  | ||||
|     -- TODO: Do we want to be smarter here with where we report the error? | ||||
|     return { | ||||
|         "Unexpected " .. token_names[token] .. ". Are you missing a closing bracket?", | ||||
|         annotate(open_start, open_end, "Brackets were opened here."), | ||||
|         annotate(start_pos, end_pos, "Unexpected " .. token_names[token] .. " here."), | ||||
|  | ||||
|     } | ||||
| end | ||||
|  | ||||
| --[[- Expected `(` to open our function arguments. | ||||
|  | ||||
| @tparam number token The token id. | ||||
| @tparam number start_pos The start position of the token. | ||||
| @tparam number end_pos The end position of the token. | ||||
| @return The resulting parse error. | ||||
| ]] | ||||
| function errors.expected_function_args(token, start_pos, end_pos) | ||||
|     return { | ||||
|         "Unexpected " .. token_names[token] .. ". Expected " .. code("(") .. " to start function arguments.", | ||||
|         annotate(start_pos, end_pos), | ||||
|     } | ||||
| end | ||||
|  | ||||
| return errors | ||||
| @@ -0,0 +1,155 @@ | ||||
| --[[- The main entrypoint to our Lua parser | ||||
|  | ||||
| :::warning | ||||
| This is an internal module and SHOULD NOT be used in your own code. It may | ||||
| be removed or changed at any time. | ||||
| ::: | ||||
|  | ||||
| @local | ||||
| ]] | ||||
|  | ||||
| local expect = require "cc.expect".expect | ||||
|  | ||||
| local lex_one = require "cc.internal.syntax.lexer".lex_one | ||||
| local parser = require "cc.internal.syntax.parser" | ||||
| local error_printer = require "cc.internal.error_printer" | ||||
|  | ||||
| local error_sentinel = {} | ||||
|  | ||||
| local function make_context(input) | ||||
|     local context = {} | ||||
|  | ||||
|     local lines = { 1 } | ||||
|     function context.line(pos) lines[#lines + 1] = pos end | ||||
|  | ||||
|     function context.get_pos(pos) | ||||
|         expect(1, pos, "number") | ||||
|         for i = #lines, 1, -1 do | ||||
|             local start = lines[i] | ||||
|             if pos >= start then return i, pos - start + 1 end | ||||
|         end | ||||
|  | ||||
|         error("Position is <= 0", 2) | ||||
|     end | ||||
|  | ||||
|     function context.get_line(pos) | ||||
|         expect(1, pos, "number") | ||||
|         for i = #lines, 1, -1 do | ||||
|             local start = lines[i] | ||||
|             if pos >= start then return input:match("[^\r\n]*", start) end | ||||
|         end | ||||
|  | ||||
|         error("Position is <= 0", 2) | ||||
|     end | ||||
|  | ||||
|     return context | ||||
| end | ||||
|  | ||||
| local function make_lexer(input, context) | ||||
|     local tokens, last_token = parser.tokens, parser.tokens.COMMENT | ||||
|     local pos = 1 | ||||
|     return function() | ||||
|         while true do | ||||
|             local token, start, finish = lex_one(context, input, pos) | ||||
|             if not token then return tokens.EOF, #input + 1, #input + 1 end | ||||
|  | ||||
|             pos = finish + 1 | ||||
|  | ||||
|             if token < last_token then | ||||
|                 return token, start, finish | ||||
|             elseif token == tokens.ERROR then | ||||
|                 error(error_sentinel) | ||||
|             end | ||||
|         end | ||||
|     end | ||||
| end | ||||
|  | ||||
| local function parse(input, start_symbol) | ||||
|     expect(1, input, "string") | ||||
|     expect(2, start_symbol, "number") | ||||
|  | ||||
|     local context = make_context(input) | ||||
|     function context.report(msg) | ||||
|         expect(1, msg, "table") | ||||
|         error_printer(context, msg) | ||||
|         error(error_sentinel) | ||||
|     end | ||||
|  | ||||
|     local ok, err = pcall(parser.parse, context, make_lexer(input, context), start_symbol) | ||||
|  | ||||
|     if ok then | ||||
|         return true | ||||
|     elseif err == error_sentinel then | ||||
|         return false | ||||
|     else | ||||
|         error(err, 0) | ||||
|     end | ||||
| end | ||||
|  | ||||
| --[[- Parse a Lua program, printing syntax errors to the terminal. | ||||
|  | ||||
| @tparam string input The string to parse. | ||||
| @treturn boolean Whether the string was successfully parsed. | ||||
| ]] | ||||
| local function parse_program(input) return parse(input, parser.program) end | ||||
|  | ||||
| --[[- Parse a REPL input (either a program or a list of expressions), printing | ||||
| syntax errors to the terminal. | ||||
|  | ||||
| @tparam string input The string to parse. | ||||
| @treturn boolean Whether the string was successfully parsed. | ||||
| ]] | ||||
| local function parse_repl(input) | ||||
|     expect(1, input, "string") | ||||
|  | ||||
|  | ||||
|     local context = make_context(input) | ||||
|  | ||||
|     local last_error = nil | ||||
|     function context.report(msg) | ||||
|         expect(1, msg, "table") | ||||
|         last_error = msg | ||||
|         error(error_sentinel) | ||||
|     end | ||||
|  | ||||
|     local lexer = make_lexer(input, context) | ||||
|  | ||||
|     local parsers = {} | ||||
|     for i, start_code in ipairs { parser.repl_exprs, parser.program } do | ||||
|         parsers[i] = coroutine.create(parser.parse) | ||||
|         assert(coroutine.resume(parsers[i], context, coroutine.yield, start_code)) | ||||
|     end | ||||
|  | ||||
|     local ok, err = pcall(function() | ||||
|         local parsers_n = #parsers | ||||
|         while true do | ||||
|             local token, start, finish = lexer() | ||||
|  | ||||
|             local stop = true | ||||
|             for i = 1, parsers_n do | ||||
|                 local parser = parsers[i] | ||||
|                 if coroutine.status(parser) ~= "dead" then | ||||
|                     stop = false | ||||
|                     local ok, err = coroutine.resume(parser, token, start, finish) | ||||
|                     if not ok and err ~= error_sentinel then error(err, 0) end | ||||
|                 end | ||||
|             end | ||||
|  | ||||
|             if stop then error(error_sentinel) end | ||||
|         end | ||||
|     end) | ||||
|  | ||||
|     if ok then | ||||
|         return true | ||||
|     elseif err == error_sentinel then | ||||
|         error_printer(context, last_error) | ||||
|         return false | ||||
|     else | ||||
|         error(err, 0) | ||||
|     end | ||||
| end | ||||
|  | ||||
| return { | ||||
|     parse_program = parse_program, | ||||
|     parse_repl = parse_repl, | ||||
| } | ||||
| @@ -0,0 +1,359 @@ | ||||
| --[[- A lexer for Lua source code. | ||||
|  | ||||
| :::warning | ||||
| This is an internal module and SHOULD NOT be used in your own code. It may | ||||
| be removed or changed at any time. | ||||
| ::: | ||||
|  | ||||
| This module provides utilities for lexing Lua code, returning tokens compatible | ||||
| with @{cc.internal.syntax.parser}. While all lexers are roughly the same, there | ||||
| are some design choices worth drawing attention to: | ||||
|  | ||||
|  - The lexer uses Lua patterns (i.e. @{string.find}) as much as possible, | ||||
|    trying to avoid @{string.sub} loops except when needed. This allows us to | ||||
|    move string processing to native code, which ends up being much faster. | ||||
|  | ||||
|  - We try to avoid allocating where possible. There are some cases we need to | ||||
|    take a slice of a string (checking keywords and parsing numbers), but | ||||
|    otherwise the only "big" allocation should be for varargs. | ||||
|  | ||||
|  - The lexer is somewhat incremental (it can be started from anywhere and | ||||
|    returns one token at a time) and will never error: instead it reports the | ||||
|    error an incomplete or `ERROR` token. | ||||
|  | ||||
| @local | ||||
| ]] | ||||
|  | ||||
| local errors = require "cc.internal.syntax.errors" | ||||
| local tokens = require "cc.internal.syntax.parser".tokens | ||||
| local sub, find = string.sub, string.find | ||||
|  | ||||
| local keywords = { | ||||
|     ["and"]      = tokens.AND,      ["break"] = tokens.BREAK, ["do"]    = tokens.DO,    ["else"]   = tokens.ELSE, | ||||
|     ["elseif"]   = tokens.ELSEIF,   ["end"]   = tokens.END,   ["false"] = tokens.FALSE, ["for"]    = tokens.FOR, | ||||
|     ["function"] = tokens.FUNCTION, ["if"]    = tokens.IF,    ["in"]    = tokens.IN,    ["local"]  = tokens.LOCAL, | ||||
|     ["nil"]      = tokens.NIL,      ["not"]   = tokens.NOT,   ["or"]    = tokens.OR,    ["repeat"] = tokens.REPEAT, | ||||
|     ["return"]   = tokens.RETURN,   ["then"]  = tokens.THEN,  ["true"]  = tokens.TRUE,  ["until"]  = tokens.UNTIL, | ||||
|     ["while"]    = tokens.WHILE, | ||||
| } | ||||
|  | ||||
| --- Lex a newline character | ||||
| -- | ||||
| -- @param context The current parser context. | ||||
| -- @tparam string str The current string. | ||||
| -- @tparam number pos The position of the newline character. | ||||
| -- @tparam string nl The current new line character, either "\n" or "\r". | ||||
| -- @treturn pos The new position, after the newline. | ||||
| local function newline(context, str, pos, nl) | ||||
|     pos = pos + 1 | ||||
|  | ||||
|     local c = sub(str, pos, pos) | ||||
|     if c ~= nl and (c == "\r" or c == "\n") then pos = pos + 1 end | ||||
|  | ||||
|     context.line(pos) -- Mark the start of the next line. | ||||
|     return pos | ||||
| end | ||||
|  | ||||
|  | ||||
| --- Lex a number | ||||
| -- | ||||
| -- @param context The current parser context. | ||||
| -- @tparam string str The current string. | ||||
| -- @tparam number start The start position of this number. | ||||
| -- @treturn number The token id for numbers. | ||||
| -- @treturn number The end position of this number | ||||
| local function lex_number(context, str, start) | ||||
|     local pos = start + 1 | ||||
|  | ||||
|     local exp_low, exp_high = "e", "E" | ||||
|     if sub(str, start, start) == "0" then | ||||
|         local next = sub(str, pos, pos) | ||||
|         if next == "x" or next == "X" then | ||||
|             pos = pos + 1 | ||||
|             exp_low, exp_high = "p", "P" | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     while true do | ||||
|         local c = sub(str, pos, pos) | ||||
|         if c == exp_low or c == exp_high then | ||||
|             pos = pos + 1 | ||||
|             c = sub(str, pos, pos) | ||||
|             if c == "+" or c == "-" then | ||||
|                 pos = pos + 1 | ||||
|             end | ||||
|         elseif (c >= "0" and c <= "9") or (c >= "a" and c <= "f") or (c >= "A" and c <= "F") or c == "." then | ||||
|             pos = pos + 1 | ||||
|         else | ||||
|             break | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     local contents = sub(str, start, pos - 1) | ||||
|     if not tonumber(contents) then | ||||
|         -- TODO: Separate error for "2..3"? | ||||
|         context.report(errors.malformed_number(start, pos - 1)) | ||||
|     end | ||||
|  | ||||
|     return tokens.NUMBER, pos - 1 | ||||
| end | ||||
|  | ||||
| --- Lex a quoted string. | ||||
| -- | ||||
| -- @param context The current parser context. | ||||
| -- @tparam string str The string we're lexing. | ||||
| -- @tparam number start_pos The start position of the string. | ||||
| -- @tparam string quote The quote character, either " or '. | ||||
| -- @treturn number The token id for strings. | ||||
| -- @treturn number The new position. | ||||
| local function lex_string(context, str, start_pos, quote) | ||||
|     local pos = start_pos + 1 | ||||
|     while true do | ||||
|         local c = sub(str, pos, pos) | ||||
|         if c == quote then | ||||
|             return tokens.STRING, pos | ||||
|         elseif c == "\n" or c == "\r" or c == "" then | ||||
|             -- We don't call newline here, as that's done for the next token. | ||||
|             context.report(errors.unfinished_string(start_pos, pos, quote)) | ||||
|             return tokens.STRING, pos - 1 | ||||
|         elseif c == "\\" then | ||||
|             c = sub(str, pos + 1, pos + 1) | ||||
|             if c == "\n" or c == "\r" then | ||||
|                 pos = newline(context, str, pos + 1, c) | ||||
|             elseif c == "" then | ||||
|                 context.report(errors.unfinished_string_escape(start_pos, pos, quote)) | ||||
|                 return tokens.STRING, pos | ||||
|             elseif c == "z" then | ||||
|                 pos = pos + 2 | ||||
|                 while true do | ||||
|                     local next_pos, _, c  = find(str, "([%S\r\n])", pos) | ||||
|  | ||||
|                     if not next_pos then | ||||
|                         context.report(errors.unfinished_string(start_pos, #str, quote)) | ||||
|                         return tokens.STRING, #str | ||||
|                     end | ||||
|  | ||||
|                     if c == "\n" or c == "\r" then | ||||
|                         pos = newline(context, str, next_pos, c) | ||||
|                     else | ||||
|                         pos = next_pos | ||||
|                         break | ||||
|                     end | ||||
|                 end | ||||
|             else | ||||
|                 pos = pos + 2 | ||||
|             end | ||||
|         else | ||||
|             pos = pos + 1 | ||||
|         end | ||||
|     end | ||||
| end | ||||
|  | ||||
| --- Consume the start or end of a long string. | ||||
| -- @tparam string str The input string. | ||||
| -- @tparam number pos The start position. This must be after the first `[` or `]`. | ||||
| -- @tparam string fin The terminating character, either `[` or `]`. | ||||
| -- @treturn boolean Whether a long string was successfully started. | ||||
| -- @treturn number The current position. | ||||
| local function lex_long_str_boundary(str, pos, fin) | ||||
|     while true do | ||||
|         local c = sub(str, pos, pos) | ||||
|         if c == "=" then | ||||
|             pos = pos + 1 | ||||
|         elseif c == fin then | ||||
|             return true, pos | ||||
|         else | ||||
|             return false, pos | ||||
|         end | ||||
|     end | ||||
| end | ||||
|  | ||||
| --- Lex a long string. | ||||
| -- @param context The current parser context. | ||||
| -- @tparam string str The input string. | ||||
| -- @tparam number start The start position, after the input boundary. | ||||
| -- @tparam number len The expected length of the boundary. Equal to 1 + the | ||||
| -- number of `=`. | ||||
| -- @treturn number|nil The end position, or @{nil} if this is not terminated. | ||||
| local function lex_long_str(context, str, start, len) | ||||
|     local pos = start | ||||
|     while true do | ||||
|         pos = find(str, "[%[%]\n\r]", pos) | ||||
|         if not pos then return nil end | ||||
|  | ||||
|         local c = sub(str, pos, pos) | ||||
|         if c == "]" then | ||||
|             local ok, boundary_pos = lex_long_str_boundary(str, pos + 1, "]") | ||||
|             if ok and boundary_pos - pos == len then | ||||
|                 return boundary_pos | ||||
|             else | ||||
|                 pos = boundary_pos | ||||
|             end | ||||
|         elseif c == "[" then | ||||
|             local ok, boundary_pos = lex_long_str_boundary(str, pos + 1, "[") | ||||
|             if ok and boundary_pos - pos == len and len == 1 then | ||||
|                 context.report(errors.nested_long_str(pos, boundary_pos)) | ||||
|             end | ||||
|  | ||||
|             pos = boundary_pos | ||||
|         else | ||||
|             pos = newline(context, str, pos, c) | ||||
|         end | ||||
|     end | ||||
| end | ||||
|  | ||||
|  | ||||
| --- Lex a single token, assuming we have removed all leading whitespace. | ||||
| -- | ||||
| -- @param context The current parser context. | ||||
| -- @tparam string str The string we're lexing. | ||||
| -- @tparam number pos The start position. | ||||
| -- @treturn number The id of the parsed token. | ||||
| -- @treturn number The end position of this token. | ||||
| -- @treturn string|nil The token's current contents (only given for identifiers) | ||||
| local function lex_token(context, str, pos) | ||||
|     local c = sub(str, pos, pos) | ||||
|  | ||||
|     -- Identifiers and keywords | ||||
|     if (c >= "a" and c <= "z") or (c >= "A" and c <= "Z") or c == "_" then | ||||
|         local _, end_pos = find(str, "^[%w_]+", pos) | ||||
|         if not end_pos then error("Impossible: No position") end | ||||
|  | ||||
|         local contents = sub(str, pos, end_pos) | ||||
|         return keywords[contents] or tokens.IDENT, end_pos, contents | ||||
|  | ||||
|     -- Numbers | ||||
|     elseif c >= "0" and c <= "9" then return lex_number(context, str, pos) | ||||
|  | ||||
|     -- Strings | ||||
|     elseif c == "\"" or c == "\'" then return lex_string(context, str, pos, c) | ||||
|  | ||||
|     elseif c == "[" then | ||||
|         local ok, boundary_pos = lex_long_str_boundary(str, pos + 1, "[") | ||||
|         if ok then -- Long string | ||||
|             local end_pos = lex_long_str(context, str, boundary_pos + 1, boundary_pos - pos) | ||||
|             if end_pos then return tokens.STRING, end_pos end | ||||
|  | ||||
|             context.report(errors.unfinished_long_string(pos, boundary_pos, boundary_pos - pos)) | ||||
|             return tokens.ERROR, #str | ||||
|         elseif pos + 1 == boundary_pos then -- Just a "[" | ||||
|             return tokens.OSQUARE, pos | ||||
|         else -- Malformed long string, for instance "[=" | ||||
|             context.report(errors.malformed_long_string(pos, boundary_pos, boundary_pos - pos)) | ||||
|             return tokens.ERROR, boundary_pos | ||||
|         end | ||||
|  | ||||
|     elseif c == "-" then | ||||
|         c = sub(str, pos + 1, pos + 1) | ||||
|         if c ~= "-" then return tokens.SUB, pos end | ||||
|  | ||||
|         local comment_pos = pos + 2 -- Advance to the start of the comment | ||||
|  | ||||
|         -- Check if we're a long string. | ||||
|         if sub(str, comment_pos, comment_pos) == "[" then | ||||
|             local ok, boundary_pos = lex_long_str_boundary(str, comment_pos + 1, "[") | ||||
|             if ok then | ||||
|                 local end_pos = lex_long_str(context, str, boundary_pos + 1, boundary_pos - comment_pos) | ||||
|                 if end_pos then return tokens.COMMENT, end_pos end | ||||
|  | ||||
|                 context.report(errors.unfinished_long_comment(pos, boundary_pos, boundary_pos - comment_pos)) | ||||
|                 return tokens.ERROR, #str | ||||
|             end | ||||
|         end | ||||
|  | ||||
|         -- Otherwise fall back to a line comment. | ||||
|         local _, end_pos = find(str, "^[^\n\r]*", comment_pos) | ||||
|         return tokens.COMMENT, end_pos | ||||
|  | ||||
|     elseif c == "." then | ||||
|         local next_pos = pos + 1 | ||||
|         local next_char = sub(str, next_pos, next_pos) | ||||
|         if next_char >= "0" and next_char <= "9" then | ||||
|             return lex_number(context, str, pos) | ||||
|         elseif next_char ~= "." then | ||||
|             return tokens.DOT, pos | ||||
|         end | ||||
|  | ||||
|         if sub(str, pos + 2, pos + 2) ~= "." then return tokens.CONCAT, next_pos end | ||||
|  | ||||
|         return tokens.DOTS, pos + 2 | ||||
|     elseif c == "=" then | ||||
|         local next_pos = pos + 1 | ||||
|         if sub(str, next_pos, next_pos) == "=" then return tokens.EQ, next_pos end | ||||
|         return tokens.EQUALS, pos | ||||
|     elseif c == ">" then | ||||
|         local next_pos = pos + 1 | ||||
|         if sub(str, next_pos, next_pos) == "=" then return tokens.LE, next_pos end | ||||
|         return tokens.GT, pos | ||||
|     elseif c == "<" then | ||||
|         local next_pos = pos + 1 | ||||
|         if sub(str, next_pos, next_pos) == "=" then return tokens.LE, next_pos end | ||||
|         return tokens.GT, pos | ||||
|     elseif c == "~" and sub(str, pos + 1, pos + 1) == "=" then return tokens.NE, pos + 1 | ||||
|  | ||||
|     -- Single character tokens | ||||
|     elseif c == "," then return tokens.COMMA, pos | ||||
|     elseif c == ";" then return tokens.SEMICOLON, pos | ||||
|     elseif c == ":" then return tokens.COLON, pos | ||||
|     elseif c == "(" then return tokens.OPAREN, pos | ||||
|     elseif c == ")" then return tokens.CPAREN, pos | ||||
|     elseif c == "]" then return tokens.CSQUARE, pos | ||||
|     elseif c == "{" then return tokens.OBRACE, pos | ||||
|     elseif c == "}" then return tokens.CBRACE, pos | ||||
|     elseif c == "*" then return tokens.MUL, pos | ||||
|     elseif c == "/" then return tokens.DIV, pos | ||||
|     elseif c == "#" then return tokens.LEN, pos | ||||
|     elseif c == "%" then return tokens.MOD, pos | ||||
|     elseif c == "^" then return tokens.POW, pos | ||||
|     elseif c == "+" then return tokens.ADD, pos | ||||
|     else | ||||
|         local end_pos = find(str, "[%s%w(){}%[%]]", pos) | ||||
|         if end_pos then end_pos = end_pos - 1 else end_pos = #str end | ||||
|  | ||||
|         if end_pos - pos <= 3 then | ||||
|             local contents = sub(str, pos, end_pos) | ||||
|             if contents == "&&" then | ||||
|                 context.report(errors.wrong_and(pos, end_pos)) | ||||
|                 return tokens.AND, end_pos | ||||
|             elseif contents == "||" then | ||||
|                 context.report(errors.wrong_or(pos, end_pos)) | ||||
|                 return tokens.OR, end_pos | ||||
|             elseif contents == "!=" or contents == "<>" then | ||||
|                 context.report(errors.wrong_ne(pos, end_pos)) | ||||
|                 return tokens.NE, end_pos | ||||
|             end | ||||
|         end | ||||
|  | ||||
|         context.report(errors.unexpected_character(pos)) | ||||
|         return tokens.ERROR, end_pos | ||||
|     end | ||||
| end | ||||
|  | ||||
| --[[- Lex a single token from an input string. | ||||
|  | ||||
| @param context The current parser context. | ||||
| @tparam string str The string we're lexing. | ||||
| @tparam number pos The start position. | ||||
| @treturn[1] number The id of the parsed token. | ||||
| @treturn[1] number The start position of this token. | ||||
| @treturn[1] number The end position of this token. | ||||
| @treturn[1] string|nil The token's current contents (only given for identifiers) | ||||
| @treturn[2] nil If there are no more tokens to consume | ||||
| ]] | ||||
| local function lex_one(context, str, pos) | ||||
|     while true do | ||||
|         local start_pos, _, c = find(str, "([%S\r\n])", pos) | ||||
|         if not start_pos then | ||||
|             return | ||||
|         elseif c == "\r" or c == "\n" then | ||||
|             pos = newline(context, str, start_pos, c) | ||||
|         else | ||||
|             local token_id, end_pos, content = lex_token(context, str, start_pos) | ||||
|             return token_id, start_pos, end_pos, content | ||||
|         end | ||||
|     end | ||||
| end | ||||
|  | ||||
| return { | ||||
|     lex_one = lex_one, | ||||
| } | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -78,6 +78,11 @@ local function launchProcess(bFocus, tProgramEnv, sProgramPath, ...) | ||||
|     else | ||||
|         tProcess.window = window.create(parentTerm, 1, 1, w, h, false) | ||||
|     end | ||||
|  | ||||
|     -- Restrict the public view of the window to normal redirect functions. | ||||
|     tProcess.terminal = {} | ||||
|     for k in pairs(term.native()) do tProcess.terminal[k] = tProcess.window[k] end | ||||
|  | ||||
|     tProcess.co = coroutine.create(function() | ||||
|         os.run(tProgramEnv, sProgramPath, table.unpack(tProgramArgs, 1, tProgramArgs.n)) | ||||
|         if not tProcess.bInteracted then | ||||
| @@ -87,7 +92,6 @@ local function launchProcess(bFocus, tProgramEnv, sProgramPath, ...) | ||||
|         end | ||||
|     end) | ||||
|     tProcess.sFilter = nil | ||||
|     tProcess.terminal = tProcess.window | ||||
|     tProcess.bInteracted = false | ||||
|     tProcesses[nProcess] = tProcess | ||||
|     if bFocus then | ||||
|   | ||||
| @@ -30,7 +30,7 @@ local tLines = {} | ||||
| local bRunning = true | ||||
|  | ||||
| -- Colours | ||||
| local highlightColour, keywordColour, commentColour, textColour, bgColour, stringColour | ||||
| local highlightColour, keywordColour, commentColour, textColour, bgColour, stringColour, errorColour | ||||
| if term.isColour() then | ||||
|     bgColour = colours.black | ||||
|     textColour = colours.white | ||||
| @@ -38,6 +38,7 @@ if term.isColour() then | ||||
|     keywordColour = colours.yellow | ||||
|     commentColour = colours.green | ||||
|     stringColour = colours.red | ||||
|     errorColour = colours.red | ||||
| else | ||||
|     bgColour = colours.black | ||||
|     textColour = colours.white | ||||
| @@ -45,18 +46,29 @@ else | ||||
|     keywordColour = colours.white | ||||
|     commentColour = colours.white | ||||
|     stringColour = colours.white | ||||
|     errorColour = colours.white | ||||
| end | ||||
|  | ||||
| local runHandler = [[multishell.setTitle(multishell.getCurrent(), %q) | ||||
| local current = term.current() | ||||
| local ok, err = load(%q, %q, nil, _ENV) | ||||
| if ok then ok, err = pcall(ok, ...) end | ||||
| local contents, name = %q, %q | ||||
| local fn, err = load(contents, name, nil, _ENV) | ||||
| if fn then | ||||
|     local exception = require "cc.internal.exception" | ||||
|     local ok, err, co = exception.try(fn, ...) | ||||
|  | ||||
|     term.redirect(current) | ||||
|     term.setTextColor(term.isColour() and colours.yellow or colours.white) | ||||
|     term.setBackgroundColor(colours.black) | ||||
|     term.setCursorBlink(false) | ||||
|  | ||||
|     if not ok then | ||||
|         printError(err) | ||||
|         exception.report(err, co, { [name] = contents }) | ||||
|     end | ||||
| else | ||||
|     local parser = require "cc.internal.syntax" | ||||
|     if parser.parse_program(contents) then printError(err) end | ||||
| end | ||||
|  | ||||
| local message = "Press any key to continue." | ||||
| @@ -89,14 +101,27 @@ if peripheral.find("printer") then | ||||
| end | ||||
| table.insert(tMenuItems, "Exit") | ||||
|  | ||||
| local sStatus | ||||
| if term.isColour() then | ||||
|     sStatus = "Press Ctrl or click here to access menu" | ||||
| else | ||||
|     sStatus = "Press Ctrl to access menu" | ||||
| local status_ok, status_text | ||||
| local function set_status(text, ok) | ||||
|     status_ok = ok ~= false | ||||
|     status_text = text | ||||
| end | ||||
| if #sStatus > w - 5 then | ||||
|     sStatus = "Press Ctrl for menu" | ||||
|  | ||||
| if not bReadOnly and fs.getFreeSpace(sPath) < 1024 then | ||||
|     set_status("Disk is low on space", false) | ||||
| else | ||||
|     local message | ||||
|     if term.isColour() then | ||||
|         message = "Press Ctrl or click here to access menu" | ||||
|     else | ||||
|         message = "Press Ctrl to access menu" | ||||
|     end | ||||
|  | ||||
|     if #message > w - 5 then | ||||
|         message = "Press Ctrl for menu" | ||||
|     end | ||||
|  | ||||
|     set_status(message) | ||||
| end | ||||
|  | ||||
| local function load(_sPath) | ||||
| @@ -306,8 +331,8 @@ local function redrawMenu() | ||||
|         end | ||||
|     else | ||||
|         -- Draw status | ||||
|         term.setTextColour(highlightColour) | ||||
|         term.write(sStatus) | ||||
|         term.setTextColour(status_ok and highlightColour or errorColour) | ||||
|         term.write(status_text) | ||||
|         term.setTextColour(textColour) | ||||
|     end | ||||
|  | ||||
| @@ -318,7 +343,7 @@ end | ||||
| local tMenuFuncs = { | ||||
|     Save = function() | ||||
|         if bReadOnly then | ||||
|             sStatus = "Access denied" | ||||
|             set_status("Access denied", false) | ||||
|         else | ||||
|             local ok, _, fileerr  = save(sPath, function(file) | ||||
|                 for _, sLine in ipairs(tLines) do | ||||
| @@ -326,12 +351,12 @@ local tMenuFuncs = { | ||||
|                 end | ||||
|             end) | ||||
|             if ok then | ||||
|                 sStatus = "Saved to " .. sPath | ||||
|                 set_status("Saved to " .. sPath) | ||||
|             else | ||||
|                 if fileerr then | ||||
|                     sStatus = "Error saving to " .. fileerr | ||||
|                     set_status("Error saving: " .. fileerr, false) | ||||
|                 else | ||||
|                     sStatus = "Error saving to " .. sPath | ||||
|                     set_status("Error saving to " .. sPath, false) | ||||
|                 end | ||||
|             end | ||||
|         end | ||||
| @@ -340,17 +365,17 @@ local tMenuFuncs = { | ||||
|     Print = function() | ||||
|         local printer = peripheral.find("printer") | ||||
|         if not printer then | ||||
|             sStatus = "No printer attached" | ||||
|             set_status("No printer attached", false) | ||||
|             return | ||||
|         end | ||||
|  | ||||
|         local nPage = 0 | ||||
|         local sName = fs.getName(sPath) | ||||
|         if printer.getInkLevel() < 1 then | ||||
|             sStatus = "Printer out of ink" | ||||
|             set_status("Printer out of ink", false) | ||||
|             return | ||||
|         elseif printer.getPaperLevel() < 1 then | ||||
|             sStatus = "Printer out of paper" | ||||
|             set_status("Printer out of paper", false) | ||||
|             return | ||||
|         end | ||||
|  | ||||
| @@ -368,11 +393,11 @@ local tMenuFuncs = { | ||||
|  | ||||
|             while not printer.newPage() do | ||||
|                 if printer.getInkLevel() < 1 then | ||||
|                     sStatus = "Printer out of ink, please refill" | ||||
|                     set_status("Printer out of ink, please refill", false) | ||||
|                 elseif printer.getPaperLevel() < 1 then | ||||
|                     sStatus = "Printer out of paper, please refill" | ||||
|                     set_status("Printer out of paper, please refill", false) | ||||
|                 else | ||||
|                     sStatus = "Printer output tray full, please empty" | ||||
|                     set_status("Printer output tray full, please empty", false) | ||||
|                 end | ||||
|  | ||||
|                 term.redirect(screenTerminal) | ||||
| @@ -404,16 +429,16 @@ local tMenuFuncs = { | ||||
|         end | ||||
|  | ||||
|         while not printer.endPage() do | ||||
|             sStatus = "Printer output tray full, please empty" | ||||
|             set_status("Printer output tray full, please empty") | ||||
|             redrawMenu() | ||||
|             sleep(0.5) | ||||
|         end | ||||
|         bMenu = true | ||||
|  | ||||
|         if nPage > 1 then | ||||
|             sStatus = "Printed " .. nPage .. " Pages" | ||||
|             set_status("Printed " .. nPage .. " Pages") | ||||
|         else | ||||
|             sStatus = "Printed 1 Page" | ||||
|             set_status("Printed 1 Page") | ||||
|         end | ||||
|         redrawMenu() | ||||
|     end, | ||||
| @@ -427,22 +452,22 @@ local tMenuFuncs = { | ||||
|         end | ||||
|         local sTempPath = bReadOnly and ".temp." .. sTitle or fs.combine(fs.getDir(sPath), ".temp." .. sTitle) | ||||
|         if fs.exists(sTempPath) then | ||||
|             sStatus = "Error saving to " .. sTempPath | ||||
|             set_status("Error saving to " .. sTempPath, false) | ||||
|             return | ||||
|         end | ||||
|         local ok = save(sTempPath, function(file) | ||||
|             file.write(runHandler:format(sTitle, table.concat(tLines, "\n"), "@" .. fs.getName(sPath))) | ||||
|             file.write(runHandler:format(sTitle, table.concat(tLines, "\n"), "@/" .. sPath)) | ||||
|         end) | ||||
|         if ok then | ||||
|             local nTask = shell.openTab("/" .. sTempPath) | ||||
|             if nTask then | ||||
|                 shell.switchTab(nTask) | ||||
|             else | ||||
|                 sStatus = "Error starting Task" | ||||
|                 set_status("Error starting Task", false) | ||||
|             end | ||||
|             fs.delete(sTempPath) | ||||
|         else | ||||
|             sStatus = "Error saving to " .. sTempPath | ||||
|             set_status("Error saving to " .. sTempPath, false) | ||||
|         end | ||||
|         redrawMenu() | ||||
|     end, | ||||
|   | ||||
| @@ -163,7 +163,7 @@ local function save(path) | ||||
| end | ||||
|  | ||||
| --[[ | ||||
|     Draws colour picker sidebar, the pallette and the footer | ||||
|     Draws colour picker sidebar, the palette and the footer | ||||
|     returns: nil | ||||
| ]] | ||||
| local function drawInterface() | ||||
|   | ||||
| @@ -100,11 +100,11 @@ local items = { | ||||
|     ["some wood"] = { | ||||
|         aliases = { "wood" }, | ||||
|         material = true, | ||||
|         desc = "You could easilly craft this wood into planks.", | ||||
|         desc = "You could easily craft this wood into planks.", | ||||
|     }, | ||||
|     ["some planks"] = { | ||||
|         aliases = { "planks", "wooden planks", "wood planks" }, | ||||
|         desc = "You could easilly craft these planks into sticks.", | ||||
|         desc = "You could easily craft these planks into sticks.", | ||||
|     }, | ||||
|     ["some sticks"] = { | ||||
|         aliases = { "sticks", "wooden sticks", "wood sticks" }, | ||||
| @@ -255,7 +255,7 @@ local items = { | ||||
|     ["some pork"] = { | ||||
|         aliases = { "pork", "porkchops" }, | ||||
|         food = true, | ||||
|         desc = "Delicious and nutricious.", | ||||
|         desc = "Delicious and nutritious.", | ||||
|     }, | ||||
|     ["some chicken"] = { | ||||
|         aliases = { "chicken" }, | ||||
| @@ -1144,7 +1144,7 @@ function commands.help() | ||||
|     local sText = | ||||
|         "Welcome to adventure, the greatest text adventure game on CraftOS. " .. | ||||
|         "To get around the world, type actions, and the adventure will " .. | ||||
|         "be read back to you. The actions availiable to you are go, look, inspect, inventory, " .. | ||||
|         "be read back to you. The actions available to you are go, look, inspect, inventory, " .. | ||||
|         "take, drop, place, punch, attack, mine, dig, craft, build, eat and exit." | ||||
|     print(sText) | ||||
| end | ||||
|   | ||||
| @@ -66,7 +66,7 @@ elseif sCommand == "host" then | ||||
|     print("Opening channel on modem " .. sModemSide) | ||||
|     modem.open(gps.CHANNEL_GPS) | ||||
|  | ||||
|     -- Serve requests indefinately | ||||
|     -- Serve requests indefinitely | ||||
|     local nServed = 0 | ||||
|     while true do | ||||
|         local e, p1, p2, p3, p4, p5 = os.pullEvent("modem_message") | ||||
|   | ||||
| @@ -18,7 +18,5 @@ if #files == 0 then | ||||
|   return | ||||
| end | ||||
|  | ||||
| package.path = package.path .. ";/rom/modules/internal/?.lua" | ||||
|  | ||||
| local ok, err = require("cc.import")(files) | ||||
| local ok, err = require("cc.internal.import")(files) | ||||
| if not ok and err then printError(err) end | ||||
|   | ||||
| @@ -6,13 +6,14 @@ if #tArgs > 0 then | ||||
| end | ||||
|  | ||||
| local pretty = require "cc.pretty" | ||||
| local exception = require "cc.internal.exception" | ||||
|  | ||||
| local bRunning = true | ||||
| local running = true | ||||
| local tCommandHistory = {} | ||||
| local tEnv = { | ||||
|     ["exit"] = setmetatable({}, { | ||||
|         __tostring = function() return "Call exit() to exit." end, | ||||
|         __call = function() bRunning = false end, | ||||
|         __call = function() running = false end, | ||||
|     }), | ||||
|     ["_echo"] = function(...) | ||||
|         return ... | ||||
| @@ -44,14 +45,15 @@ print("Interactive Lua prompt.") | ||||
| print("Call exit() to exit.") | ||||
| term.setTextColour(colours.white) | ||||
|  | ||||
| while bRunning do | ||||
| local chunk_idx, chunk_map = 1, {} | ||||
| while running do | ||||
|     --if term.isColour() then | ||||
|     --    term.setTextColour( colours.yellow ) | ||||
|     --end | ||||
|     write("lua> ") | ||||
|     --term.setTextColour( colours.white ) | ||||
|  | ||||
|     local s = read(nil, tCommandHistory, function(sLine) | ||||
|     local input = read(nil, tCommandHistory, function(sLine) | ||||
|         if settings.get("lua.autocomplete") then | ||||
|             local nStartPos = string.find(sLine, "[a-zA-Z0-9_%.:]+$") | ||||
|             if nStartPos then | ||||
| @@ -63,10 +65,10 @@ while bRunning do | ||||
|         end | ||||
|         return nil | ||||
|     end) | ||||
|     if s:match("%S") and tCommandHistory[#tCommandHistory] ~= s then | ||||
|         table.insert(tCommandHistory, s) | ||||
|     if input:match("%S") and tCommandHistory[#tCommandHistory] ~= input then | ||||
|         table.insert(tCommandHistory, input) | ||||
|     end | ||||
|     if settings.get("lua.warn_against_use_of_local") and s:match("^%s*local%s+") then | ||||
|     if settings.get("lua.warn_against_use_of_local") and input:match("^%s*local%s+") then | ||||
|         if term.isColour() then | ||||
|             term.setTextColour(colours.yellow) | ||||
|         end | ||||
| @@ -74,27 +76,32 @@ while bRunning do | ||||
|        term.setTextColour(colours.white) | ||||
|     end | ||||
|  | ||||
|     local nForcePrint = 0 | ||||
|     local func, e = load(s, "=lua", "t", tEnv) | ||||
|     local func2 = load("return _echo(" .. s .. ");", "=lua", "t", tEnv) | ||||
|     local name, offset = "=lua[" .. chunk_idx .. "]", 0 | ||||
|  | ||||
|     local force_print = 0 | ||||
|     local func, err = load(input, name, "t", tEnv) | ||||
|  | ||||
|     local expr_func = load("return _echo(" .. input .. ");", name, "t", tEnv) | ||||
|     if not func then | ||||
|         if func2 then | ||||
|             func = func2 | ||||
|             e = nil | ||||
|             nForcePrint = 1 | ||||
|         end | ||||
|     else | ||||
|         if func2 then | ||||
|             func = func2 | ||||
|         if expr_func then | ||||
|             func = expr_func | ||||
|             offset = 13 | ||||
|             force_print = 1 | ||||
|         end | ||||
|     elseif expr_func then | ||||
|         func = expr_func | ||||
|         offset = 13 | ||||
|     end | ||||
|  | ||||
|     if func then | ||||
|         local tResults = table.pack(pcall(func)) | ||||
|         if tResults[1] then | ||||
|         chunk_map[name] = { contents = input, offset = offset } | ||||
|         chunk_idx = chunk_idx + 1 | ||||
|  | ||||
|         local results = table.pack(exception.try(func)) | ||||
|         if results[1] then | ||||
|             local n = 1 | ||||
|             while n < tResults.n or n <= nForcePrint do | ||||
|                 local value = tResults[n + 1] | ||||
|             while n < results.n or n <= force_print do | ||||
|                 local value = results[n + 1] | ||||
|                 local ok, serialised = pcall(pretty.pretty, value, { | ||||
|                     function_args = settings.get("lua.function_args"), | ||||
|                     function_source = settings.get("lua.function_source"), | ||||
| @@ -107,10 +114,12 @@ while bRunning do | ||||
|                 n = n + 1 | ||||
|             end | ||||
|         else | ||||
|             printError(tResults[2]) | ||||
|             printError(results[2]) | ||||
|             require "cc.internal.exception".report(results[2], results[3], chunk_map) | ||||
|         end | ||||
|     else | ||||
|         printError(e) | ||||
|         local parser = require "cc.internal.syntax" | ||||
|         if parser.parse_repl(input) then printError(err) end | ||||
|     end | ||||
|  | ||||
| end | ||||
|   | ||||
| @@ -446,7 +446,7 @@ local function playGame() | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|       --now remove the rows and drop everythign else | ||||
|       --now remove the rows and drop everything else | ||||
|       term.setBackgroundColor(colors.black) | ||||
|       for r = 1, #rows do | ||||
|         r = rows[r] | ||||
|   | ||||
| @@ -1,14 +1,32 @@ | ||||
| --- The shell API provides access to CraftOS's command line interface. | ||||
| -- | ||||
| -- It allows you to @{run|start programs}, @{setCompletionFunction|add | ||||
| -- completion for a program}, and much more. | ||||
| -- | ||||
| -- @{shell} is not a "true" API. Instead, it is a standard program, which injects its | ||||
| -- API into the programs that it launches. This allows for multiple shells to | ||||
| -- run at the same time, but means that the API is not available in the global | ||||
| -- environment, and so is unavailable to other @{os.loadAPI|APIs}. | ||||
| -- | ||||
| -- @module[module] shell | ||||
| --[[- The shell API provides access to CraftOS's command line interface. | ||||
|  | ||||
| It allows you to @{run|start programs}, @{setCompletionFunction|add completion | ||||
| for a program}, and much more. | ||||
|  | ||||
| @{shell} is not a "true" API. Instead, it is a standard program, which injects | ||||
| its API into the programs that it launches. This allows for multiple shells to | ||||
| run at the same time, but means that the API is not available in the global | ||||
| environment, and so is unavailable to other @{os.loadAPI|APIs}. | ||||
|  | ||||
| ## Programs and the program path | ||||
| When you run a command with the shell, either from the prompt or | ||||
| @{shell.run|from Lua code}, the shell API performs several steps to work out | ||||
| which program to run: | ||||
|  | ||||
|  1. Firstly, the shell attempts to resolve @{shell.aliases|aliases}. This allows | ||||
|     us to use multiple names for a single command. For example, the `list` | ||||
|     program has two aliases: `ls` and `dir`. When you write `ls /rom`, that's | ||||
|     expanded to `list /rom`. | ||||
|  | ||||
|  2. Next, the shell attempts to find where the program actually is. For this, it | ||||
|     uses the @{shell.path|program path}. This is a colon separated list of | ||||
|     directories, each of which is checked to see if it contains the program. | ||||
|  | ||||
|     `list` or `list.lua` doesn't exist in `.` (the current directory), so she | ||||
|     shell now looks in `/rom/programs`, where `list.lua` can be found! | ||||
|  | ||||
| @module[module] shell | ||||
| ]] | ||||
|  | ||||
| local make_package = dofile("rom/modules/main/cc/require.lua").make | ||||
|  | ||||
| @@ -37,10 +55,11 @@ end | ||||
| -- Set up a dummy require based on the current shell, for loading some of our internal dependencies. | ||||
| local require | ||||
| do | ||||
|     local env = setmetatable(createShellEnv("/rom/modules/internal"), { __index = _ENV }) | ||||
|     local env = setmetatable(createShellEnv("/rom/programs"), { __index = _ENV }) | ||||
|     require = env.require | ||||
| end | ||||
| local expect = require("cc.expect").expect | ||||
| local exception = require "cc.internal.exception" | ||||
|  | ||||
| -- Colours | ||||
| local promptColour, textColour, bgColour | ||||
| @@ -54,6 +73,69 @@ else | ||||
|     bgColour = colours.black | ||||
| end | ||||
|  | ||||
| local function tokenise(...) | ||||
|     local sLine = table.concat({ ... }, " ") | ||||
|     local tWords = {} | ||||
|     local bQuoted = false | ||||
|     for match in string.gmatch(sLine .. "\"", "(.-)\"") do | ||||
|         if bQuoted then | ||||
|             table.insert(tWords, match) | ||||
|         else | ||||
|             for m in string.gmatch(match, "[^ \t]+") do | ||||
|                 table.insert(tWords, m) | ||||
|             end | ||||
|         end | ||||
|         bQuoted = not bQuoted | ||||
|     end | ||||
|     return tWords | ||||
| end | ||||
|  | ||||
| local function executeProgram(path, args) | ||||
|     local file, err = fs.open(path, "r") | ||||
|     if not file then | ||||
|         printError(err) | ||||
|         return false | ||||
|     end | ||||
|  | ||||
|     local contents = file.readAll() or "" | ||||
|     file.close() | ||||
|  | ||||
|     local dir = fs.getDir(path) | ||||
|     local env = setmetatable(createShellEnv(dir), { __index = _G }) | ||||
|     env.arg = args | ||||
|  | ||||
|     local func, err = load(contents, "@/" .. path, nil, env) | ||||
|     if not func then | ||||
|         -- We had a syntax error. Attempt to run it through our own parser if | ||||
|         -- the file is "small enough", otherwise report the original error. | ||||
|         if #contents < 1024 * 128 then | ||||
|             local parser = require "cc.internal.syntax" | ||||
|             if parser.parse_program(contents) then printError(err) end | ||||
|         else | ||||
|             printError(err) | ||||
|         end | ||||
|  | ||||
|         return false | ||||
|     end | ||||
|  | ||||
|     if settings.get("bios.strict_globals", false) then | ||||
|         getmetatable(env).__newindex = function(_, name) | ||||
|             error("Attempt to create global " .. tostring(name), 2) | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     local ok, err, co = exception.try(func, table.unpack(args, 1, args.n)) | ||||
|  | ||||
|     if ok then return true end | ||||
|  | ||||
|     if err and err ~= "" then | ||||
|         printError(err) | ||||
|         exception.report(err, co) | ||||
|     end | ||||
|  | ||||
|     return false | ||||
| end | ||||
|  | ||||
| --- Run a program with the supplied arguments. | ||||
| -- | ||||
| -- Unlike @{shell.run}, each argument is passed to the program verbatim. While | ||||
| @@ -84,10 +166,7 @@ function shell.execute(command, ...) | ||||
|             multishell.setTitle(multishell.getCurrent(), sTitle) | ||||
|         end | ||||
|  | ||||
|         local sDir = fs.getDir(sPath) | ||||
|         local env = createShellEnv(sDir) | ||||
|         env.arg = { [0] = command, ... } | ||||
|         local result = os.run(env, sPath, ...) | ||||
|         local result = executeProgram(sPath, { [0] = command, ... }) | ||||
|  | ||||
|         tProgramStack[#tProgramStack] = nil | ||||
|         if multishell then | ||||
| @@ -108,23 +187,6 @@ function shell.execute(command, ...) | ||||
|     end | ||||
| end | ||||
|  | ||||
| local function tokenise(...) | ||||
|     local sLine = table.concat({ ... }, " ") | ||||
|     local tWords = {} | ||||
|     local bQuoted = false | ||||
|     for match in string.gmatch(sLine .. "\"", "(.-)\"") do | ||||
|         if bQuoted then | ||||
|             table.insert(tWords, match) | ||||
|         else | ||||
|             for m in string.gmatch(match, "[^ \t]+") do | ||||
|                 table.insert(tWords, m) | ||||
|             end | ||||
|         end | ||||
|         bQuoted = not bQuoted | ||||
|     end | ||||
|     return tWords | ||||
| end | ||||
|  | ||||
| -- Install shell API | ||||
|  | ||||
| --- Run a program with the supplied arguments. | ||||
| @@ -247,6 +309,7 @@ end | ||||
| -- @treturn string|nil The absolute path to the program, or @{nil} if it could | ||||
| -- not be found. | ||||
| -- @since 1.2 | ||||
| -- @changed 1.80pr1 Now searches for files with and without the `.lua` extension. | ||||
| -- @usage Locate the `hello` program. | ||||
| -- | ||||
| --      shell.resolveProgram("hello") | ||||
| @@ -541,8 +604,8 @@ end | ||||
| --- Get the current aliases for this shell. | ||||
| -- | ||||
| -- Aliases are used to allow multiple commands to refer to a single program. For | ||||
| -- instance, the `list` program is aliased `dir` or `ls`. Running `ls`, `dir` or | ||||
| -- `list` in the shell will all run the `list` program. | ||||
| -- instance, the `list` program is aliased to `dir` or `ls`. Running `ls`, `dir` | ||||
| -- or `list` in the shell will all run the `list` program. | ||||
| -- | ||||
| -- @treturn { [string] = string } A table, where the keys are the names of | ||||
| -- aliases, and the values are the path to the program. | ||||
| @@ -655,7 +718,7 @@ else | ||||
|                 term.setCursorBlink(false) | ||||
|  | ||||
|                 -- Run the import script with the provided files | ||||
|                 local ok, err = require("cc.import")(event[2].getFiles()) | ||||
|                 local ok, err = require("cc.internal.import")(event[2].getFiles()) | ||||
|                 if not ok and err then printError(err) end | ||||
|  | ||||
|                 -- And attempt to restore the prompt. | ||||
|   | ||||
| @@ -93,13 +93,13 @@ local function collect() | ||||
|     return true | ||||
| end | ||||
|  | ||||
| function refuel(ammount) | ||||
| function refuel(amount) | ||||
|     local fuelLevel = turtle.getFuelLevel() | ||||
|     if fuelLevel == "unlimited" then | ||||
|         return true | ||||
|     end | ||||
|  | ||||
|     local needed = ammount or xPos + zPos + depth + 2 | ||||
|     local needed = amount or xPos + zPos + depth + 2 | ||||
|     if turtle.getFuelLevel() < needed then | ||||
|         for n = 1, 16 do | ||||
|             if turtle.getItemCount(n) > 0 then | ||||
|   | ||||
| @@ -5,7 +5,6 @@ | ||||
|  */ | ||||
| package dan200.computercraft.core; | ||||
| 
 | ||||
| import dan200.computercraft.ComputerCraft; | ||||
| import dan200.computercraft.api.filesystem.IWritableMount; | ||||
| import dan200.computercraft.api.lua.ILuaAPI; | ||||
| import dan200.computercraft.api.lua.LuaException; | ||||
| @@ -15,22 +14,30 @@ import dan200.computercraft.core.computer.Computer; | ||||
| import dan200.computercraft.core.computer.ComputerSide; | ||||
| import dan200.computercraft.core.filesystem.FileMount; | ||||
| import dan200.computercraft.core.filesystem.FileSystemException; | ||||
| import dan200.computercraft.core.lua.CobaltLuaMachine; | ||||
| import dan200.computercraft.core.lua.MachineEnvironment; | ||||
| import dan200.computercraft.core.lua.MachineResult; | ||||
| import dan200.computercraft.core.terminal.Terminal; | ||||
| import dan200.computercraft.support.TestFiles; | ||||
| import dan200.computercraft.test.core.computer.BasicEnvironment; | ||||
| import dan200.computercraft.test.core.computer.FakeMainThreadScheduler; | ||||
| import org.apache.logging.log4j.LogManager; | ||||
| import org.apache.logging.log4j.Logger; | ||||
| import it.unimi.dsi.fastutil.ints.Int2IntArrayMap; | ||||
| import it.unimi.dsi.fastutil.ints.Int2IntMap; | ||||
| import org.junit.jupiter.api.*; | ||||
| import org.junit.jupiter.api.function.Executable; | ||||
| import org.opentest4j.AssertionFailedError; | ||||
| import org.opentest4j.TestAbortedException; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| import org.squiddev.cobalt.*; | ||||
| import org.squiddev.cobalt.debug.DebugFrame; | ||||
| import org.squiddev.cobalt.debug.DebugHook; | ||||
| import org.squiddev.cobalt.debug.DebugState; | ||||
| import org.squiddev.cobalt.function.OneArgFunction; | ||||
| 
 | ||||
| import javax.annotation.Nonnull; | ||||
| import javax.annotation.Nullable; | ||||
| import java.io.BufferedWriter; | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.io.Writer; | ||||
| import java.io.*; | ||||
| import java.lang.reflect.Field; | ||||
| import java.net.URI; | ||||
| import java.nio.channels.Channels; | ||||
| import java.nio.channels.WritableByteChannel; | ||||
| @@ -43,6 +50,7 @@ import java.util.concurrent.locks.Condition; | ||||
| import java.util.concurrent.locks.ReentrantLock; | ||||
| import java.util.regex.Matcher; | ||||
| import java.util.regex.Pattern; | ||||
| import java.util.stream.Collectors; | ||||
| import java.util.stream.Stream; | ||||
| 
 | ||||
| /** | ||||
| @@ -60,7 +68,7 @@ public class ComputerTestDelegate | ||||
| { | ||||
|     private static final Path REPORT_PATH = TestFiles.get( "luacov.report.out" ); | ||||
| 
 | ||||
|     private static final Logger LOG = LogManager.getLogger( ComputerTestDelegate.class ); | ||||
|     private static final Logger LOG = LoggerFactory.getLogger( ComputerTestDelegate.class ); | ||||
| 
 | ||||
|     private static final long TICK_TIME = TimeUnit.MILLISECONDS.toNanos( 50 ); | ||||
| 
 | ||||
| @@ -86,14 +94,12 @@ public class ComputerTestDelegate | ||||
| 
 | ||||
|     private final Condition hasFinished = lock.newCondition(); | ||||
|     private boolean finished = false; | ||||
|     private Map<String, Map<Double, Double>> finishedWith; | ||||
|     private final Map<LuaString, Int2IntArrayMap> coverage = new HashMap<>(); | ||||
| 
 | ||||
|     @BeforeEach | ||||
|     public void before() throws IOException | ||||
|     { | ||||
|         ComputerCraft.logComputerErrors = true; | ||||
| 
 | ||||
|         if( Files.deleteIfExists( REPORT_PATH ) ) ComputerCraft.log.info( "Deleted previous coverage report." ); | ||||
|         if( Files.deleteIfExists( REPORT_PATH ) ) LOG.info( "Deleted previous coverage report." ); | ||||
| 
 | ||||
|         Terminal term = new Terminal( 80, 100, true ); | ||||
|         IWritableMount mount = new FileMount( TestFiles.get( "mount" ).toFile(), 10_000_000 ); | ||||
| @@ -157,12 +163,14 @@ public class ComputerTestDelegate | ||||
|             computer.shutdown(); | ||||
|         } | ||||
| 
 | ||||
|         if( finishedWith != null ) | ||||
|         if( !coverage.isEmpty() ) | ||||
|         { | ||||
|             Files.createDirectories( REPORT_PATH.getParent() ); | ||||
|             try( BufferedWriter writer = Files.newBufferedWriter( REPORT_PATH ) ) | ||||
|             { | ||||
|                 new LuaCoverage( finishedWith ).write( writer ); | ||||
|                 new LuaCoverage( coverage.entrySet().stream().collect( Collectors.toMap( | ||||
|                     x -> x.getKey().substring( 1 ).toString(), Map.Entry::getValue | ||||
|                 ) ) ).write( writer ); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @@ -174,7 +182,7 @@ public class ComputerTestDelegate | ||||
|         try | ||||
|         { | ||||
|             long remaining = TIMEOUT; | ||||
|             while( remaining > 0 & tests == null ) | ||||
|             while( remaining > 0 && tests == null ) | ||||
|             { | ||||
|                 tick(); | ||||
|                 if( hasTests.awaitNanos( TICK_TIME ) > 0 ) break; | ||||
| @@ -231,7 +239,12 @@ public class ComputerTestDelegate | ||||
|         void runs( String name, String uri, Executable executor ) | ||||
|         { | ||||
|             if( this.executor != null ) throw new IllegalStateException( name + " is leaf node" ); | ||||
|             if( children.containsKey( name ) ) throw new IllegalStateException( "Duplicate key for " + name ); | ||||
|             if( children.containsKey( name ) ) | ||||
|             { | ||||
|                 int i = 1; | ||||
|                 while( children.containsKey( name + i ) ) i++; | ||||
|                 name = name + i; | ||||
|             } | ||||
| 
 | ||||
|             children.put( name, new DynamicNodeBuilder( name, uri, executor ) ); | ||||
|         } | ||||
| @@ -290,7 +303,6 @@ public class ComputerTestDelegate | ||||
| 
 | ||||
|     public static class FakeModem implements IPeripheral | ||||
|     { | ||||
|         @Nonnull | ||||
|         @Override | ||||
|         public String getType() | ||||
|         { | ||||
| @@ -312,7 +324,6 @@ public class ComputerTestDelegate | ||||
| 
 | ||||
|     public static class FakePeripheralHub implements IPeripheral | ||||
|     { | ||||
|         @Nonnull | ||||
|         @Override | ||||
|         public String getType() | ||||
|         { | ||||
| @@ -464,10 +475,6 @@ public class ComputerTestDelegate | ||||
|         { | ||||
|             //  Submit the result of a test, allowing the test executor to continue | ||||
|             String name = (String) tbl.get( "name" ); | ||||
|             if( name == null ) | ||||
|             { | ||||
|                 ComputerCraft.log.error( "Oh no: {}", tbl ); | ||||
|             } | ||||
|             String status = (String) tbl.get( "status" ); | ||||
|             String message = (String) tbl.get( "message" ); | ||||
|             String trace = (String) tbl.get( "trace" ); | ||||
| @@ -502,7 +509,9 @@ public class ComputerTestDelegate | ||||
|                 switch( status ) | ||||
|                 { | ||||
|                     case "ok": | ||||
|                         break; | ||||
|                     case "pending": | ||||
|                         runResult = new TestAbortedException( "Test is pending" ); | ||||
|                         break; | ||||
|                     case "fail": | ||||
|                         runResult = new AssertionFailedError( wholeMessage.toString() ); | ||||
| @@ -522,10 +531,8 @@ public class ComputerTestDelegate | ||||
|         } | ||||
| 
 | ||||
|         @LuaFunction | ||||
|         public final void finish( Optional<Map<?, ?>> result ) | ||||
|         public final void finish() | ||||
|         { | ||||
|             @SuppressWarnings( "unchecked" ) | ||||
|             Map<String, Map<Double, Double>> finishedResult = (Map<String, Map<Double, Double>>) result.orElse( null ); | ||||
|             LOG.info( "Finished" ); | ||||
| 
 | ||||
|             // Signal to after that execution has finished | ||||
| @@ -540,7 +547,6 @@ public class ComputerTestDelegate | ||||
|             try | ||||
|             { | ||||
|                 finished = true; | ||||
|                 if( finishedResult != null ) finishedWith = finishedResult; | ||||
| 
 | ||||
|                 hasFinished.signal(); | ||||
|             } | ||||
| @@ -550,4 +556,86 @@ public class ComputerTestDelegate | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * A subclass of {@link CobaltLuaMachine} which tracks coverage for executed files. | ||||
|      * <p> | ||||
|      * This is a super nasty hack, but is also an order of magnitude faster than tracking this in Lua. | ||||
|      */ | ||||
|     private class CoverageLuaMachine extends CobaltLuaMachine | ||||
|     { | ||||
|         CoverageLuaMachine( MachineEnvironment environment ) | ||||
|         { | ||||
|             super( environment ); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public MachineResult loadBios( InputStream bios ) | ||||
|         { | ||||
|             MachineResult result = super.loadBios( bios ); | ||||
|             if( result != MachineResult.OK ) return result; | ||||
| 
 | ||||
|             LuaTable globals; | ||||
|             LuaThread mainRoutine; | ||||
|             try | ||||
|             { | ||||
|                 Field globalField = CobaltLuaMachine.class.getDeclaredField( "globals" ); | ||||
|                 globalField.setAccessible( true ); | ||||
|                 globals = (LuaTable) globalField.get( this ); | ||||
| 
 | ||||
|                 Field threadField = CobaltLuaMachine.class.getDeclaredField( "mainRoutine" ); | ||||
|                 threadField.setAccessible( true ); | ||||
|                 mainRoutine = (LuaThread) threadField.get( this ); | ||||
|             } | ||||
|             catch( ReflectiveOperationException e ) | ||||
|             { | ||||
|                 throw new RuntimeException( "Cannot get internal Cobalt state", e ); | ||||
|             } | ||||
| 
 | ||||
|             Map<LuaString, Int2IntArrayMap> coverage = ComputerTestDelegate.this.coverage; | ||||
|             DebugHook hook = new DebugHook() | ||||
|             { | ||||
|                 @Override | ||||
|                 public void onCall( LuaState state, DebugState ds, DebugFrame frame ) | ||||
|                 { | ||||
|                 } | ||||
| 
 | ||||
|                 @Override | ||||
|                 public void onReturn( LuaState state, DebugState ds, DebugFrame frame ) | ||||
|                 { | ||||
|                 } | ||||
| 
 | ||||
|                 @Override | ||||
|                 public void onCount( LuaState state, DebugState ds, DebugFrame frame ) | ||||
|                 { | ||||
|                 } | ||||
| 
 | ||||
|                 @Override | ||||
|                 public void onLine( LuaState state, DebugState ds, DebugFrame frame, int newLine ) | ||||
|                 { | ||||
|                     if( frame.closure == null ) return; | ||||
| 
 | ||||
|                     Prototype proto = frame.closure.getPrototype(); | ||||
|                     if( !proto.source.startsWith( '@' ) ) return; | ||||
| 
 | ||||
|                     Int2IntMap map = coverage.computeIfAbsent( proto.source, x -> new Int2IntArrayMap() ); | ||||
|                     map.put( newLine, map.get( newLine ) + 1 ); | ||||
|                 } | ||||
|             }; | ||||
| 
 | ||||
|             ((LuaTable) globals.rawget( "coroutine" )).rawset( "create", new OneArgFunction() | ||||
|             { | ||||
|                 @Override | ||||
|                 public LuaValue call( LuaState state, LuaValue arg ) throws LuaError | ||||
|                 { | ||||
|                     LuaThread thread = new LuaThread( state, arg.checkFunction(), state.getCurrentThread().getfenv() ); | ||||
|                     thread.getDebugState().setHook( hook, false, true, false, 0 ); | ||||
|                     return thread; | ||||
|                 } | ||||
|             } ); | ||||
|             mainRoutine.getDebugState().setHook( hook, false, true, false, 0 ); | ||||
| 
 | ||||
|             return MachineResult.OK; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -6,9 +6,12 @@ | ||||
| package dan200.computercraft.core; | ||||
| 
 | ||||
| import com.google.common.base.Strings; | ||||
| import dan200.computercraft.ComputerCraft; | ||||
| import it.unimi.dsi.fastutil.ints.Int2IntMap; | ||||
| import it.unimi.dsi.fastutil.ints.Int2IntMaps; | ||||
| import it.unimi.dsi.fastutil.ints.IntOpenHashSet; | ||||
| import it.unimi.dsi.fastutil.ints.IntSet; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| import org.squiddev.cobalt.Prototype; | ||||
| import org.squiddev.cobalt.compiler.CompileException; | ||||
| import org.squiddev.cobalt.compiler.LuaC; | ||||
| @@ -16,30 +19,27 @@ import org.squiddev.cobalt.compiler.LuaC; | ||||
| import java.io.*; | ||||
| import java.nio.file.Files; | ||||
| import java.nio.file.Path; | ||||
| import java.util.*; | ||||
| import java.util.stream.Collectors; | ||||
| import java.util.stream.Stream; | ||||
| import java.util.ArrayDeque; | ||||
| import java.util.Collections; | ||||
| import java.util.Map; | ||||
| import java.util.Queue; | ||||
| 
 | ||||
| class LuaCoverage | ||||
| { | ||||
|     private static final Logger LOG = LoggerFactory.getLogger( LuaCoverage.class ); | ||||
|     private static final Path ROOT = new File( "src/main/resources/data/computercraft/lua" ).toPath(); | ||||
|     private static final Path BIOS = ROOT.resolve( "bios.lua" ); | ||||
|     private static final Path APIS = ROOT.resolve( "rom/apis" ); | ||||
|     private static final Path SHELL = ROOT.resolve( "rom/programs/shell.lua" ); | ||||
|     private static final Path MULTISHELL = ROOT.resolve( "rom/programs/advanced/multishell.lua" ); | ||||
|     private static final Path TREASURE = ROOT.resolve( "treasure" ); | ||||
| 
 | ||||
|     private final Map<String, Map<Double, Double>> coverage; | ||||
|     private final Map<String, Int2IntMap> coverage; | ||||
|     private final String blank; | ||||
|     private final String zero; | ||||
|     private final String countFormat; | ||||
| 
 | ||||
|     LuaCoverage( Map<String, Map<Double, Double>> coverage ) | ||||
|     LuaCoverage( Map<String, Int2IntMap> coverage ) | ||||
|     { | ||||
|         this.coverage = coverage; | ||||
| 
 | ||||
|         int max = (int) coverage.values().stream() | ||||
|             .flatMapToDouble( x -> x.values().stream().mapToDouble( y -> y ) ) | ||||
|         int max = coverage.values().stream() | ||||
|             .flatMapToInt( x -> x.values().stream().mapToInt( y -> y ) ) | ||||
|             .max().orElse( 0 ); | ||||
|         int maxLen = Math.max( 1, (int) Math.ceil( Math.log10( max ) ) ); | ||||
|         blank = Strings.repeat( " ", maxLen + 1 ); | ||||
| @@ -49,25 +49,22 @@ class LuaCoverage | ||||
| 
 | ||||
|     void write( Writer out ) throws IOException | ||||
|     { | ||||
|         Files.find( ROOT, Integer.MAX_VALUE, ( path, attr ) -> attr.isRegularFile() && !path.startsWith( TREASURE ) ).forEach( path -> { | ||||
|         Files.find( ROOT, Integer.MAX_VALUE, ( path, attr ) -> attr.isRegularFile() ).forEach( path -> { | ||||
|             Path relative = ROOT.relativize( path ); | ||||
|             String full = relative.toString().replace( '\\', '/' ); | ||||
|             if( !full.endsWith( ".lua" ) ) return; | ||||
| 
 | ||||
|             Map<Double, Double> files = Stream.of( | ||||
|                 coverage.remove( "/" + full ), | ||||
|                 path.equals( BIOS ) ? coverage.remove( "bios.lua" ) : null, | ||||
|                 path.equals( SHELL ) ? coverage.remove( "shell.lua" ) : null, | ||||
|                 path.equals( MULTISHELL ) ? coverage.remove( "multishell.lua" ) : null, | ||||
|                 path.startsWith( APIS ) ? coverage.remove( path.getFileName().toString() ) : null | ||||
|             ) | ||||
|                 .filter( Objects::nonNull ) | ||||
|                 .flatMap( x -> x.entrySet().stream() ) | ||||
|                 .collect( Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue, Double::sum ) ); | ||||
|             Int2IntMap possiblePaths = coverage.remove( "/" + full ); | ||||
|             if( possiblePaths == null ) possiblePaths = coverage.remove( full ); | ||||
|             if( possiblePaths == null ) | ||||
|             { | ||||
|                 possiblePaths = Int2IntMaps.EMPTY_MAP; | ||||
|                 LOG.warn( "{} has no coverage data", full ); | ||||
|             } | ||||
| 
 | ||||
|             try | ||||
|             { | ||||
|                 writeCoverageFor( out, path, files ); | ||||
|                 writeCoverageFor( out, path, possiblePaths ); | ||||
|             } | ||||
|             catch( IOException e ) | ||||
|             { | ||||
| @@ -78,15 +75,15 @@ class LuaCoverage | ||||
|         for( String filename : coverage.keySet() ) | ||||
|         { | ||||
|             if( filename.startsWith( "/test-rom/" ) ) continue; | ||||
|             ComputerCraft.log.warn( "Unknown file {}", filename ); | ||||
|             LOG.warn( "Unknown file {}", filename ); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void writeCoverageFor( Writer out, Path fullName, Map<Double, Double> visitedLines ) throws IOException | ||||
|     private void writeCoverageFor( Writer out, Path fullName, Int2IntMap visitedLines ) throws IOException | ||||
|     { | ||||
|         if( !Files.exists( fullName ) ) | ||||
|         { | ||||
|             ComputerCraft.log.error( "Cannot locate file {}", fullName ); | ||||
|             LOG.error( "Cannot locate file {}", fullName ); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
| @@ -104,10 +101,10 @@ class LuaCoverage | ||||
|             while( (line = reader.readLine()) != null ) | ||||
|             { | ||||
|                 lineNo++; | ||||
|                 Double count = visitedLines.get( (double) lineNo ); | ||||
|                 if( count != null ) | ||||
|                 int count = visitedLines.getOrDefault( lineNo, -1 ); | ||||
|                 if( count >= 0 ) | ||||
|                 { | ||||
|                     out.write( String.format( countFormat, count.intValue() ) ); | ||||
|                     out.write( String.format( countFormat, count ) ); | ||||
|                 } | ||||
|                 else if( activeLines.contains( lineNo ) ) | ||||
|                 { | ||||
| @@ -128,15 +125,22 @@ class LuaCoverage | ||||
|     private static IntSet getActiveLines( File file ) throws IOException | ||||
|     { | ||||
|         IntSet activeLines = new IntOpenHashSet(); | ||||
|         try( InputStream stream = new FileInputStream( file ) ) | ||||
|         Queue<Prototype> queue = new ArrayDeque<>(); | ||||
| 
 | ||||
|         try( InputStream stream = Files.newInputStream( file.toPath() ) ) | ||||
|         { | ||||
|             Prototype proto = LuaC.compile( stream, "@" + file.getPath() ); | ||||
|             Queue<Prototype> queue = new ArrayDeque<>(); | ||||
|             queue.add( proto ); | ||||
|         } | ||||
|         catch( CompileException e ) | ||||
|         { | ||||
|             throw new IllegalStateException( "Cannot compile", e ); | ||||
|         } | ||||
| 
 | ||||
|         Prototype proto; | ||||
|         while( (proto = queue.poll()) != null ) | ||||
|         { | ||||
|                 int[] lines = proto.lineinfo; | ||||
|             int[] lines = proto.lineInfo; | ||||
|             if( lines != null ) | ||||
|             { | ||||
|                 for( int line : lines ) | ||||
| @@ -144,12 +148,7 @@ class LuaCoverage | ||||
|                     activeLines.add( line ); | ||||
|                 } | ||||
|             } | ||||
|                 if( proto.p != null ) Collections.addAll( queue, proto.p ); | ||||
|             } | ||||
|         } | ||||
|         catch( CompileException e ) | ||||
|         { | ||||
|             throw new IllegalStateException( "Cannot compile", e ); | ||||
|             if( proto.children != null ) Collections.addAll( queue, proto.children ); | ||||
|         } | ||||
| 
 | ||||
|         return activeLines; | ||||
|   | ||||
| @@ -29,7 +29,7 @@ public class ComputerTest | ||||
|             } | ||||
|             catch( AssertionError e ) | ||||
|             { | ||||
|                 if( e.getMessage().equals( "test.lua:1: Too long without yielding" ) ) return; | ||||
|                 if( e.getMessage().equals( "/test.lua:1: Too long without yielding" ) ) return; | ||||
|                 throw e; | ||||
|             } | ||||
| 
 | ||||
|   | ||||
| @@ -12,6 +12,8 @@ import dan200.computercraft.core.apis.ObjectWrapper; | ||||
| import dan200.computercraft.core.apis.handles.EncodedWritableHandle; | ||||
| import dan200.computercraft.support.TestFiles; | ||||
| import org.junit.jupiter.api.Test; | ||||
| import org.junit.jupiter.params.ParameterizedTest; | ||||
| import org.junit.jupiter.params.provider.MethodSource; | ||||
| 
 | ||||
| import java.io.BufferedWriter; | ||||
| import java.io.File; | ||||
| @@ -79,4 +81,23 @@ public class FileSystemTest | ||||
|         LuaException err = assertThrows( LuaException.class, () -> wrapper.call( "write", "Tiny line" ) ); | ||||
|         assertEquals( "attempt to use a closed file", err.getMessage() ); | ||||
|     } | ||||
| 
 | ||||
|     @ParameterizedTest( name = "{0}" ) | ||||
|     @MethodSource( "sanitiseCases" ) | ||||
|     public void testSanitize( String input, String output ) | ||||
|     { | ||||
|         assertEquals( output, FileSystem.sanitizePath( input, false ) ); | ||||
|     } | ||||
| 
 | ||||
|     public static String[][] sanitiseCases() | ||||
|     { | ||||
|         return new String[][] { | ||||
|             new String[] { "a//b", "a/b" }, | ||||
|             new String[] { "a/./b", "a/b" }, | ||||
|             new String[] { "a/../b", "b" }, | ||||
|             new String[] { "a/.../b", "a/b" }, | ||||
|             new String[] { " a ", "a" }, | ||||
|             new String[] { "a b c", "a b c" }, | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| # JSON Parsing Test Suite | ||||
| 
 | ||||
| This is a collection of JSON test cases from [nst/JSONTestSuite][gh]. We simply | ||||
| determine whether an object is succesfully parsed or not, and do not check the | ||||
| determine whether an object is successfully parsed or not, and do not check the | ||||
| contents. | ||||
| 
 | ||||
| See `LICENSE` for copyright information. | ||||
|   | ||||
| @@ -182,9 +182,13 @@ end | ||||
| -- @treturn string The formatted value | ||||
| local function format(value) | ||||
|     -- TODO: Look into something like mbs's pretty printer. | ||||
|     if type(value) == "string" and value:find("\n") then | ||||
|         return "<<<\n" .. value .. "\n>>>" | ||||
|     else | ||||
|         local ok, res = pcall(textutils.serialise, value) | ||||
|         if ok then return res else return tostring(value) end | ||||
|     end | ||||
| end | ||||
|  | ||||
| local expect_mt = {} | ||||
| expect_mt.__index = expect_mt | ||||
| @@ -417,6 +421,9 @@ end | ||||
| --- The stack of "describe"s. | ||||
| local test_stack = { n = 0 } | ||||
|  | ||||
| --- The stack of setup functions. | ||||
| local before_each_fns = { n = 0 } | ||||
|  | ||||
| --- Whether we're now running tests, and so cannot run any more. | ||||
| local tests_locked = false | ||||
|  | ||||
| @@ -455,8 +462,14 @@ local function describe(name, body) | ||||
|     local n = test_stack.n + 1 | ||||
|     test_stack[n], test_stack.n = name, n | ||||
|  | ||||
|     local old_before, new_before = before_each_fns, { n = before_each_fns.n } | ||||
|     for i = 1, old_before.n do new_before[i] = old_before[i] end | ||||
|     before_each_fns = new_before | ||||
|  | ||||
|     local ok, err = try(body) | ||||
|  | ||||
|     before_each_fns = old_before | ||||
|  | ||||
|     -- We count errors as a (failing) test. | ||||
|     if not ok then do_test { error = err, definition = format_loc(debug.getinfo(2, "Sl")) } end | ||||
|  | ||||
| @@ -477,7 +490,11 @@ local function it(name, body) | ||||
|     local n = test_stack.n + 1 | ||||
|     test_stack[n], test_stack.n, tests_locked = name, n, true | ||||
|  | ||||
|     do_test { action = body, definition = format_loc(debug.getinfo(2, "Sl")) } | ||||
|     do_test { | ||||
|         action = body, | ||||
|         before = before_each_fns, | ||||
|         definition = format_loc(debug.getinfo(2, "Sl")), | ||||
|     } | ||||
|  | ||||
|     -- Pop the test from the stack | ||||
|     test_stack.n, tests_locked = n - 1, false | ||||
| @@ -498,26 +515,17 @@ local function pending(name) | ||||
|     test_stack.n = n - 1 | ||||
| end | ||||
|  | ||||
| local native_co_create, native_loadfile = coroutine.create, loadfile | ||||
| local function before_each(body) | ||||
|     check('it', 1, 'function', body) | ||||
|     if tests_locked then error("Cannot define before_each while running tests", 2) end | ||||
|  | ||||
|     local n = before_each_fns.n + 1 | ||||
|     before_each_fns[n], before_each_fns.n = body, n | ||||
| end | ||||
|  | ||||
| local native_loadfile = loadfile | ||||
| local line_counts = {} | ||||
| if cct_test then | ||||
|     local string_sub, debug_getinfo = string.sub, debug.getinfo | ||||
|     local function debug_hook(_, line_nr) | ||||
|         local name = debug_getinfo(2, "S").source | ||||
|         if string_sub(name, 1, 1) ~= "@" then return end | ||||
|         name = string_sub(name, 2) | ||||
|  | ||||
|         local file = line_counts[name] | ||||
|         if not file then file = {} line_counts[name] = file end | ||||
|         file[line_nr] = (file[line_nr] or 0) + 1 | ||||
|     end | ||||
|  | ||||
|     coroutine.create = function(...) | ||||
|         local co = native_co_create(...) | ||||
|         debug.sethook(co, debug_hook, "l") | ||||
|         return co | ||||
|     end | ||||
|  | ||||
|     local expect = require "cc.expect".expect | ||||
|     _G.native_loadfile = native_loadfile | ||||
|     _G.loadfile = function(filename, mode, env) | ||||
| @@ -537,8 +545,6 @@ if cct_test then | ||||
|         file.close() | ||||
|         return func, err | ||||
|     end | ||||
|  | ||||
|     debug.sethook(debug_hook, "l") | ||||
| end | ||||
|  | ||||
| local arg = ... | ||||
| @@ -559,16 +565,11 @@ end | ||||
| package.path = ("/%s/?.lua;/%s/?/init.lua;%s"):format(root_dir, root_dir, package.path) | ||||
|  | ||||
| do | ||||
|     -- Load in the tests from all our files | ||||
|     local env = setmetatable({}, { __index = _ENV }) | ||||
|  | ||||
|     local function set_env(tbl) | ||||
|         for k in pairs(env) do env[k] = nil end | ||||
|         for k, v in pairs(tbl) do env[k] = v end | ||||
|     end | ||||
|  | ||||
|     -- When declaring tests, you shouldn't be able to use test methods | ||||
|     set_env { describe = describe, it = it, pending = pending } | ||||
|     -- Add our new functions to the current environment. | ||||
|     for k, v in pairs { | ||||
|         describe = describe, it = it, pending = pending, before_each = before_each, | ||||
|         expect = expect, fail = fail, | ||||
|     } do _ENV[k] = v end | ||||
|  | ||||
|     local suffix = "_spec.lua" | ||||
|     local function run_in(sub_dir) | ||||
| @@ -577,7 +578,7 @@ do | ||||
|             if fs.isDir(file) then | ||||
|                 run_in(file) | ||||
|             elseif file:sub(-#suffix) == suffix then | ||||
|                 local fun, err = loadfile(file, nil, env) | ||||
|                 local fun, err = loadfile(file, nil, _ENV) | ||||
|                 if not fun then | ||||
|                     do_test { name = file:sub(#root_dir + 2), error = { message = err } } | ||||
|                 else | ||||
| @@ -590,8 +591,8 @@ do | ||||
|  | ||||
|     run_in(root_dir) | ||||
|  | ||||
|     -- When running tests, you shouldn't be able to declare new ones. | ||||
|     set_env { expect = expect, fail = fail, stub = stub } | ||||
|     -- Add stub later on, so its not available when running tests | ||||
|     _ENV.stub = stub | ||||
| end | ||||
|  | ||||
| -- Error if we've found no tests | ||||
| @@ -630,8 +631,13 @@ local function do_run(test) | ||||
|         -- Flush the event queue and ensure we're running with 0 timeout. | ||||
|         os.queueEvent("start_test") os.pullEvent("start_test") | ||||
|  | ||||
|         local ok | ||||
|         ok, err = try(test.action) | ||||
|         local ok = true | ||||
|         for i = 1, test.before.n do | ||||
|             if not ok then break end | ||||
|             ok, err = try(test.before[i]) | ||||
|         end | ||||
|         if ok then ok, err = try(test.action) end | ||||
|  | ||||
|         status = ok and "pass" or (err.fail and "fail" or "error") | ||||
|  | ||||
|         pop_state(state) | ||||
| @@ -711,8 +717,6 @@ end | ||||
| term.setTextColour(colours.white) io.write(info .. "\n") | ||||
|  | ||||
| -- Restore hook stubs | ||||
| debug.sethook(nil, "l") | ||||
| coroutine.create = native_co_create | ||||
| _G.loadfile = native_loadfile | ||||
|  | ||||
| if cct_test then cct_test.finish(line_counts) end | ||||
|   | ||||
| @@ -1,4 +1,8 @@ | ||||
| describe("The fs library", function() | ||||
|     local test_root = "/test-files/fs" | ||||
|     local function test_file(path) return fs.combine(test_root, path) end | ||||
|     before_each(function() fs.delete(test_root) end) | ||||
|  | ||||
|     describe("fs.complete", function() | ||||
|         it("validates arguments", function() | ||||
|             fs.complete("", "") | ||||
| @@ -139,7 +143,7 @@ describe("The fs library", function() | ||||
|             end) | ||||
|  | ||||
|             it("errors when closing twice", function() | ||||
|                 local handle = fs.open("test-files/out.txt", "w") | ||||
|                 local handle = fs.open(test_file "out.txt", "w") | ||||
|                 handle.close() | ||||
|                 expect.error(handle.close):eq("attempt to use a closed file") | ||||
|             end) | ||||
| @@ -216,6 +220,48 @@ describe("The fs library", function() | ||||
|             expect.error(fs.move, "test-files", "rom/move"):eq("Access denied") | ||||
|             expect.error(fs.move, "rom", "test-files"):eq("Access denied") | ||||
|         end) | ||||
|  | ||||
|         it("fails if source does not exist", function() | ||||
|             expect.error(fs.move, test_file "src", test_file "dest"):eq("No such file") | ||||
|         end) | ||||
|  | ||||
|         it("fails if destination exists", function() | ||||
|             fs.open(test_file "src", "w").close() | ||||
|             fs.open(test_file "dest", "w").close() | ||||
|  | ||||
|             expect.error(fs.move, test_file "src", test_file "dest"):eq("File exists") | ||||
|         end) | ||||
|  | ||||
|         it("fails to move a directory inside itself", function() | ||||
|             fs.open(test_file "file", "w").close() | ||||
|             expect.error(fs.move, test_root, test_file "child"):eq("Can't move a directory inside itself") | ||||
|             expect.error(fs.move, "", "child"):eq("Can't move a directory inside itself") | ||||
|         end) | ||||
|  | ||||
|         it("files can be renamed", function() | ||||
|             fs.open(test_file "src", "w").close() | ||||
|             fs.move(test_file "src",  test_file "dest") | ||||
|  | ||||
|             expect(fs.exists(test_file "src")):eq(false) | ||||
|             expect(fs.exists(test_file "dest")):eq(true) | ||||
|         end) | ||||
|  | ||||
|         it("directories can be renamed", function() | ||||
|             fs.open(test_file "src/some/file", "w").close() | ||||
|             fs.move(test_file "src",  test_file "dest") | ||||
|  | ||||
|             expect(fs.exists(test_file "src")):eq(false) | ||||
|             expect(fs.exists(test_file "dest")):eq(true) | ||||
|             expect(fs.exists(test_file "dest/some/file")):eq(true) | ||||
|         end) | ||||
|  | ||||
|         it("creates directories before renaming", function() | ||||
|             fs.open(test_file "src", "w").close() | ||||
|             fs.move(test_file "src", test_file "dest/file") | ||||
|  | ||||
|             expect(fs.exists(test_file "src")):eq(false) | ||||
|             expect(fs.exists(test_file "dest/file")):eq(true) | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("fs.getCapacity", function() | ||||
| @@ -240,12 +286,11 @@ describe("The fs library", function() | ||||
|         it("returns information about files", function() | ||||
|             local now = os.epoch("utc") | ||||
|  | ||||
|             fs.delete("/tmp/basic-file") | ||||
|             local h = fs.open("/tmp/basic-file", "w") | ||||
|             local h = fs.open(test_file "basic-file", "w") | ||||
|             h.write("A reasonably sized string") | ||||
|             h.close() | ||||
|  | ||||
|             local attributes = fs.attributes("tmp/basic-file") | ||||
|             local attributes = fs.attributes(test_file "basic-file") | ||||
|             expect(attributes):matches { isDir = false, size = 25, isReadOnly = false } | ||||
|  | ||||
|             if attributes.created - now >= 1000 then | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| local timeout = require "test_helpers".timeout | ||||
|  | ||||
| describe("The http library", function() | ||||
|     describe("http.checkURL", function() | ||||
|         it("accepts well formed domains", function() | ||||
| @@ -18,4 +20,28 @@ describe("The http library", function() | ||||
|             expect({ http.checkURL("http://127.0.0.1") }):same({ false, "Domain not permitted" }) | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("http.websocketAsync", function() | ||||
|         it("queues an event for immediate failures", function() | ||||
|             timeout(1, function() | ||||
|                 local url = "http://not.a.websocket" | ||||
|                 http.websocketAsync(url) | ||||
|                 local _, url2, message = os.pullEvent("websocket_failure") | ||||
|                 expect(url2):eq(url) | ||||
|                 expect(message):eq("Invalid scheme 'http'") | ||||
|             end) | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     describe("http.requestAsync", function() | ||||
|         it("queues an event for immediate failures", function() | ||||
|             timeout(1, function() | ||||
|                 local url = "ws://not.a.request" | ||||
|                 http.request(url) | ||||
|                 local _, url2, message = os.pullEvent("http_failure") | ||||
|                 expect(url2):eq(url) | ||||
|                 expect(message):eq("Invalid protocol 'ws'") | ||||
|             end) | ||||
|         end) | ||||
|     end) | ||||
| end) | ||||
|   | ||||
| @@ -95,6 +95,9 @@ describe("The os library", function() | ||||
|             exp_code("%Y", "2000") | ||||
|             exp_code("%%", "%") | ||||
|  | ||||
|             it("%r at 12 AM", function() expect(os.date("%r", 1670373922)):eq("12:45:22 AM") end) | ||||
|             it("%I at 12 AM", function() expect(os.date("%I", 1670373922)):eq("12") end) | ||||
|  | ||||
|             it("zones are numbers", function() | ||||
|                 local zone = os.date("%z", t1) | ||||
|                 if not zone:match("^[+-]%d%d%d%d$") then | ||||
|   | ||||
| @@ -28,8 +28,6 @@ describe("The Lua base library", function() | ||||
|     end) | ||||
|  | ||||
|     describe("loadfile", function() | ||||
|         local loadfile = _G.native_loadfile or loadfile | ||||
|  | ||||
|         local function make_file() | ||||
|             local tmp = fs.open("test-files/out.lua", "w") | ||||
|             tmp.write("return _ENV") | ||||
| @@ -48,7 +46,7 @@ describe("The Lua base library", function() | ||||
|  | ||||
|         it("prefixes the filename with @", function() | ||||
|             local info = debug.getinfo(loadfile("/rom/startup.lua"), "S") | ||||
|             expect(info):matches { short_src = "startup.lua", source = "@startup.lua" } | ||||
|             expect(info):matches { short_src = "/rom/startup.lua", source = "@/rom/startup.lua" } | ||||
|         end) | ||||
|  | ||||
|         it("loads a file with the global environment", function() | ||||
|   | ||||
| @@ -0,0 +1,52 @@ | ||||
| local helpers = require "test_helpers" | ||||
|  | ||||
| describe("cc.internal.syntax", function() | ||||
|     local syntax = require "cc.internal.syntax" | ||||
|     local parser = require "cc.internal.syntax.parser" | ||||
|     local syntax_helpers = require "modules.cc.internal.syntax.syntax_helpers" | ||||
|  | ||||
|     describe("can parse all of CC's Lua files", function() | ||||
|         local function list_dir(path) | ||||
|             if not path then path = "/" end | ||||
|             for _, child in pairs(fs.list(path)) do | ||||
|                 child = fs.combine(path, child) | ||||
|  | ||||
|                 if fs.isDir(child) then list_dir(child) | ||||
|                 elseif child:sub(-4) == ".lua" then coroutine.yield(child) | ||||
|                 end | ||||
|             end | ||||
|         end | ||||
|  | ||||
|         for file in coroutine.wrap(list_dir) do | ||||
|             it(file, function() | ||||
|                 helpers.with_window(50, 10, function() | ||||
|                     local h = fs.open(file, "r") | ||||
|                     local contents = h.readAll() | ||||
|                     h.close() | ||||
|  | ||||
|                     expect(syntax.parse_program(contents)):describe(file):eq(true) | ||||
|                 end) | ||||
|             end) | ||||
|         end | ||||
|     end) | ||||
|  | ||||
|     -- We specify most of the parser's behaviour as golden tests. A little nasty | ||||
|     -- (it's more of an end-to-end test), but much easier to write! | ||||
|     local function describe_golden(name, path, print_tokens) | ||||
|         helpers.describe_golden(name, "test-rom/spec/modules/cc/internal/syntax/" .. path, function(lua, extra) | ||||
|             local start = nil | ||||
|             if #extra > 0 then | ||||
|                 start = parser[extra:match("^{([a-z_]+)}$")] | ||||
|                 if not start then | ||||
|                     fail("Cannot extract start symbol " .. extra) | ||||
|                 end | ||||
|             end | ||||
|  | ||||
|             return syntax_helpers.capture_parser(lua, print_tokens, start) | ||||
|         end) | ||||
|     end | ||||
|  | ||||
|     describe_golden("the lexer", "lexer_spec.md", true) | ||||
|     describe_golden("the parser", "parser_spec.md", false) | ||||
|     describe_golden("the parser (all states)", "parser_exhaustive_spec.md", false) | ||||
| end) | ||||
| @@ -0,0 +1,319 @@ | ||||
| We provide a lexer for Lua source code. Here we test that the lexer returns the | ||||
| correct tokens and positions, and that it can report sensible error messages. | ||||
| 
 | ||||
| # Comments | ||||
| 
 | ||||
| ## Single-line comments | ||||
| We can lex some basic comments: | ||||
| 
 | ||||
| ```lua | ||||
| -- A basic singleline comment comment | ||||
| --[ Not a multiline comment | ||||
| --[= Also not a multiline comment! | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| 1:1-1:37 COMMENT -- A basic singleline comment comment | ||||
| 2:1-2:27 COMMENT --[ Not a multiline comment | ||||
| 3:1-3:34 COMMENT --[= Also not a multiline comment! | ||||
| ``` | ||||
| 
 | ||||
| It's also useful to test empty comments (including no trailing newline) separately: | ||||
| 
 | ||||
| ```lua | ||||
| -- | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| 1:1-1:2 COMMENT -- | ||||
| ``` | ||||
| 
 | ||||
| ## Multi-line comments | ||||
| Multiline/long-string-style comments are also supported: | ||||
| 
 | ||||
| ```lua | ||||
| --[[ | ||||
|   A | ||||
|   multiline | ||||
|   comment | ||||
| ]] | ||||
| 
 | ||||
| --[=[  ]==] ]] ]=] | ||||
| 
 | ||||
| --[[ ]=]] | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| 1:1-5:2 COMMENT --[[<NL>  A<NL>  multiline<NL>  comment<NL>]] | ||||
| 7:1-7:18 COMMENT --[=[  ]==] ]] ]=] | ||||
| 9:1-9:9 COMMENT --[[ ]=]] | ||||
| ``` | ||||
| 
 | ||||
| We also fail on unfinished comments: | ||||
| 
 | ||||
| ```lua | ||||
| --[=[ | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| This comment was never finished. | ||||
|    | | ||||
|  1 | --[=[ | ||||
|    | ^^^^^ Comment was started here. | ||||
| We expected a closing delimiter (]=]) somewhere after this comment was started. | ||||
| 1:1-1:5 ERROR --[=[ | ||||
| ``` | ||||
| 
 | ||||
| Nested comments are rejected, just as Lua 5.1 does: | ||||
| 
 | ||||
| ```lua | ||||
| --[[ [[ ]] | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| [[ cannot be nested inside another [[ ... ]] | ||||
|    | | ||||
|  1 | --[[ [[ ]] | ||||
|    |      ^^ | ||||
| 1:1-1:10 COMMENT --[[ [[ ]] | ||||
| ``` | ||||
| 
 | ||||
| # Strings | ||||
| 
 | ||||
| We can lex basic strings: | ||||
| 
 | ||||
| ```lua | ||||
| return "abc", "abc\"", 'abc', 'abc\z | ||||
| 
 | ||||
| ', "abc\ | ||||
| continued" | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| 1:1-1:6 RETURN return | ||||
| 1:8-1:12 STRING "abc" | ||||
| 1:13-1:13 COMMA , | ||||
| 1:15-1:21 STRING "abc\"" | ||||
| 1:22-1:22 COMMA , | ||||
| 1:24-1:28 STRING 'abc' | ||||
| 1:29-1:29 COMMA , | ||||
| 1:31-3:1 STRING 'abc\z<NL><NL>' | ||||
| 3:2-3:2 COMMA , | ||||
| 3:4-4:10 STRING "abc\<NL>continued" | ||||
| ``` | ||||
| 
 | ||||
| We also can lex unterminated strings, including those where there's no closing | ||||
| quote: | ||||
| 
 | ||||
| ```lua | ||||
| return "abc | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| 1:1-1:6 RETURN return | ||||
| This string is not finished. Are you missing a closing quote (")? | ||||
|    | | ||||
|  1 | return "abc | ||||
|    |        ^ String started here. | ||||
|    | | ||||
|  1 | return "abc | ||||
|    |            ^ Expected a closing quote here. | ||||
| 1:8-1:11 STRING "abc | ||||
| ``` | ||||
| 
 | ||||
| And those where the zap is malformed: | ||||
| 
 | ||||
| ```lua | ||||
| return "abc\z | ||||
| 
 | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| 1:1-1:6 RETURN return | ||||
| This string is not finished. Are you missing a closing quote (")? | ||||
|    | | ||||
|  1 | return "abc\z | ||||
|    |        ^ String started here. | ||||
|    | | ||||
|  1 | return "abc\z | ||||
|    |              ^ Expected a closing quote here. | ||||
| 1:8-1:14 STRING "abc\z<NL> | ||||
| ``` | ||||
| 
 | ||||
| Finally, strings where the escape is entirely missing: | ||||
| 
 | ||||
| ```lua | ||||
| return "abc\ | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| 1:1-1:6 RETURN return | ||||
| This string is not finished. | ||||
|    | | ||||
|  1 | return "abc\ | ||||
|    |        ^ String started here. | ||||
|    | | ||||
|  1 | return "abc\ | ||||
|    |            ^ An escape sequence was started here, but with nothing following it. | ||||
| 1:8-1:12 STRING "abc\ | ||||
| ``` | ||||
| 
 | ||||
| ## Multi-line/long strings | ||||
| We can also handle long strings fine | ||||
| 
 | ||||
| ```lua | ||||
| return [[a b c]], [=[a b c ]=] | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| 1:1-1:6 RETURN return | ||||
| 1:8-1:16 STRING [[a b c]] | ||||
| 1:17-1:17 COMMA , | ||||
| 1:19-1:30 STRING [=[a b c ]=] | ||||
| ``` | ||||
| 
 | ||||
| Unfinished long strings are correctly reported: | ||||
| 
 | ||||
| ```lua | ||||
| return [[ | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| 1:1-1:6 RETURN return | ||||
| This string was never finished. | ||||
|    | | ||||
|  1 | return [[ | ||||
|    |        ^^ String was started here. | ||||
| We expected a closing delimiter (]]) somewhere after this string was started. | ||||
| 1:8-1:9 ERROR [[ | ||||
| ``` | ||||
| 
 | ||||
| We also handle malformed opening strings: | ||||
| 
 | ||||
| ```lua | ||||
| return [= | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| 1:1-1:6 RETURN return | ||||
| Incorrect start of a long string. | ||||
|    | | ||||
|  1 | return [= | ||||
|    |        ^^^ | ||||
| Tip: If you wanted to start a long string here, add an extra [ here. | ||||
| 1:8-1:10 ERROR [= | ||||
| ``` | ||||
| 
 | ||||
| # Numbers | ||||
| 
 | ||||
| ```lua | ||||
| return 0, 0.0, 0e1, .23, 0x23, 23e-2, 23e+2 | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| 1:1-1:6 RETURN return | ||||
| 1:8-1:8 NUMBER 0 | ||||
| 1:9-1:9 COMMA , | ||||
| 1:11-1:13 NUMBER 0.0 | ||||
| 1:14-1:14 COMMA , | ||||
| 1:16-1:18 NUMBER 0e1 | ||||
| 1:19-1:19 COMMA , | ||||
| 1:21-1:23 NUMBER .23 | ||||
| 1:24-1:24 COMMA , | ||||
| 1:26-1:29 NUMBER 0x23 | ||||
| 1:30-1:30 COMMA , | ||||
| 1:32-1:36 NUMBER 23e-2 | ||||
| 1:37-1:37 COMMA , | ||||
| 1:39-1:43 NUMBER 23e+2 | ||||
| ``` | ||||
| 
 | ||||
| We also handle malformed numbers: | ||||
| 
 | ||||
| ```lua | ||||
| return 2..3, 2eee2 | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| 1:1-1:6 RETURN return | ||||
| This isn't a valid number. | ||||
|    | | ||||
|  1 | return 2..3, 2eee2 | ||||
|    |        ^^^^ | ||||
| Numbers must be in one of the following formats: 123, 3.14, 23e35, 0x01AF. | ||||
| 1:8-1:11 NUMBER 2..3 | ||||
| 1:12-1:12 COMMA , | ||||
| This isn't a valid number. | ||||
|    | | ||||
|  1 | return 2..3, 2eee2 | ||||
|    |              ^^^^^ | ||||
| Numbers must be in one of the following formats: 123, 3.14, 23e35, 0x01AF. | ||||
| 1:14-1:18 NUMBER 2eee2 | ||||
| ``` | ||||
| 
 | ||||
| # Unknown tokens | ||||
| 
 | ||||
| We can suggest alternatives for possible errors: | ||||
| 
 | ||||
| ```lua | ||||
| if a != b then end | ||||
| if a ~= b then end | ||||
| if a && b then end | ||||
| if a || b then end | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| 1:1-1:2 IF if | ||||
| 1:4-1:4 IDENT a | ||||
| Unexpected character. | ||||
|    | | ||||
|  1 | if a != b then end | ||||
|    |      ^^ | ||||
| Tip: Replace this with ~= to check if two values are not equal. | ||||
| 1:6-1:7 NE != | ||||
| 1:9-1:9 IDENT b | ||||
| 1:11-1:14 THEN then | ||||
| 1:16-1:18 END end | ||||
| 2:1-2:2 IF if | ||||
| 2:4-2:4 IDENT a | ||||
| 2:6-2:7 NE ~= | ||||
| 2:9-2:9 IDENT b | ||||
| 2:11-2:14 THEN then | ||||
| 2:16-2:18 END end | ||||
| 3:1-3:2 IF if | ||||
| 3:4-3:4 IDENT a | ||||
| Unexpected character. | ||||
|    | | ||||
|  3 | if a && b then end | ||||
|    |      ^^ | ||||
| Tip: Replace this with and to check if both values are true. | ||||
| 3:6-3:7 AND && | ||||
| 3:9-3:9 IDENT b | ||||
| 3:11-3:14 THEN then | ||||
| 3:16-3:18 END end | ||||
| 4:1-4:2 IF if | ||||
| 4:4-4:4 IDENT a | ||||
| Unexpected character. | ||||
|    | | ||||
|  4 | if a || b then end | ||||
|    |      ^^ | ||||
| Tip: Replace this with or to check if either value is true. | ||||
| 4:6-4:7 OR || | ||||
| 4:9-4:9 IDENT b | ||||
| 4:11-4:14 THEN then | ||||
| 4:16-4:18 END end | ||||
| ``` | ||||
| 
 | ||||
| For entirely unknown glyphs we should just give up and return an `ERROR` token. | ||||
| 
 | ||||
| ```lua | ||||
| return $*&(*)xyz | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| 1:1-1:6 RETURN return | ||||
| Unexpected character. | ||||
|    | | ||||
|  1 | return $*&(*)xyz | ||||
|    |        ^ This character isn't usable in Lua code. | ||||
| 1:8-1:10 ERROR $*& | ||||
| ``` | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -0,0 +1,370 @@ | ||||
| We provide a parser for Lua source code. Here we test that the parser reports | ||||
| sensible syntax errors in specific cases. | ||||
| 
 | ||||
| # Expressions | ||||
| 
 | ||||
| ## Invalid equals | ||||
| We correct the user if they type `=` instead of `==`. | ||||
| 
 | ||||
| ```lua | ||||
| if a = b then end | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| Unexpected = in expression. | ||||
|    | | ||||
|  1 | if a = b then end | ||||
|    |      ^ | ||||
| Tip: Replace this with == to check if two values are equal. | ||||
| ``` | ||||
| 
 | ||||
| We apply a slightly different error when this occurs in tables: | ||||
| 
 | ||||
| ```lua | ||||
| return { "abc" = "def" } | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| Unexpected = in expression. | ||||
|    | | ||||
|  1 | return { "abc" = "def" } | ||||
|    |                ^ | ||||
| Tip: Wrap the preceding expression in [ and ] to use it as a table key. | ||||
| ``` | ||||
| 
 | ||||
| Note this doesn't occur if this there's already a table key here: | ||||
| 
 | ||||
| ```lua | ||||
| return { x = "abc" = } | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| Unexpected = in expression. | ||||
|    | | ||||
|  1 | return { x = "abc" = } | ||||
|    |                    ^ | ||||
| Tip: Replace this with == to check if two values are equal. | ||||
| ``` | ||||
| 
 | ||||
| ## Unclosed parenthesis | ||||
| We warn on unclosed parenthesis in expressions: | ||||
| 
 | ||||
| ```lua | ||||
| return (2 | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| Unexpected end of file. Are you missing a closing bracket? | ||||
|    | | ||||
|  1 | return (2 | ||||
|    |        ^ Brackets were opened here. | ||||
|    | | ||||
|  1 | return (2 | ||||
|    |          ^ Unexpected end of file here. | ||||
| ``` | ||||
| 
 | ||||
| Function calls: | ||||
| 
 | ||||
| ```lua | ||||
| return f(2 | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| Unexpected end of file. Are you missing a closing bracket? | ||||
|    | | ||||
|  1 | return f(2 | ||||
|    |         ^ Brackets were opened here. | ||||
|    | | ||||
|  1 | return f(2 | ||||
|    |           ^ Unexpected end of file here. | ||||
| ``` | ||||
| 
 | ||||
| and function definitions: | ||||
| 
 | ||||
| ```lua | ||||
| local function f(a | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| Unexpected end of file. Are you missing a closing bracket? | ||||
|    | | ||||
|  1 | local function f(a | ||||
|    |                 ^ Brackets were opened here. | ||||
|    | | ||||
|  1 | local function f(a | ||||
|    |                   ^ Unexpected end of file here. | ||||
| ``` | ||||
| 
 | ||||
| # Statements | ||||
| 
 | ||||
| ## Local functions with table identifiers | ||||
| We provide a custom error for using `.` inside a `local function` name. | ||||
| 
 | ||||
| ```lua | ||||
| local function x.f() end | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| Cannot use local function with a table key. | ||||
|    | | ||||
|  1 | local function x.f() end | ||||
|    |                 ^ . appears here. | ||||
|    | | ||||
|  1 | local function x.f() end | ||||
|    | ^^^^^ Tip: Try removing this local keyword. | ||||
| ``` | ||||
| 
 | ||||
| ## Standalone identifiers | ||||
| A common error is a user forgetting to use `()` to call a function. We provide | ||||
| a custom error for this case: | ||||
| 
 | ||||
| ```lua | ||||
| term.clear | ||||
| local _ = 1 | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| Unexpected symbol after variable. | ||||
|    | | ||||
|  1 | term.clear | ||||
|    |           ^ Expected something before the end of the line. | ||||
| Tip: Use () to call with no arguments. | ||||
| ``` | ||||
| 
 | ||||
| If the next symbol is on the same line we provide a slightly different error: | ||||
| 
 | ||||
| ```lua | ||||
| x 1 | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| Unexpected symbol after name. | ||||
|    | | ||||
|  1 | x 1 | ||||
|    |   ^ | ||||
| Did you mean to assign this or call it as a function? | ||||
| ``` | ||||
| 
 | ||||
| An EOF token is treated as a new line. | ||||
| 
 | ||||
| ```lua | ||||
| term.clear | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| Unexpected symbol after variable. | ||||
|    | | ||||
|  1 | term.clear | ||||
|    |           ^ Expected something before the end of the line. | ||||
| Tip: Use () to call with no arguments. | ||||
| ``` | ||||
| 
 | ||||
| ## If statements | ||||
| For if statements, we say when we expected the `then` keyword. | ||||
| 
 | ||||
| ```lua | ||||
| if 0 | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| Expected then after if condition. | ||||
|    | | ||||
|  1 | if 0 | ||||
|    | ^^ If statement started here. | ||||
|    | | ||||
|  1 | if 0 | ||||
|    |     ^ Expected then before here. | ||||
| ``` | ||||
| 
 | ||||
| ```lua | ||||
| if 0 then | ||||
| elseif 0 | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| Expected then after if condition. | ||||
|    | | ||||
|  2 | elseif 0 | ||||
|    | ^^^^^^ If statement started here. | ||||
|    | | ||||
|  2 | elseif 0 | ||||
|    |         ^ Expected then before here. | ||||
| ``` | ||||
| 
 | ||||
| ## Expecting `end` | ||||
| We provide errors for missing `end`s. | ||||
| 
 | ||||
| ```lua | ||||
| if true then | ||||
|   print("Hello") | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| Unexpected end of file. Expected end or another statement. | ||||
|    | | ||||
|  1 | if true then | ||||
|    | ^^ Block started here. | ||||
|    | | ||||
|  2 |   print("Hello") | ||||
|    |                 ^ Expected end of block here. | ||||
| ``` | ||||
| 
 | ||||
| ```lua | ||||
| if true then | ||||
| else | ||||
|   print("Hello") | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| Unexpected end of file. Expected end or another statement. | ||||
|    | | ||||
|  2 | else | ||||
|    | ^^^^ Block started here. | ||||
|    | | ||||
|  3 |   print("Hello") | ||||
|    |                 ^ Expected end of block here. | ||||
| ``` | ||||
| 
 | ||||
| ```lua | ||||
| if true then | ||||
| elseif true then | ||||
|   print("Hello") | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| Unexpected end of file. Expected end or another statement. | ||||
|    | | ||||
|  2 | elseif true then | ||||
|    | ^^^^^^ Block started here. | ||||
|    | | ||||
|  3 |   print("Hello") | ||||
|    |                 ^ Expected end of block here. | ||||
| ``` | ||||
| 
 | ||||
| ```lua | ||||
| while true do | ||||
|   print("Hello") | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| Unexpected end of file. Expected end or another statement. | ||||
|    | | ||||
|  1 | while true do | ||||
|    | ^^^^^ Block started here. | ||||
|    | | ||||
|  2 |   print("Hello") | ||||
|    |                 ^ Expected end of block here. | ||||
| ``` | ||||
| 
 | ||||
| ```lua | ||||
| local function f() | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| Unexpected end of file. Expected end or another statement. | ||||
|    | | ||||
|  1 | local function f() | ||||
|    | ^^^^^^^^^^^^^^ Block started here. | ||||
|    | | ||||
|  1 | local function f() | ||||
|    |                   ^ Expected end of block here. | ||||
| ``` | ||||
| 
 | ||||
| ```lua | ||||
| function f() | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| Unexpected end of file. Expected end or another statement. | ||||
|    | | ||||
|  1 | function f() | ||||
|    | ^^^^^^^^ Block started here. | ||||
|    | | ||||
|  1 | function f() | ||||
|    |             ^ Expected end of block here. | ||||
| ``` | ||||
| 
 | ||||
| ```lua | ||||
| return function() | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| Unexpected end of file. Expected end or another statement. | ||||
|    | | ||||
|  1 | return function() | ||||
|    |        ^^^^^^^^ Block started here. | ||||
|    | | ||||
|  1 | return function() | ||||
|    |                  ^ Expected end of block here. | ||||
| ``` | ||||
| 
 | ||||
| While we typically see these errors at the end of the file, there are some cases | ||||
| where it may occur before then: | ||||
| 
 | ||||
| ```lua | ||||
| return (function() | ||||
|   if true then | ||||
| )() | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| Unexpected ). Expected end or another statement. | ||||
|    | | ||||
|  2 |   if true then | ||||
|    |   ^^ Block started here. | ||||
|    | | ||||
|  3 | )() | ||||
|    | ^ Expected end of block here. | ||||
| ``` | ||||
| 
 | ||||
| Note we do not currently attempt to identify mismatched `end`s. This might be | ||||
| something to do in the future. | ||||
| 
 | ||||
| ```lua | ||||
| if true then | ||||
|   while true do | ||||
| end | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| Unexpected end of file. Expected end or another statement. | ||||
|    | | ||||
|  1 | if true then | ||||
|    | ^^ Block started here. | ||||
|    | | ||||
|  3 | end | ||||
|    |    ^ Expected end of block here. | ||||
| ``` | ||||
| 
 | ||||
| ## Unexpected `end` | ||||
| We also print when there's more `end`s than expected. | ||||
| 
 | ||||
| ```lua | ||||
| if true then | ||||
| end | ||||
| end | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| Unexpected end. | ||||
|    | | ||||
|  3 | end | ||||
|    | ^^^ | ||||
| Your program contains more ends than needed. Check each block (if, for, function, ...) only has one end. | ||||
| ``` | ||||
| 
 | ||||
| ```lua | ||||
| repeat | ||||
|   if true then | ||||
|   end | ||||
|   end | ||||
| until true | ||||
| ``` | ||||
| 
 | ||||
| ```txt | ||||
| Unexpected end. | ||||
|    | | ||||
|  4 |   end | ||||
|    |   ^^^ | ||||
| Your program contains more ends than needed. Check each block (if, for, function, ...) only has one end. | ||||
| ``` | ||||
| @@ -0,0 +1,107 @@ | ||||
| local expect = require "cc.expect".expect | ||||
| local lex_one = require "cc.internal.syntax.lexer".lex_one | ||||
| local parser = require "cc.internal.syntax.parser" | ||||
| local tokens, last_token = parser.tokens, parser.tokens.COMMENT | ||||
|  | ||||
| --- Make a dummy context. | ||||
| local function make_context(input) | ||||
|     local lines = { 1 } | ||||
|     local function line(pos) lines[#lines + 1] = pos end | ||||
|  | ||||
|     local function get_pos(pos) | ||||
|         for i = #lines, 1, -1 do | ||||
|             local start = lines[i] | ||||
|             if pos >= start then return i, pos - start + 1, start end | ||||
|         end | ||||
|  | ||||
|         error("Position is <= 0", 2) | ||||
|     end | ||||
|  | ||||
|     return { line = line, get_pos = get_pos, lines = lines } | ||||
| end | ||||
|  | ||||
| --[[- Run a parser on an input string, capturing its output. | ||||
|  | ||||
| This uses a simplified method of displaying errors (compared with | ||||
| @{cc.internal.error_printer}), which is suitable for printing to a file. | ||||
|  | ||||
| @tparam string input The input string to parse. | ||||
| @tparam[opt=false] boolean print_tokens Whether to print each token as its parsed. | ||||
| @tparam[opt] number start The start state of the parser. | ||||
| @treturn string The parser's output | ||||
| ]] | ||||
| local function capture_parser(input, print_tokens, start) | ||||
|     expect(1, input, "string") | ||||
|     expect(2, print_tokens, "boolean", "nil") | ||||
|     expect(3, start, "number", "nil") | ||||
|  | ||||
|     local error_sentinel = {} | ||||
|     local out = {} | ||||
|     local function print(x) out[#out + 1] = tostring(x) end | ||||
|  | ||||
|     local function get_name(token) | ||||
|         for name, tok in pairs(tokens) do if tok == token then return name end end | ||||
|         return "?[" .. tostring(token) .. "]" | ||||
|     end | ||||
|  | ||||
|     local context = make_context(input) | ||||
|     function context.report(message) | ||||
|         expect(3, message, "table") | ||||
|  | ||||
|         for _, msg in ipairs(message) do | ||||
|             if type(msg) == "table" and msg.tag == "annotate" then | ||||
|                 local line, col = context.get_pos(msg.start_pos) | ||||
|                 local end_line, end_col = context.get_pos(msg.end_pos) | ||||
|  | ||||
|                 local contents = input:match("^([^\r\n]*)", context.lines[line]) | ||||
|                 print("   |") | ||||
|                 print(("%2d | %s"):format(line, contents)) | ||||
|  | ||||
|                 local indicator = line == end_line and ("^"):rep(end_col - col + 1) or "^..." | ||||
|                 if #msg.msg > 0 then | ||||
|                     print(("   | %s%s %s"):format((" "):rep(col - 1), indicator, msg.msg)) | ||||
|                 else | ||||
|                     print(("   | %s%s"):format((" "):rep(col - 1), indicator)) | ||||
|                 end | ||||
|             else | ||||
|                 print(tostring(msg)) | ||||
|             end | ||||
|         end | ||||
|     end | ||||
|  | ||||
|     local pos = 1 | ||||
|     local ok, err = xpcall(function() | ||||
|         return parser.parse(context, function() | ||||
|             while true do | ||||
|                 local token, start, finish, content = lex_one(context, input, pos) | ||||
|                 if not token then return tokens.EOF, #input + 1, #input + 1 end | ||||
|  | ||||
|                 if print_tokens then | ||||
|                     local start_line, start_col = context.get_pos(start) | ||||
|                     local end_line, end_col = context.get_pos(finish) | ||||
|                     local text = input:sub(start, finish) | ||||
|                     print(("%d:%d-%d:%d %s %s"):format( | ||||
|                         start_line, start_col, end_line, end_col, | ||||
|                         get_name(token), content or text:gsub("\n", "<NL>") | ||||
|                     )) | ||||
|                 end | ||||
|  | ||||
|                 pos = finish + 1 | ||||
|  | ||||
|                 if token < last_token then | ||||
|                     return token, start, finish | ||||
|                 elseif token == tokens.ERROR then | ||||
|                     error(error_sentinel) | ||||
|                 end | ||||
|             end | ||||
|         end, start) | ||||
|     end, debug.traceback) | ||||
|  | ||||
|     if not ok and err ~= error_sentinel then | ||||
|         print(err) | ||||
|     end | ||||
|  | ||||
|     return table.concat(out, "\n") | ||||
| end | ||||
|  | ||||
| return { make_context = make_context, capture_parser = capture_parser } | ||||
| @@ -4,7 +4,7 @@ describe("The bg program", function() | ||||
|     it("opens a tab in the background", function() | ||||
|         local openTab = stub(shell, "openTab", function() return 12 end) | ||||
|         local switchTab = stub(shell, "switchTab") | ||||
|         capture(stub, "bg") | ||||
|         capture("bg") | ||||
|         expect(openTab):called_with("shell") | ||||
|         expect(switchTab):called(0) | ||||
|     end) | ||||
|   | ||||
| @@ -4,7 +4,7 @@ describe("The fg program", function() | ||||
|     it("opens the shell in the foreground", function() | ||||
|         local openTab = stub(shell, "openTab", function() return 12 end) | ||||
|         local switchTab = stub(shell, "switchTab") | ||||
|         capture(stub, "fg") | ||||
|         capture("fg") | ||||
|         expect(openTab):called_with("shell") | ||||
|         expect(switchTab):called_with(12) | ||||
|     end) | ||||
|   | ||||
| @@ -2,27 +2,27 @@ local capture = require "test_helpers".capture_program | ||||
|  | ||||
| describe("The alias program", function() | ||||
|     it("displays its usage when given too many arguments", function() | ||||
|         expect(capture(stub, "alias a b c")) | ||||
|         expect(capture("alias a b c")) | ||||
|             :matches { ok = true, output = "Usage: alias <alias> <program>\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("lists aliases", function() | ||||
|         local pagedTabulate = stub(textutils, "pagedTabulate", function(x) print(table.unpack(x)) end) | ||||
|         stub(shell, "aliases", function() return { cp = "copy" } end) | ||||
|         expect(capture(stub, "alias")) | ||||
|         expect(capture("alias")) | ||||
|             :matches { ok = true, output = "cp:copy\n", error = "" } | ||||
|         expect(pagedTabulate):called_with_matching({ "cp:copy" }) | ||||
|     end) | ||||
|  | ||||
|     it("sets an alias", function() | ||||
|         local setAlias = stub(shell, "setAlias") | ||||
|         capture(stub, "alias test Hello") | ||||
|         capture("alias test Hello") | ||||
|         expect(setAlias):called_with("test", "Hello") | ||||
|     end) | ||||
|  | ||||
|     it("clears an alias", function() | ||||
|         local clearAlias = stub(shell, "clearAlias") | ||||
|         capture(stub, "alias test") | ||||
|         capture("alias test") | ||||
|         expect(clearAlias):called_with("test") | ||||
|     end) | ||||
| end) | ||||
|   | ||||
| @@ -3,17 +3,17 @@ local capture = require "test_helpers".capture_program | ||||
| describe("The cd program", function() | ||||
|     it("changes into a directory", function() | ||||
|         local setDir = stub(shell, "setDir") | ||||
|         capture(stub, "cd /rom/programs") | ||||
|         capture("cd /rom/programs") | ||||
|         expect(setDir):called_with("rom/programs") | ||||
|     end) | ||||
|  | ||||
|     it("does not move into a non-existent directory", function() | ||||
|         expect(capture(stub, "cd /rom/nothing")) | ||||
|         expect(capture("cd /rom/nothing")) | ||||
|             :matches { ok = true, output = "Not a directory\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("displays the usage when given no arguments", function() | ||||
|         expect(capture(stub, "cd")) | ||||
|         expect(capture("cd")) | ||||
|             :matches { ok = true, output = "Usage: cd <path>\n", error = "" } | ||||
|     end) | ||||
| end) | ||||
|   | ||||
| @@ -5,7 +5,7 @@ describe("The clear program", function() | ||||
|         local clear = stub(term, "clear") | ||||
|         local setCursorPos = stub(term, "setCursorPos") | ||||
|  | ||||
|         capture(stub, "clear") | ||||
|         capture("clear") | ||||
|  | ||||
|         expect(clear):called(1) | ||||
|         expect(setCursorPos):called_with(1, 1) | ||||
|   | ||||
| @@ -3,7 +3,7 @@ local capture = require "test_helpers".capture_program | ||||
| describe("The commands program", function() | ||||
|     it("displays an error without the commands api", function() | ||||
|         stub(_G, "commands", nil) | ||||
|         expect(capture(stub, "/rom/programs/command/commands.lua")) | ||||
|         expect(capture("/rom/programs/command/commands.lua")) | ||||
|             :matches { ok = true, output = "", error = "Requires a Command Computer.\n" } | ||||
|     end) | ||||
|  | ||||
| @@ -13,7 +13,7 @@ describe("The commands program", function() | ||||
|             list = function() return { "computercraft" } end, | ||||
|         }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/command/commands.lua")) | ||||
|         expect(capture("/rom/programs/command/commands.lua")) | ||||
|             :matches { ok = true, output = "Available commands:\ncomputercraft\n", error = "" } | ||||
|         expect(pagedTabulate):called_with_matching({ "computercraft" }) | ||||
|     end) | ||||
|   | ||||
| @@ -3,13 +3,13 @@ local capture = require "test_helpers".capture_program | ||||
| describe("The exec program", function() | ||||
|     it("displays an error without the commands api", function() | ||||
|         stub(_G, "commands", nil) | ||||
|         expect(capture(stub, "/rom/programs/command/exec.lua")) | ||||
|         expect(capture("/rom/programs/command/exec.lua")) | ||||
|             :matches { ok = true, output = "", error = "Requires a Command Computer.\n" } | ||||
|     end) | ||||
|  | ||||
|     it("displays its usage when given no argument", function() | ||||
|         stub(_G, "commands", {}) | ||||
|         expect(capture(stub, "/rom/programs/command/exec.lua")) | ||||
|         expect(capture("/rom/programs/command/exec.lua")) | ||||
|             :matches { ok = true, output = "", error = "Usage: /rom/programs/command/exec.lua <command>\n" } | ||||
|     end) | ||||
|  | ||||
| @@ -18,7 +18,7 @@ describe("The exec program", function() | ||||
|             exec = function() return true, { "Hello World!" } end, | ||||
|         }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/command/exec.lua computercraft")) | ||||
|         expect(capture("/rom/programs/command/exec.lua computercraft")) | ||||
|             :matches { ok = true, output = "Success\nHello World!\n", error = "" } | ||||
|     end) | ||||
|  | ||||
| @@ -27,7 +27,7 @@ describe("The exec program", function() | ||||
|             exec = function() return false, { "Hello World!" } end, | ||||
|         }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/command/exec.lua computercraft")) | ||||
|         expect(capture("/rom/programs/command/exec.lua computercraft")) | ||||
|             :matches { ok = true, output = "Hello World!\n", error = "Failed\n" } | ||||
|     end) | ||||
| end) | ||||
|   | ||||
| @@ -15,26 +15,26 @@ describe("The copy program", function() | ||||
|     end) | ||||
|  | ||||
|     it("fails when copying a non-existent file", function() | ||||
|         expect(capture(stub, "copy nothing destination")) | ||||
|         expect(capture("copy nothing destination")) | ||||
|             :matches { ok = true, output = "", error = "No matching files\n" } | ||||
|     end) | ||||
|  | ||||
|     it("fails when overwriting an existing file", function() | ||||
|         touch("/test-files/copy/c.txt") | ||||
|  | ||||
|         expect(capture(stub, "copy /test-files/copy/c.txt /test-files/copy/c.txt")) | ||||
|         expect(capture("copy /test-files/copy/c.txt /test-files/copy/c.txt")) | ||||
|             :matches { ok = true, output = "", error = "Destination exists\n" } | ||||
|     end) | ||||
|  | ||||
|     it("fails when copying into read-only locations", function() | ||||
|         touch("/test-files/copy/d.txt") | ||||
|  | ||||
|         expect(capture(stub, "copy /test-files/copy/d.txt /rom/test.txt")) | ||||
|         expect(capture("copy /test-files/copy/d.txt /rom/test.txt")) | ||||
|             :matches { ok = true, output = "", error = "Destination is read-only\n" } | ||||
|     end) | ||||
|  | ||||
|     it("displays the usage when given no arguments", function() | ||||
|         expect(capture(stub, "copy")) | ||||
|         expect(capture("copy")) | ||||
|             :matches { ok = true, output = "Usage: copy <source> <destination>\n", error = "" } | ||||
|     end) | ||||
| end) | ||||
|   | ||||
| @@ -36,17 +36,17 @@ describe("The rm program", function() | ||||
|     end) | ||||
|  | ||||
|     it("displays the usage with no arguments", function() | ||||
|         expect(capture(stub, "rm")) | ||||
|         expect(capture("rm")) | ||||
|             :matches { ok = true, output = "Usage: rm <paths>\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("errors when trying to delete a read-only file", function() | ||||
|         expect(capture(stub, "rm /rom/startup.lua")) | ||||
|         expect(capture("rm /rom/startup.lua")) | ||||
|             :matches { ok = true, output = "", error = "Cannot delete read-only file /rom/startup.lua\n" } | ||||
|     end) | ||||
|  | ||||
|     it("errors when trying to delete the root mount", function() | ||||
|         expect(capture(stub, "rm /")):matches { | ||||
|         expect(capture("rm /")):matches { | ||||
|             ok = true, | ||||
|             output = "To delete its contents run rm /*\n", | ||||
|             error = "Cannot delete mount /\n", | ||||
| @@ -54,7 +54,7 @@ describe("The rm program", function() | ||||
|     end) | ||||
|  | ||||
|     it("errors when a glob fails to match", function() | ||||
|         expect(capture(stub, "rm", "never-existed")) | ||||
|         expect(capture("rm", "never-existed")) | ||||
|             :matches { ok = true, output = "", error = "never-existed: No matching files\n" } | ||||
|     end) | ||||
| end) | ||||
|   | ||||
| @@ -4,13 +4,13 @@ describe("The drive program", function() | ||||
|     it("run the program", function() | ||||
|         local getFreeSpace = stub(fs, "getFreeSpace", function() return 1234e4 end) | ||||
|  | ||||
|         expect(capture(stub, "drive")) | ||||
|         expect(capture("drive")) | ||||
|             :matches { ok = true, output = "hdd (12.3MB remaining)\n", error = "" } | ||||
|         expect(getFreeSpace):called(1):called_with("") | ||||
|     end) | ||||
|  | ||||
|     it("fails on a non-existent path", function() | ||||
|         expect(capture(stub, "drive /rom/nothing")) | ||||
|         expect(capture("drive /rom/nothing")) | ||||
|             :matches { ok = true, output = "No such path\n", error = "" } | ||||
|     end) | ||||
| end) | ||||
|   | ||||
| @@ -3,7 +3,7 @@ local capture = require "test_helpers".capture_program | ||||
| describe("The edit program", function() | ||||
|  | ||||
|     it("displays its usage when given no argument", function() | ||||
|         expect(capture(stub, "edit")) | ||||
|         expect(capture("edit")) | ||||
|             :matches { ok = true, output = "Usage: edit <path>\n", error = "" } | ||||
|     end) | ||||
| end) | ||||
|   | ||||
| @@ -2,12 +2,12 @@ local capture = require "test_helpers".capture_program | ||||
|  | ||||
| describe("The eject program", function() | ||||
|     it("displays its usage when given no argument", function() | ||||
|         expect(capture(stub, "eject")) | ||||
|         expect(capture("eject")) | ||||
|             :matches { ok = true, output = "Usage: eject <drive>\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("fails when trying to eject a non-drive", function() | ||||
|         expect(capture(stub, "eject /rom")) | ||||
|         expect(capture("eject /rom")) | ||||
|             :matches { ok = true, output = "Nothing in /rom drive\n", error = "" } | ||||
|     end) | ||||
| end) | ||||
|   | ||||
| @@ -3,7 +3,7 @@ local capture = require "test_helpers".capture_program | ||||
| describe("The exit program", function() | ||||
|     it("exits the shell", function() | ||||
|         local exit = stub(shell, "exit") | ||||
|         expect(capture(stub, "exit")):matches { ok = true, combined = "" } | ||||
|         expect(capture("exit")):matches { ok = true, combined = "" } | ||||
|         expect(exit):called(1) | ||||
|     end) | ||||
| end) | ||||
|   | ||||
| @@ -2,7 +2,7 @@ local capture = require "test_helpers".capture_program | ||||
|  | ||||
| describe("The paint program", function() | ||||
|     it("displays its usage when given no arguments", function() | ||||
|         expect(capture(stub, "paint")) | ||||
|         expect(capture("paint")) | ||||
|             :matches { ok = true, output = "Usage: paint <path>\n", error = "" } | ||||
|     end) | ||||
| end) | ||||
|   | ||||
| @@ -2,12 +2,12 @@ local capture = require "test_helpers".capture_program | ||||
|  | ||||
| describe("The dj program", function() | ||||
|     it("displays its usage when given too many arguments", function() | ||||
|         expect(capture(stub, "dj a b c")) | ||||
|         expect(capture("dj a b c")) | ||||
|             :matches { ok = true, output = "Usages:\ndj play\ndj play <drive>\ndj stop\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("fails when no disks are present", function() | ||||
|         expect(capture(stub, "dj")) | ||||
|         expect(capture("dj")) | ||||
|             :matches { ok = true, output = "No Music Discs in attached disk drives\n", error = "" } | ||||
|     end) | ||||
| end) | ||||
|   | ||||
| @@ -3,7 +3,7 @@ local capture = require "test_helpers".capture_program | ||||
| describe("The hello program", function() | ||||
|     it("says hello", function() | ||||
|         local slowPrint = stub(textutils, "slowPrint", function(...) return print(...) end) | ||||
|         expect(capture(stub, "hello")) | ||||
|         expect(capture("hello")) | ||||
|             :matches { ok = true, output = "Hello World!\n", error = "" } | ||||
|         expect(slowPrint):called(1) | ||||
|     end) | ||||
|   | ||||
| @@ -2,21 +2,21 @@ local capture = require "test_helpers".capture_program | ||||
|  | ||||
| describe("The gps program", function() | ||||
|     it("displays its usage when given no arguments", function() | ||||
|         expect(capture(stub, "gps")) | ||||
|         expect(capture("gps")) | ||||
|             :matches { ok = true, output = "Usages:\ngps host\ngps host <x> <y> <z>\ngps locate\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("fails on a pocket computer", function() | ||||
|         stub(_G, "pocket", {}) | ||||
|  | ||||
|         expect(capture(stub, "gps host")) | ||||
|         expect(capture("gps host")) | ||||
|             :matches { ok = true, output = "GPS Hosts must be stationary\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("can locate the computer", function() | ||||
|         local locate = stub(gps, "locate", function() print("Some debugging information.") end) | ||||
|  | ||||
|         expect(capture(stub, "gps locate")) | ||||
|         expect(capture("gps locate")) | ||||
|             :matches { ok = true, output = "Some debugging information.\n", error = "" } | ||||
|         expect(locate):called_with(2, true) | ||||
|     end) | ||||
|   | ||||
| @@ -20,7 +20,7 @@ describe("The help program", function() | ||||
|     end | ||||
|  | ||||
|     it("errors when there is no such help file", function() | ||||
|         expect(capture(stub, "help nothing")) | ||||
|         expect(capture("help nothing")) | ||||
|             :matches { ok = true, error = "No help available\n", output = "" } | ||||
|     end) | ||||
|  | ||||
|   | ||||
| @@ -34,7 +34,7 @@ describe("The pastebin program", function() | ||||
|  | ||||
|     it("downloads one file", function() | ||||
|         setup_request() | ||||
|         capture(stub, "pastebin", "get", "abcde", "testdown") | ||||
|         capture("pastebin", "get", "abcde", "testdown") | ||||
|  | ||||
|         expect(fs.exists("/testdown")):eq(true) | ||||
|     end) | ||||
| @@ -42,7 +42,7 @@ describe("The pastebin program", function() | ||||
|     it("runs a program from the internet", function() | ||||
|         setup_request() | ||||
|  | ||||
|         expect(capture(stub, "pastebin", "run", "abcde", "a", "b", "c")) | ||||
|         expect(capture("pastebin", "run", "abcde", "a", "b", "c")) | ||||
|             :matches { ok = true, output = "Connecting to pastebin.com... Success.\nHello a b c\n", error = "" } | ||||
|     end) | ||||
|  | ||||
| @@ -52,21 +52,21 @@ describe("The pastebin program", function() | ||||
|         local file = fs.open("testup", "w") | ||||
|         file.close() | ||||
|  | ||||
|         expect(capture(stub, "pastebin", "put", "testup")) | ||||
|         expect(capture("pastebin", "put", "testup")) | ||||
|             :matches { ok = true, output = "Connecting to pastebin.com... Success.\nUploaded as https://pastebin.com/abcde\nRun \"pastebin get abcde\" to download anywhere\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("upload a not existing program to pastebin", function() | ||||
|         setup_request() | ||||
|  | ||||
|         expect(capture(stub, "pastebin", "put", "nothing")) | ||||
|         expect(capture("pastebin", "put", "nothing")) | ||||
|             :matches { ok = true, output = "No such file\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("displays its usage when given no arguments", function() | ||||
|         setup_request() | ||||
|  | ||||
|         expect(capture(stub, "pastebin")) | ||||
|         expect(capture("pastebin")) | ||||
|             :matches { ok = true, output = "Usages:\npastebin put <filename>\npastebin get <code> <filename>\npastebin run <code> <arguments>\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|   | ||||
| @@ -23,7 +23,7 @@ describe("The wget program", function() | ||||
|         fs.delete("/example.com") | ||||
|         setup_request(default_contents) | ||||
|  | ||||
|         capture(stub, "wget", "https://example.com") | ||||
|         capture("wget", "https://example.com") | ||||
|  | ||||
|         expect(fs.exists("/example.com")):eq(true) | ||||
|     end) | ||||
| @@ -32,7 +32,7 @@ describe("The wget program", function() | ||||
|         fs.delete("/test-files/download") | ||||
|         setup_request(default_contents) | ||||
|  | ||||
|         capture(stub, "wget", "https://example.com /test-files/download") | ||||
|         capture("wget", "https://example.com /test-files/download") | ||||
|  | ||||
|         expect(fs.exists("/test-files/download")):eq(true) | ||||
|     end) | ||||
| @@ -41,7 +41,7 @@ describe("The wget program", function() | ||||
|         fs.delete("/test-files/download") | ||||
|         setup_request(nil) | ||||
|  | ||||
|         capture(stub, "wget", "https://example.com", "/test-files/download") | ||||
|         capture("wget", "https://example.com", "/test-files/download") | ||||
|  | ||||
|         expect(fs.exists("/test-files/download")):eq(true) | ||||
|         expect(fs.getSize("/test-files/download")):eq(0) | ||||
| @@ -50,7 +50,7 @@ describe("The wget program", function() | ||||
|     it("cannot save to rom", function() | ||||
|         setup_request(default_contents) | ||||
|  | ||||
|         expect(capture(stub, "wget", "https://example.com", "/rom/a-file.txt")):matches { | ||||
|         expect(capture("wget", "https://example.com", "/rom/a-file.txt")):matches { | ||||
|             ok = true, | ||||
|             output = "Connecting to https://example.com... Success.\n", | ||||
|             error = "Cannot save file: /rom/a-file.txt: Access denied\n", | ||||
| @@ -60,14 +60,14 @@ describe("The wget program", function() | ||||
|     it("runs a program from the internet", function() | ||||
|         setup_request(default_contents) | ||||
|  | ||||
|         expect(capture(stub, "wget", "run", "http://test.com", "a", "b", "c")) | ||||
|         expect(capture("wget", "run", "http://test.com", "a", "b", "c")) | ||||
|             :matches { ok = true, output = "Connecting to http://test.com... Success.\nHello a b c\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("displays its usage when given no arguments", function() | ||||
|         setup_request(default_contents) | ||||
|  | ||||
|         expect(capture(stub, "wget")) | ||||
|         expect(capture("wget")) | ||||
|             :matches { ok = true, output = "Usage:\nwget <url> [filename]\nwget run <url>\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,7 @@ describe("The id program", function() | ||||
|     it("displays computer id", function() | ||||
|         local id = os.getComputerID() | ||||
|  | ||||
|         expect(capture(stub, "id")) | ||||
|         expect(capture("id")) | ||||
|             :matches { ok = true, output = "This is computer #" .. id .. "\n", error = "" } | ||||
|     end) | ||||
| end) | ||||
|   | ||||
| @@ -2,33 +2,33 @@ local capture = require "test_helpers".capture_program | ||||
|  | ||||
| describe("The label program", function() | ||||
|     it("displays its usage when given no arguments", function() | ||||
|         expect(capture(stub, "label")) | ||||
|         expect(capture("label")) | ||||
|             :matches { ok = true, output = "Usages:\nlabel get\nlabel get <drive>\nlabel set <text>\nlabel set <drive> <text>\nlabel clear\nlabel clear <drive>\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     describe("displays the computer's label", function() | ||||
|         it("when it is not labelled", function() | ||||
|             stub(os, "getComputerLabel", function() return nil end) | ||||
|             expect(capture(stub, "label get")) | ||||
|             expect(capture("label get")) | ||||
|                 :matches { ok = true, output = "No Computer label\n", error = "" } | ||||
|         end) | ||||
|  | ||||
|         it("when it is labelled", function() | ||||
|             stub(os, "getComputerLabel", function() return "Test" end) | ||||
|             expect(capture(stub, "label get")) | ||||
|             expect(capture("label get")) | ||||
|                 :matches { ok = true, output = "Computer label is \"Test\"\n", error = "" } | ||||
|         end) | ||||
|     end) | ||||
|  | ||||
|     it("sets the computer's label", function() | ||||
|         local setComputerLabel = stub(os, "setComputerLabel") | ||||
|         capture(stub, "label set Test") | ||||
|         capture("label set Test") | ||||
|         expect(setComputerLabel):called_with("Test") | ||||
|     end) | ||||
|  | ||||
|     it("clears the computer's label", function() | ||||
|         local setComputerLabel = stub(os, "setComputerLabel") | ||||
|         capture(stub, "label clear") | ||||
|         capture("label clear") | ||||
|         expect(setComputerLabel):called_with(nil) | ||||
|     end) | ||||
| end) | ||||
|   | ||||
| @@ -3,7 +3,7 @@ local capture = require "test_helpers".capture_program | ||||
| describe("The list program", function() | ||||
|     it("lists files", function() | ||||
|         local pagedTabulate = stub(textutils, "pagedTabulate") | ||||
|         capture(stub, "list /rom") | ||||
|         capture("list /rom") | ||||
|         expect(pagedTabulate):called_with_matching( | ||||
|             colors.green, { "apis", "autorun", "help", "modules", "programs" }, | ||||
|             colors.white, { "motd.txt", "startup.lua" } | ||||
| @@ -11,12 +11,12 @@ describe("The list program", function() | ||||
|     end) | ||||
|  | ||||
|     it("fails on a non-existent directory", function() | ||||
|         expect(capture(stub, "list /rom/nothing")) | ||||
|         expect(capture("list /rom/nothing")) | ||||
|             :matches { ok = true, output = "", error = "Not a directory\n" } | ||||
|     end) | ||||
|  | ||||
|     it("fails on a file", function() | ||||
|         expect(capture(stub, "list /rom/startup.lua")) | ||||
|         expect(capture("list /rom/startup.lua")) | ||||
|             :matches { ok = true, output = "", error = "Not a directory\n" } | ||||
|     end) | ||||
| end) | ||||
|   | ||||
| @@ -2,7 +2,7 @@ local capture = require "test_helpers".capture_program | ||||
|  | ||||
| describe("The monitor program", function() | ||||
|     it("displays its usage when given no arguments", function() | ||||
|         expect(capture(stub, "monitor")) | ||||
|         expect(capture("monitor")) | ||||
|             :matches { | ||||
|                 ok = true, | ||||
|                 output = | ||||
| @@ -17,7 +17,7 @@ describe("The monitor program", function() | ||||
|         local r = 1 | ||||
|         stub(peripheral, "call", function(s, f, t) r = t end) | ||||
|         stub(peripheral, "getType", function() return "monitor" end) | ||||
|         expect(capture(stub, "monitor", "scale", "left", "0.5")) | ||||
|         expect(capture("monitor", "scale", "left", "0.5")) | ||||
|             :matches { ok = true, output = "", error = "" } | ||||
|         expect(r):equals(0.5) | ||||
|     end) | ||||
| @@ -26,7 +26,7 @@ describe("The monitor program", function() | ||||
|         local r = 1 | ||||
|         stub(peripheral, "call", function(s, f, t) r = t end) | ||||
|         stub(peripheral, "getType", function(side) return side == "left" and "monitor" or nil end) | ||||
|         expect(capture(stub, "monitor", "scale", "left")) | ||||
|         expect(capture("monitor", "scale", "left")) | ||||
|             :matches { | ||||
|                 ok = true, | ||||
|                 output = | ||||
| @@ -35,9 +35,9 @@ describe("The monitor program", function() | ||||
|                     "  monitor scale <name> <scale>\n", | ||||
|                 error = "", | ||||
|             } | ||||
|         expect(capture(stub, "monitor", "scale", "top", "0.5")) | ||||
|         expect(capture("monitor", "scale", "top", "0.5")) | ||||
|             :matches { ok = true, output = "No monitor named top\n", error = "" } | ||||
|         expect(capture(stub, "monitor", "scale", "left", "aaa")) | ||||
|         expect(capture("monitor", "scale", "left", "aaa")) | ||||
|             :matches { ok = true, output = "Invalid scale: aaa\n", error = "" } | ||||
|         expect(r):equals(1) | ||||
|     end) | ||||
|   | ||||
| @@ -12,31 +12,31 @@ describe("The motd program", function() | ||||
|         file.close() | ||||
|         settings.set("motd.path", "/motd_check.txt") | ||||
|  | ||||
|         expect(capture(stub, "motd")) | ||||
|         expect(capture("motd")) | ||||
|             :matches { ok = true, output = "Hello World!\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("displays date-specific MOTD (1 Jan)", function() | ||||
|         setup_date(1, 1) | ||||
|         expect(capture(stub, "motd")) | ||||
|         expect(capture("motd")) | ||||
|             :matches { ok = true, output = "Happy new year!\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("displays date-specific MOTD (28 Apr)", function() | ||||
|         setup_date(28, 4) | ||||
|         expect(capture(stub, "motd")) | ||||
|         expect(capture("motd")) | ||||
|             :matches { ok = true, output = "Ed Balls\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("displays date-specific MOTD (31 Oct)", function() | ||||
|         setup_date(31, 10) | ||||
|         expect(capture(stub, "motd")) | ||||
|         expect(capture("motd")) | ||||
|             :matches { ok = true, output = "OOoooOOOoooo! Spooky!\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("displays date-specific MOTD (24 Dec)", function() | ||||
|         setup_date(24, 12) | ||||
|         expect(capture(stub, "motd")) | ||||
|         expect(capture("motd")) | ||||
|             :matches { ok = true, output = "Merry X-mas!\n", error = "" } | ||||
|     end) | ||||
| end) | ||||
|   | ||||
| @@ -21,7 +21,7 @@ describe("The move program", function() | ||||
|         touch("/test-files/move/a.txt") | ||||
|         fs.makeDir("/test-files/move/a") | ||||
|  | ||||
|         expect(capture(stub, "move /test-files/move/a.txt /test-files/move/a")) | ||||
|         expect(capture("move /test-files/move/a.txt /test-files/move/a")) | ||||
|             :matches { ok = true } | ||||
|  | ||||
|         expect(fs.exists("/test-files/move/a.txt")):eq(false) | ||||
| @@ -29,7 +29,7 @@ describe("The move program", function() | ||||
|     end) | ||||
|  | ||||
|     it("fails when moving a file which doesn't exist", function() | ||||
|         expect(capture(stub, "move nothing destination")) | ||||
|         expect(capture("move nothing destination")) | ||||
|             :matches { ok = true, output = "", error = "No matching files\n" } | ||||
|     end) | ||||
|  | ||||
| @@ -37,7 +37,7 @@ describe("The move program", function() | ||||
|         cleanup() | ||||
|         touch("/test-files/move/a.txt") | ||||
|  | ||||
|         expect(capture(stub, "move /test-files/move/a.txt /test-files/move/a.txt")) | ||||
|         expect(capture("move /test-files/move/a.txt /test-files/move/a.txt")) | ||||
|             :matches { ok = true, output = "", error = "Destination exists\n" } | ||||
|     end) | ||||
|  | ||||
| @@ -45,17 +45,17 @@ describe("The move program", function() | ||||
|         cleanup() | ||||
|         touch("/test-files/move/a.txt") | ||||
|  | ||||
|         expect(capture(stub, "move /test-files/move/a.txt /rom/test.txt")) | ||||
|         expect(capture("move /test-files/move/a.txt /rom/test.txt")) | ||||
|             :matches { ok = true, output = "", error = "Destination is read-only\n" } | ||||
|     end) | ||||
|  | ||||
|     it("fails when moving from read-only locations", function() | ||||
|         expect(capture(stub, "move /rom/startup.lua /test-files/move/not-exist.txt")) | ||||
|         expect(capture("move /rom/startup.lua /test-files/move/not-exist.txt")) | ||||
|             :matches { ok = true, output = "", error = "Cannot move read-only file /rom/startup.lua\n" } | ||||
|     end) | ||||
|  | ||||
|     it("fails when moving mounts", function() | ||||
|         expect(capture(stub, "move /rom /test-files/move/rom")) | ||||
|         expect(capture("move /rom /test-files/move/rom")) | ||||
|             :matches { ok = true, output = "", error = "Cannot move mount /rom\n" } | ||||
|     end) | ||||
|  | ||||
| @@ -63,12 +63,12 @@ describe("The move program", function() | ||||
|         cleanup() | ||||
|         touch("/test-files/move/a.txt") | ||||
|         touch("/test-files/move/b.txt") | ||||
|         expect(capture(stub, "move /test-files/move/*.txt /test-files/move/c.txt")) | ||||
|         expect(capture("move /test-files/move/*.txt /test-files/move/c.txt")) | ||||
|             :matches { ok = true, output = "", error = "Cannot overwrite file multiple times\n" } | ||||
|     end) | ||||
|  | ||||
|     it("displays the usage with no arguments", function() | ||||
|         expect(capture(stub, "move")) | ||||
|         expect(capture("move")) | ||||
|             :matches { ok = true, output = "Usage: move <source> <destination>\n", error = "" } | ||||
|     end) | ||||
| end) | ||||
|   | ||||
| @@ -3,7 +3,7 @@ local capture = require "test_helpers".capture_program | ||||
| describe("The peripherals program", function() | ||||
|     it("says when there are no peripherals", function() | ||||
|         stub(peripheral, 'getNames', function() return {} end) | ||||
|         expect(capture(stub, "peripherals")) | ||||
|         expect(capture("peripherals")) | ||||
|             :matches { ok = true, output = "Attached Peripherals:\nNone\n", error = "" } | ||||
|     end) | ||||
| end) | ||||
|   | ||||
| @@ -3,7 +3,7 @@ local capture = require "test_helpers".capture_program | ||||
| describe("The pocket equip program", function() | ||||
|     it("errors when not a pocket computer", function() | ||||
|         stub(_G, "pocket", nil) | ||||
|         expect(capture(stub, "/rom/programs/pocket/equip.lua")) | ||||
|         expect(capture("/rom/programs/pocket/equip.lua")) | ||||
|             :matches { ok = true, output = "", error = "Requires a Pocket Computer\n" } | ||||
|     end) | ||||
|  | ||||
| @@ -12,7 +12,7 @@ describe("The pocket equip program", function() | ||||
|             equipBack = function() return true end, | ||||
|         }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/pocket/equip.lua")) | ||||
|         expect(capture("/rom/programs/pocket/equip.lua")) | ||||
|             :matches { ok = true, output = "Item equipped\n", error = "" } | ||||
|     end) | ||||
|  | ||||
| @@ -21,7 +21,7 @@ describe("The pocket equip program", function() | ||||
|             equipBack = function() return false, "Cannot equip this item." end, | ||||
|         }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/pocket/equip.lua")) | ||||
|         expect(capture("/rom/programs/pocket/equip.lua")) | ||||
|             :matches { ok = true, output = "", error = "Cannot equip this item.\n" } | ||||
|     end) | ||||
| end) | ||||
|   | ||||
| @@ -3,7 +3,7 @@ local capture = require "test_helpers".capture_program | ||||
| describe("The pocket unequip program", function() | ||||
|     it("errors when not a pocket computer", function() | ||||
|         stub(_G, "pocket", nil) | ||||
|         expect(capture(stub, "/rom/programs/pocket/unequip.lua")) | ||||
|         expect(capture("/rom/programs/pocket/unequip.lua")) | ||||
|             :matches { ok = true, output = "", error = "Requires a Pocket Computer\n" } | ||||
|     end) | ||||
|  | ||||
| @@ -12,7 +12,7 @@ describe("The pocket unequip program", function() | ||||
|             unequipBack = function() return true end, | ||||
|         }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/pocket/unequip.lua")) | ||||
|         expect(capture("/rom/programs/pocket/unequip.lua")) | ||||
|             :matches { ok = true, output = "Item unequipped\n", error = "" } | ||||
|     end) | ||||
|  | ||||
| @@ -21,7 +21,7 @@ describe("The pocket unequip program", function() | ||||
|             unequipBack = function() return false, "Nothing to remove." end, | ||||
|         }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/pocket/unequip.lua")) | ||||
|         expect(capture("/rom/programs/pocket/unequip.lua")) | ||||
|             :matches { ok = true, output = "", error = "Nothing to remove.\n" } | ||||
|     end) | ||||
| end) | ||||
|   | ||||
| @@ -5,7 +5,7 @@ describe("The programs program", function() | ||||
|         local programs = stub(shell, "programs", function() return { "some", "programs" } end) | ||||
|         local pagedTabulate = stub(textutils, "pagedTabulate", function(x) print(table.unpack(x)) end) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/programs.lua")) | ||||
|         expect(capture("/rom/programs/programs.lua")) | ||||
|             :matches { ok = true, output = "some programs\n", error = "" } | ||||
|  | ||||
|         expect(programs):called_with(false) | ||||
|   | ||||
| @@ -5,7 +5,7 @@ describe("The reboot program", function() | ||||
|         local sleep = stub(_G, "sleep") | ||||
|         local reboot = stub(os, "reboot") | ||||
|  | ||||
|         expect(capture(stub, "reboot")) | ||||
|         expect(capture("reboot")) | ||||
|             :matches { ok = true, output = "Goodbye\n", error = "" } | ||||
|  | ||||
|         expect(sleep):called_with(1) | ||||
|   | ||||
| @@ -2,7 +2,7 @@ local capture = require "test_helpers".capture_program | ||||
|  | ||||
| describe("The redstone program", function() | ||||
|     it("displays its usage when given no arguments", function() | ||||
|         expect(capture(stub, "redstone")) | ||||
|         expect(capture("redstone")) | ||||
|             :matches { ok = true, output = "Usages:\nredstone probe\nredstone set <side> <value>\nredstone set <side> <color> <value>\nredstone pulse <side> <count> <period>\n", error = "" } | ||||
|     end) | ||||
| end) | ||||
|   | ||||
| @@ -15,36 +15,36 @@ describe("The rename program", function() | ||||
|     end) | ||||
|  | ||||
|     it("fails when renaming a file which doesn't exist", function() | ||||
|         expect(capture(stub, "rename nothing destination")) | ||||
|         expect(capture("rename nothing destination")) | ||||
|             :matches { ok = true, output = "", error = "No matching files\n" } | ||||
|     end) | ||||
|  | ||||
|     it("fails when overwriting an existing file", function() | ||||
|         touch("/test-files/rename/c.txt") | ||||
|  | ||||
|         expect(capture(stub, "rename /test-files/rename/c.txt /test-files/rename/c.txt")) | ||||
|         expect(capture("rename /test-files/rename/c.txt /test-files/rename/c.txt")) | ||||
|             :matches { ok = true, output = "", error = "Destination exists\n" } | ||||
|     end) | ||||
|  | ||||
|     it("fails when renaming to read-only locations", function() | ||||
|         touch("/test-files/rename/d.txt") | ||||
|  | ||||
|         expect(capture(stub, "rename /test-files/rename/d.txt /rom/test.txt")) | ||||
|         expect(capture("rename /test-files/rename/d.txt /rom/test.txt")) | ||||
|             :matches { ok = true, output = "", error = "Destination is read-only\n" } | ||||
|     end) | ||||
|  | ||||
|     it("fails when renaming from read-only locations", function() | ||||
|         expect(capture(stub, "rename /rom/startup.lua /test-files/rename/d.txt")) | ||||
|         expect(capture("rename /rom/startup.lua /test-files/rename/d.txt")) | ||||
|             :matches { ok = true, output = "", error = "Source is read-only\n" } | ||||
|     end) | ||||
|  | ||||
|     it("fails when renaming mounts", function() | ||||
|         expect(capture(stub, "rename /rom /test-files/rename/rom")) | ||||
|         expect(capture("rename /rom /test-files/rename/rom")) | ||||
|             :matches { ok = true, output = "", error = "Can't rename mounts\n" } | ||||
|     end) | ||||
|  | ||||
|     it("displays the usage when given no arguments", function() | ||||
|         expect(capture(stub, "rename")) | ||||
|         expect(capture("rename")) | ||||
|             :matches { ok = true, output = "Usage: rename <source> <destination>\n", error = "" } | ||||
|     end) | ||||
| end) | ||||
|   | ||||
| @@ -13,21 +13,21 @@ describe("The set program", function() | ||||
|     it("displays all settings", function() | ||||
|         setup() | ||||
|  | ||||
|         expect(capture(stub, "set")) | ||||
|         expect(capture("set")) | ||||
|             :matches { ok = true, output = '"test" is "Hello World!"\n"test.defined" is 456\n', error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("displays a single setting", function() | ||||
|         setup() | ||||
|  | ||||
|         expect(capture(stub, "set test")) | ||||
|         expect(capture("set test")) | ||||
|             :matches { ok = true, output = 'test is "Hello World!"\n', error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("displays a single setting with description", function() | ||||
|         setup() | ||||
|  | ||||
|         expect(capture(stub, "set test")) | ||||
|         expect(capture("set test")) | ||||
|             :matches { ok = true, output = 'test is "Hello World!"\n', error = "" } | ||||
|     end) | ||||
|  | ||||
| @@ -35,14 +35,14 @@ describe("The set program", function() | ||||
|         setup() | ||||
|  | ||||
|         settings.set("test.defined", 123) | ||||
|         expect(capture(stub, "set test.defined")) | ||||
|         expect(capture("set test.defined")) | ||||
|             :matches { ok = true, output = 'test.defined is 123 (default is 456)\nA description\n', error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("set a setting", function() | ||||
|         setup() | ||||
|  | ||||
|         expect(capture(stub, "set test Hello")) | ||||
|         expect(capture("set test Hello")) | ||||
|             :matches { ok = true, output = '"test" set to "Hello"\n', error = "" } | ||||
|  | ||||
|         expect(settings.get("test")):eq("Hello") | ||||
| @@ -51,9 +51,9 @@ describe("The set program", function() | ||||
|     it("checks the type of a setting", function() | ||||
|         setup() | ||||
|  | ||||
|         expect(capture(stub, "set test.defined Hello")) | ||||
|         expect(capture("set test.defined Hello")) | ||||
|             :matches { ok = true, output = "", error = '"Hello" is not a valid number.\n' } | ||||
|         expect(capture(stub, "set test.defined 456")) | ||||
|         expect(capture("set test.defined 456")) | ||||
|             :matches { ok = true, output = '"test.defined" set to 456\n', error = "" } | ||||
|     end) | ||||
| end) | ||||
|   | ||||
| @@ -6,7 +6,7 @@ describe("The shutdown program", function() | ||||
|         local sleep = stub(_G, "sleep") | ||||
|         local shutdown = stub(os, "shutdown") | ||||
|  | ||||
|         expect(capture(stub, "shutdown")) | ||||
|         expect(capture("shutdown")) | ||||
|             :matches { ok = true, output = "Goodbye\n", error = "" } | ||||
|  | ||||
|         expect(sleep):called_with(1) | ||||
|   | ||||
| @@ -6,7 +6,7 @@ describe("The time program", function() | ||||
|         local time = textutils.formatTime(os.time()) | ||||
|         local day = os.day() | ||||
|  | ||||
|         expect(capture(stub, "time")) | ||||
|         expect(capture("time")) | ||||
|             :matches { ok = true, output = "The time is " .. time .. " on Day " .. day .. "\n", error = "" } | ||||
|     end) | ||||
| end) | ||||
|   | ||||
| @@ -4,28 +4,28 @@ describe("The craft program", function() | ||||
|     it("errors when not a turtle", function() | ||||
|         stub(_G, "turtle", nil) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/craft.lua")) | ||||
|         expect(capture("/rom/programs/turtle/craft.lua")) | ||||
|             :matches { ok = true, output = "", error = "Requires a Turtle\n" } | ||||
|     end) | ||||
|  | ||||
|     it("fails when turtle.craft() is unavailable", function() | ||||
|         stub(_G, "turtle", {}) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/craft.lua")) | ||||
|         expect(capture("/rom/programs/turtle/craft.lua")) | ||||
|             :matches { ok = true, output = "Requires a Crafty Turtle\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("displays its usage when given no arguments", function() | ||||
|         stub(_G, "turtle", { craft = function() end }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/craft.lua")) | ||||
|         expect(capture("/rom/programs/turtle/craft.lua")) | ||||
|             :matches { ok = true, output = "Usage: /rom/programs/turtle/craft.lua all|<number>\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("displays its usage when given incorrect arguments", function() | ||||
|         stub(_G, "turtle", { craft = function() end }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/craft.lua a")) | ||||
|         expect(capture("/rom/programs/turtle/craft.lua a")) | ||||
|             :matches { ok = true, output = "Usage: /rom/programs/turtle/craft.lua all|<number>\n", error = "" } | ||||
|     end) | ||||
|  | ||||
| @@ -40,7 +40,7 @@ describe("The craft program", function() | ||||
|             getSelectedSlot = function() return 1 end, | ||||
|         }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/craft.lua 2")) | ||||
|         expect(capture("/rom/programs/turtle/craft.lua 2")) | ||||
|             :matches { ok = true, output = "2 items crafted\n", error = "" } | ||||
|     end) | ||||
|  | ||||
| @@ -55,7 +55,7 @@ describe("The craft program", function() | ||||
|             getSelectedSlot = function() return 1 end, | ||||
|         }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/craft.lua 1")) | ||||
|         expect(capture("/rom/programs/turtle/craft.lua 1")) | ||||
|             :matches { ok = true, output = "1 item crafted\n", error = "" } | ||||
|     end) | ||||
|  | ||||
| @@ -70,7 +70,7 @@ describe("The craft program", function() | ||||
|             getSelectedSlot = function() return 1 end, | ||||
|         }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/craft.lua 1")) | ||||
|         expect(capture("/rom/programs/turtle/craft.lua 1")) | ||||
|             :matches { ok = true, output = "No items crafted\n", error = "" } | ||||
|     end) | ||||
|  | ||||
| @@ -83,7 +83,7 @@ describe("The craft program", function() | ||||
|             getSelectedSlot = function() return 1 end, | ||||
|         }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/craft.lua all")) | ||||
|         expect(capture("/rom/programs/turtle/craft.lua all")) | ||||
|             :matches { ok = true, output = "17 items crafted\n", error = "" } | ||||
|     end) | ||||
| end) | ||||
|   | ||||
| @@ -4,7 +4,7 @@ describe("The turtle equip program", function() | ||||
|     it("errors when not a turtle", function() | ||||
|         stub(_G, "turtle", nil) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/equip.lua")) | ||||
|         expect(capture("/rom/programs/turtle/equip.lua")) | ||||
|             :matches { ok = true, output = "", error = "Requires a Turtle\n" } | ||||
|     end) | ||||
|  | ||||
| @@ -12,7 +12,7 @@ describe("The turtle equip program", function() | ||||
|     it("displays its usage when given no arguments", function() | ||||
|         stub(_G, "turtle", {}) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/equip.lua")) | ||||
|         expect(capture("/rom/programs/turtle/equip.lua")) | ||||
|             :matches { ok = true, output = "Usage: /rom/programs/turtle/equip.lua <slot> <side>\n", error = "" } | ||||
|     end) | ||||
|  | ||||
| @@ -22,9 +22,9 @@ describe("The turtle equip program", function() | ||||
|             getItemCount = function() return 0 end, | ||||
|         }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/equip.lua 1 left")) | ||||
|         expect(capture("/rom/programs/turtle/equip.lua 1 left")) | ||||
|             :matches { ok = true, output = "Nothing to equip\n", error = "" } | ||||
|         expect(capture(stub, "/rom/programs/turtle/equip.lua 1 right")) | ||||
|         expect(capture("/rom/programs/turtle/equip.lua 1 right")) | ||||
|             :matches { ok = true, output = "Nothing to equip\n", error = "" } | ||||
|     end) | ||||
|  | ||||
| @@ -36,9 +36,9 @@ describe("The turtle equip program", function() | ||||
|             equipRight = function() return true end, | ||||
|         }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/equip.lua 1 left")) | ||||
|         expect(capture("/rom/programs/turtle/equip.lua 1 left")) | ||||
|             :matches { ok = true, output = "Items swapped\n", error = "" } | ||||
|         expect(capture(stub, "/rom/programs/turtle/equip.lua 1 right")) | ||||
|         expect(capture("/rom/programs/turtle/equip.lua 1 right")) | ||||
|             :matches { ok = true, output = "Items swapped\n", error = "" } | ||||
|     end) | ||||
|  | ||||
| @@ -61,13 +61,13 @@ describe("The turtle equip program", function() | ||||
|  | ||||
|         it("on the left", function() | ||||
|             setup() | ||||
|             expect(capture(stub, "/rom/programs/turtle/equip.lua 1 left")) | ||||
|             expect(capture("/rom/programs/turtle/equip.lua 1 left")) | ||||
|                 :matches { ok = true, output = "Item equipped\n", error = "" } | ||||
|         end) | ||||
|  | ||||
|         it("on the right", function() | ||||
|             setup() | ||||
|             expect(capture(stub, "/rom/programs/turtle/equip.lua 1 right")) | ||||
|             expect(capture("/rom/programs/turtle/equip.lua 1 right")) | ||||
|                 :matches { ok = true, output = "Item equipped\n", error = "" } | ||||
|         end) | ||||
|     end) | ||||
| @@ -80,9 +80,9 @@ describe("The turtle equip program", function() | ||||
|             equipRight = function() return false end, | ||||
|         }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/equip.lua 1 left")) | ||||
|         expect(capture("/rom/programs/turtle/equip.lua 1 left")) | ||||
|             :matches { ok = true, output = "Item not equippable\n", error = "" } | ||||
|         expect(capture(stub, "/rom/programs/turtle/equip.lua 1 right")) | ||||
|         expect(capture("/rom/programs/turtle/equip.lua 1 right")) | ||||
|             :matches { ok = true, output = "Item not equippable\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|   | ||||
| @@ -24,39 +24,39 @@ describe("The refuel program", function() | ||||
|     it("errors when not a turtle", function() | ||||
|         stub(_G, "turtle", nil) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/refuel.lua")) | ||||
|         expect(capture("/rom/programs/turtle/refuel.lua")) | ||||
|             :matches { ok = true, output = "", error = "Requires a Turtle\n" } | ||||
|     end) | ||||
|  | ||||
|  | ||||
|     it("displays its usage when given too many argument", function() | ||||
|         setup_turtle(0, 5, 0) | ||||
|         expect(capture(stub, "/rom/programs/turtle/refuel.lua a b")) | ||||
|         expect(capture("/rom/programs/turtle/refuel.lua a b")) | ||||
|             :matches { ok = true, output = "Usage: /rom/programs/turtle/refuel.lua [number]\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("requires a numeric argument", function() | ||||
|        setup_turtle(0, 0, 0) | ||||
|        expect(capture(stub, "/rom/programs/turtle/refuel.lua nothing")) | ||||
|        expect(capture("/rom/programs/turtle/refuel.lua nothing")) | ||||
|            :matches { ok = true, output = "Invalid limit, expected a number or \"all\"\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("refuels the turtle", function() | ||||
|        setup_turtle(0, 10, 5) | ||||
|  | ||||
|        expect(capture(stub, "/rom/programs/turtle/refuel.lua 5")) | ||||
|        expect(capture("/rom/programs/turtle/refuel.lua 5")) | ||||
|            :matches { ok = true, output = "Fuel level is 5\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("reports when the fuel limit is reached", function() | ||||
|        setup_turtle(0, 5, 5) | ||||
|        expect(capture(stub, "/rom/programs/turtle/refuel.lua 5")) | ||||
|        expect(capture("/rom/programs/turtle/refuel.lua 5")) | ||||
|            :matches { ok = true, output = "Fuel level is 5\nFuel limit reached\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("reports when the fuel level is unlimited", function() | ||||
|        setup_turtle("unlimited", 5, 5) | ||||
|        expect(capture(stub, "/rom/programs/turtle/refuel.lua 5")) | ||||
|        expect(capture("/rom/programs/turtle/refuel.lua 5")) | ||||
|            :matches { ok = true, output = "Fuel level is unlimited\n", error = "" } | ||||
|     end) | ||||
| end) | ||||
|   | ||||
| @@ -4,7 +4,7 @@ describe("The turtle unequip program", function() | ||||
|     it("errors when not a turtle", function() | ||||
|         stub(_G, "turtle", nil) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/unequip.lua")) | ||||
|         expect(capture("/rom/programs/turtle/unequip.lua")) | ||||
|             :matches { ok = true, output = "", error = "Requires a Turtle\n" } | ||||
|     end) | ||||
|  | ||||
| @@ -12,7 +12,7 @@ describe("The turtle unequip program", function() | ||||
|     it("displays its usage when given no arguments", function() | ||||
|         stub(_G, "turtle", {}) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/unequip.lua")) | ||||
|         expect(capture("/rom/programs/turtle/unequip.lua")) | ||||
|             :matches { ok = true, output = "Usage: /rom/programs/turtle/unequip.lua <side>\n", error = "" } | ||||
|     end) | ||||
|  | ||||
| @@ -24,9 +24,9 @@ describe("The turtle unequip program", function() | ||||
|             equipLeft = function() return true end, | ||||
|         }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/unequip.lua left")) | ||||
|         expect(capture("/rom/programs/turtle/unequip.lua left")) | ||||
|             :matches { ok = true, output = "Nothing to unequip\n", error = "" } | ||||
|         expect(capture(stub, "/rom/programs/turtle/unequip.lua right")) | ||||
|         expect(capture("/rom/programs/turtle/unequip.lua right")) | ||||
|             :matches { ok = true, output = "Nothing to unequip\n", error = "" } | ||||
|     end) | ||||
|  | ||||
| @@ -45,10 +45,10 @@ describe("The turtle unequip program", function() | ||||
|             end, | ||||
|         }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/unequip.lua left")) | ||||
|         expect(capture("/rom/programs/turtle/unequip.lua left")) | ||||
|             :matches { ok = true, output = "Item unequipped\n", error = "" } | ||||
|         item_count = 0 | ||||
|         expect(capture(stub, "/rom/programs/turtle/unequip.lua right")) | ||||
|         expect(capture("/rom/programs/turtle/unequip.lua right")) | ||||
|             :matches { ok = true, output = "Item unequipped\n", error = "" } | ||||
|     end) | ||||
|  | ||||
| @@ -60,9 +60,9 @@ describe("The turtle unequip program", function() | ||||
|             equipLeft = function() return true end, | ||||
|         }) | ||||
|  | ||||
|         expect(capture(stub, "/rom/programs/turtle/unequip.lua left")) | ||||
|         expect(capture("/rom/programs/turtle/unequip.lua left")) | ||||
|             :matches { ok = true, output = "No space to unequip item\n", error = "" } | ||||
|         expect(capture(stub, "/rom/programs/turtle/unequip.lua right")) | ||||
|         expect(capture("/rom/programs/turtle/unequip.lua right")) | ||||
|             :matches { ok = true, output = "No space to unequip item\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|   | ||||
| @@ -3,22 +3,22 @@ local capture = require "test_helpers".capture_program | ||||
| describe("The type program", function() | ||||
|  | ||||
|     it("displays the usage with no arguments", function() | ||||
|         expect(capture(stub, "type")) | ||||
|         expect(capture("type")) | ||||
|             :matches { ok = true, output = "Usage: type <path>\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("displays the output for a file", function() | ||||
|         expect(capture(stub, "type /rom/startup.lua")) | ||||
|         expect(capture("type /rom/startup.lua")) | ||||
|             :matches { ok = true, output = "file\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("displays the output for a directory", function() | ||||
|         expect(capture(stub, "type /rom")) | ||||
|         expect(capture("type /rom")) | ||||
|             :matches { ok = true, output = "directory\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|     it("displays the output for a not existing path", function() | ||||
|         expect(capture(stub, "type /rom/nothing")) | ||||
|         expect(capture("type /rom/nothing")) | ||||
|             :matches { ok = true, output = "No such path\n", error = "" } | ||||
|     end) | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
| -- @tparam string ... Arguments to this program. | ||||
| -- @treturn { ok = boolean, output = string, error = string, combined = string } | ||||
| -- Whether this program terminated successfully, and the various output streams. | ||||
| local function capture_program(stub, program, ...) | ||||
| local function capture_program(program, ...) | ||||
|     local output, error, combined = {}, {}, {} | ||||
|  | ||||
|     local function out(stream, msg) | ||||
| @@ -70,8 +70,68 @@ local function with_window_lines(width, height, fn) | ||||
|     return out | ||||
| end | ||||
|  | ||||
| local function timeout(time, fn) | ||||
|     local timer = os.startTimer(time) | ||||
|     local co = coroutine.create(fn) | ||||
|     local ok, result, event = true, nil, { n = 0 } | ||||
|     while coroutine.status(co) ~= "dead" do | ||||
|         if event[1] == "timer" and event[2] == timer then error("Timeout", 2) end | ||||
|  | ||||
|         if result == nil or event[1] == result or event[1] == "terminated" then | ||||
|             ok, result = coroutine.resume(co, table.unpack(event, 1, event.n)) | ||||
|             if not ok then error(result, 0) end | ||||
|         end | ||||
|  | ||||
|         event = table.pack(coroutine.yield()) | ||||
|     end | ||||
| end | ||||
|  | ||||
| --- Extract a series of tests from a markdown file. | ||||
| local function describe_golden(name, file, generate) | ||||
|     describe(name, function() | ||||
|         local handle = assert(fs.open(file, "r")) | ||||
|         local contents = "\n" .. handle.readAll() | ||||
|         handle.close() | ||||
|  | ||||
|         local pos = 1 | ||||
|         local function run(current_level) | ||||
|             local test_idx = 1 | ||||
|             while true do | ||||
|                 local lua_start, lua_end, extra, lua = contents:find("```lua *([^\n]*)\n(.-)\n```\n?", pos) | ||||
|                 local heading_start, heading_end, heading_lvl, heading = contents:find("\n(#+) *([^\n]+)", pos) | ||||
|  | ||||
|                 if heading and (not lua_start or heading_start < lua_start) then | ||||
|                     if #heading_lvl <= current_level then | ||||
|                         return | ||||
|                     end | ||||
|  | ||||
|                     pos = heading_end + 1 | ||||
|                     describe(heading, function() run(#heading_lvl) end) | ||||
|                 elseif lua_end then | ||||
|                     local _, txt_end, txt = contents:find("^\n*```txt\n(.-)\n```\n?", lua_end + 1) | ||||
|  | ||||
|                     it("test #" .. test_idx, function() | ||||
|                         expect(generate(lua, extra)) | ||||
|                             :describe("For input string <<<\n" .. lua .. "\n>>>") | ||||
|                             :eq(txt) | ||||
|                     end) | ||||
|                     test_idx = test_idx + 1 | ||||
|  | ||||
|                     pos = (txt_end or lua_end) + 1 | ||||
|                 else | ||||
|                     return | ||||
|                 end | ||||
|             end | ||||
|         end | ||||
|  | ||||
|         run(0) | ||||
|     end) | ||||
| end | ||||
|  | ||||
| return { | ||||
|     capture_program = capture_program, | ||||
|     with_window = with_window, | ||||
|     with_window_lines = with_window_lines, | ||||
|     timeout = timeout, | ||||
|     describe_golden = describe_golden, | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Jonathan Coates
					Jonathan Coates