From e82af91d0dc57bd8138dc5881a9b342e2a97456e Mon Sep 17 00:00:00 2001 From: Hisham Muhammad Date: Sun, 5 Nov 2023 23:07:03 -0300 Subject: [PATCH] standard library: support "n"/"*n" in file reader functions A pragmatic implementation which describes it as a polymorphic function which covers the most common cases first, avoiding the need for casts. Yes, this is a form of poor-man's dependent typing and abusing enums as poor-man's literal types. No, I don't want to add fixed-arity cases for all combinations of 2, 3, 4... arguments -- 1-arity and the variadic fallback should be enough. Closes #718. --- spec/stdlib/io_spec.lua | 152 +++++++++++++++++++++++++++++++++++----- tl.lua | 89 ++++++++++++++++------- tl.tl | 89 ++++++++++++++++------- 3 files changed, 264 insertions(+), 66 deletions(-) diff --git a/spec/stdlib/io_spec.lua b/spec/stdlib/io_spec.lua index d8d2bebd3..fffc39d0f 100644 --- a/spec/stdlib/io_spec.lua +++ b/spec/stdlib/io_spec.lua @@ -2,6 +2,47 @@ local util = require("spec.util") describe("io", function() + describe("read", function() + it("with no arguments", util.check([[ + local l = io.read() + print(l:upper()) + ]])) + + it("with a bytes format argument", util.check([[ + local l = io.read(100) + print(l:upper()) + ]])) + + it("with a string format argument", util.check([[ + local l = io.read("*a") + print(l:upper()) + ]])) + + it("with a numeric format", util.check([[ + local n = io.read("n") + print(n * 2) + local m = io.read("*n") + print(n + m) + ]])) + + it("with multiple formats", util.check([[ + local a, b, c = io.read("l", 12, 13) + print(a:upper()) + print(b:upper()) + print(c:upper()) + ]])) + + it("resolves the type of mixed numeric/string formats as unions for now", util.check([[ + local a, b = io.read("n", 12, 13) + if a is number then + print(a * 2) + end + if b is string then + print(b:upper()) + end + ]])) + end) + describe("lines", function() it("with no arguments", util.check([[ for l in io.lines() do @@ -15,23 +56,43 @@ describe("io", function() end ]])) - it("with a format argument", util.check([[ + it("with a bytes format argument", util.check([[ for c in io.lines("filename.txt", 1) do print(c:upper()) end ]])) - it("with multiple formats", util.check([[ + it("with a string format argument", util.check([[ + for c in io.lines("filename.txt", "*l") do + print(c:upper()) + end + ]])) + + it("with multiple string formats", util.check([[ for a, b in io.lines("filename.txt", "l", 12) do print(a:upper()) print(b:upper()) end ]])) - pending("resolves the type of numeric formats", util.check([[ - for a, b in io.lines("filename.txt", "n", 12) do - print(n * 2) - print(b:upper()) + it("with a numeric format", util.check([[ + for a in io.lines("n") do + print(a * 2) + end + + for a in io.lines("*n") do + print(a * 2) + end + ]])) + + it("resolves the type of mixed numeric/string formats as unions for now", util.check([[ + for a, b in io.lines("n", 12) do + if a is number then + print(a * 2) + end + if b is string then + print(b:upper()) + end end ]])) end) @@ -47,7 +108,7 @@ describe("io", function() describe("read", function() it("accepts a union (#317)", util.check([[ - local function loadFile(textFile: string, amount: string | number): string, FILE + local function loadFile(textFile: string, amount: string | integer): string, FILE local file = io.open(textFile, "r") if not file then error("ftcsv: File not found at " .. textFile) end local lines: string @@ -58,6 +119,51 @@ describe("io", function() return lines, file end ]])) + + it("with no arguments", util.check([[ + local file = io.open("filename.txt") + local l = file:read() + print(l:upper()) + ]])) + + it("with a bytes format argument", util.check([[ + local file = io.open("filename.txt") + local l = file:read(100) + print(l:upper()) + ]])) + + it("with a string format argument", util.check([[ + local file = io.open("filename.txt") + local l = file:read("*a") + print(l:upper()) + ]])) + + it("with a numeric format", util.check([[ + local file = io.open("filename.txt") + local n = file:read("n") + print(n * 2) + local m = file:read("*n") + print(n + m) + ]])) + + it("with multiple formats", util.check([[ + local file = io.open("filename.txt") + local a, b, c = file:read("l", 12, 13) + print(a:upper()) + print(b:upper()) + print(c:upper()) + ]])) + + it("resolves the type of mixed numeric/string formats as unions for now", util.check([[ + local file = io.open("filename.txt") + local a, b = file:read("n", 12, 13) + if a is number then + print(a * 2) + end + if b is string then + print(b:upper()) + end + ]])) end) describe("lines", function() @@ -67,18 +173,28 @@ describe("io", function() end ]])) - it("with a filename argument", util.check([[ - for l in io.popen("ls"):lines("filename.txt") do - print(l:upper()) + it("with a bytes format argument", util.check([[ + for c in io.popen("ls"):lines("filename.txt", 1) do + print(c:upper()) end ]])) - it("with a format argument", util.check([[ - for c in io.popen("ls"):lines("filename.txt", 1) do + it("with a string format argument", util.check([[ + for c in io.popen("ls"):lines("*l") do print(c:upper()) end ]])) + it("with a numeric format", util.check([[ + for a in io.popen("ls"):lines("n") do + print(a * 2) + end + + for a in io.popen("ls"):lines("*n") do + print(a * 2) + end + ]])) + it("with multiple formats", util.check([[ for a, b, c in io.popen("ls"):lines("filename.txt", "l", 12, 13) do print(a:upper()) @@ -87,10 +203,14 @@ describe("io", function() end ]])) - pending("resolves the type of numeric formats", util.check([[ - for a, b in io.popen("ls"):lines("filename.txt", "n", 12) do - print(n * 2) - print(b:upper()) + it("resolves the type of mixed numeric/string formats as unions for now", util.check([[ + for a, b in io.popen("ls"):lines("n", 12) do + if a is number then + print(a * 2) + end + if b is string then + print(b:upper()) + end end ]])) end) diff --git a/tl.lua b/tl.lua index 747e8e137..15f45a07f 100644 --- a/tl.lua +++ b/tl.lua @@ -5068,6 +5068,45 @@ local function init_globals(lax) return t end + local function an_enum(keys) + local t = a_type({ + typename = "enum", + enumset = {}, + }) + for _, k in ipairs(keys) do + t.enumset[k] = true + end + return t + end + + + + + + + + + + local file_reader_poly_types = { + { ctor = VARARG, args = { UNION({ NUMBER, an_enum({ "*a", "a", "*l", "l", "*L", "L" }) }) }, rets = { STRING } }, + { ctor = TUPLE, args = { an_enum({ "*n", "n" }) }, rets = { NUMBER, STRING } }, + { ctor = VARARG, args = { UNION({ NUMBER, an_enum({ "*a", "a", "*l", "l", "*L", "L", "*n", "n" }) }) }, rets = { UNION({ STRING, NUMBER }) } }, + { ctor = VARARG, args = { UNION({ NUMBER, STRING }) }, rets = { STRING } }, + } + + local function a_file_reader(fn) + local t = a_type({ + typename = "poly", + types = {}, + }) + for _, entry in ipairs(file_reader_poly_types) do + local args = shallow_copy_type(entry.args) + local rets = shallow_copy_type(entry.rets) + table.insert(t.types, fn(entry.ctor, args, rets)) + end + return t + end + local LOAD_FUNCTION = a_type({ typename = "function", args = {}, rets = TUPLE({ STRING }) }) local OS_DATE_TABLE = a_type({ @@ -5085,8 +5124,6 @@ local function init_globals(lax) }, }) - local OS_DATE_TABLE_FORMAT = a_type({ typename = "enum", enumset = { ["!*t"] = true, ["*t"] = true } }) - local DEBUG_GETINFO_TABLE = a_type({ typename = "record", fields = { @@ -5107,16 +5144,8 @@ local function init_globals(lax) }, }) - local DEBUG_HOOK_EVENT = a_type({ - typename = "enum", - enumset = { - ["call"] = true, - ["tail call"] = true, - ["return"] = true, - ["line"] = true, - ["count"] = true, - }, - }) + local DEBUG_HOOK_EVENT = an_enum({ "call", "tail call", "return", "line", "count" }) + local DEBUG_HOOK_FUNCTION = a_type({ typename = "function", args = TUPLE({ DEBUG_HOOK_EVENT, INTEGER }), @@ -5161,9 +5190,9 @@ local function init_globals(lax) ["collectgarbage"] = a_type({ typename = "poly", types = { - a_type({ typename = "function", args = TUPLE({ a_type({ typename = "enum", enumset = { ["collect"] = true, ["count"] = true, ["stop"] = true, ["restart"] = true } }) }), rets = TUPLE({ NUMBER }) }), - a_type({ typename = "function", args = TUPLE({ a_type({ typename = "enum", enumset = { ["step"] = true, ["setpause"] = true, ["setstepmul"] = true } }), NUMBER }), rets = TUPLE({ NUMBER }) }), - a_type({ typename = "function", args = TUPLE({ a_type({ typename = "enum", enumset = { ["isrunning"] = true } }) }), rets = TUPLE({ BOOLEAN }) }), + a_type({ typename = "function", args = TUPLE({ an_enum({ "collect", "count", "stop", "restart" }) }), rets = TUPLE({ NUMBER }) }), + a_type({ typename = "function", args = TUPLE({ an_enum({ "step", "setpause", "setstepmul" }), NUMBER }), rets = TUPLE({ NUMBER }) }), + a_type({ typename = "function", args = TUPLE({ an_enum({ "isrunning" }) }), rets = TUPLE({ BOOLEAN }) }), a_type({ typename = "function", args = TUPLE({ STRING, OPT(NUMBER) }), rets = TUPLE({ a_type({ typename = "union", types = { BOOLEAN, NUMBER } }) }) }), }, }), @@ -5226,10 +5255,16 @@ local function init_globals(lax) fields = { ["close"] = a_type({ typename = "function", args = TUPLE({ NOMINAL_FILE }), rets = TUPLE({ BOOLEAN, STRING, INTEGER }) }), ["flush"] = a_type({ typename = "function", args = TUPLE({ NOMINAL_FILE }), rets = TUPLE({}) }), - ["lines"] = a_type({ typename = "function", args = VARARG({ NOMINAL_FILE, a_type({ typename = "union", types = { STRING, NUMBER } }) }), rets = TUPLE({ - a_type({ typename = "function", args = TUPLE({}), rets = VARARG({ STRING }) }), - }), }), - ["read"] = a_type({ typename = "function", args = TUPLE({ NOMINAL_FILE, UNION({ STRING, NUMBER }) }), rets = TUPLE({ STRING, STRING }) }), + ["lines"] = a_file_reader(function(ctor, args, rets) + table.insert(args, 1, NOMINAL_FILE) + return a_type({ typename = "function", args = ctor(args), rets = TUPLE({ + a_type({ typename = "function", args = TUPLE({}), rets = ctor(rets) }), + }), }) + end), + ["read"] = a_file_reader(function(ctor, args, rets) + table.insert(args, 1, NOMINAL_FILE) + return a_type({ typename = "function", args = ctor(args), rets = ctor(rets) }) + end), ["seek"] = a_type({ typename = "function", args = TUPLE({ NOMINAL_FILE, OPT(STRING), OPT(NUMBER) }), rets = TUPLE({ INTEGER, STRING }) }), ["setvbuf"] = a_type({ typename = "function", args = TUPLE({ NOMINAL_FILE, STRING, OPT(NUMBER) }), rets = TUPLE({}) }), ["write"] = a_type({ typename = "function", args = VARARG({ NOMINAL_FILE, UNION({ STRING, NUMBER }) }), rets = TUPLE({ NOMINAL_FILE, STRING }) }), @@ -5247,7 +5282,7 @@ local function init_globals(lax) ["__gc"] = a_type({ typename = "function", args = TUPLE({ a }), rets = TUPLE({}) }), ["__index"] = ANY, ["__len"] = a_type({ typename = "function", args = TUPLE({ a }), rets = TUPLE({ ANY }) }), - ["__mode"] = a_type({ typename = "enum", enumset = { ["k"] = true, ["v"] = true, ["kv"] = true } }), + ["__mode"] = an_enum({ "k", "v", "kv" }), ["__newindex"] = ANY, ["__pairs"] = a_gfunction(2, function(k, v) return { @@ -5365,13 +5400,17 @@ local function init_globals(lax) ["close"] = a_type({ typename = "function", args = TUPLE({ OPT(NOMINAL_FILE) }), rets = TUPLE({ BOOLEAN, STRING }) }), ["flush"] = a_type({ typename = "function", args = TUPLE({}), rets = TUPLE({}) }), ["input"] = a_type({ typename = "function", args = TUPLE({ OPT(UNION({ STRING, NOMINAL_FILE })) }), rets = TUPLE({ NOMINAL_FILE }) }), - ["lines"] = a_type({ typename = "function", args = VARARG({ OPT(STRING), a_type({ typename = "union", types = { STRING, NUMBER } }) }), rets = TUPLE({ - a_type({ typename = "function", args = TUPLE({}), rets = VARARG({ STRING }) }), - }), }), + ["lines"] = a_file_reader(function(ctor, args, rets) + return a_type({ typename = "function", args = ctor(args), rets = TUPLE({ + a_type({ typename = "function", args = TUPLE({}), rets = ctor(rets) }), + }), }) + end), ["open"] = a_type({ typename = "function", args = TUPLE({ STRING, STRING }), rets = TUPLE({ NOMINAL_FILE, STRING }) }), ["output"] = a_type({ typename = "function", args = TUPLE({ OPT(UNION({ STRING, NOMINAL_FILE })) }), rets = TUPLE({ NOMINAL_FILE }) }), ["popen"] = a_type({ typename = "function", args = TUPLE({ STRING, STRING }), rets = TUPLE({ NOMINAL_FILE, STRING }) }), - ["read"] = a_type({ typename = "function", args = TUPLE({ UNION({ STRING, NUMBER }) }), rets = TUPLE({ STRING, STRING }) }), + ["read"] = a_file_reader(function(ctor, args, rets) + return a_type({ typename = "function", args = ctor(args), rets = ctor(rets) }) + end), ["stderr"] = NOMINAL_FILE, ["stdin"] = NOMINAL_FILE, ["stdout"] = NOMINAL_FILE, @@ -5462,7 +5501,7 @@ local function init_globals(lax) typename = "poly", types = { a_type({ typename = "function", args = TUPLE({}), rets = TUPLE({ STRING }) }), - a_type({ typename = "function", args = TUPLE({ OS_DATE_TABLE_FORMAT, NUMBER }), rets = TUPLE({ OS_DATE_TABLE }) }), + a_type({ typename = "function", args = TUPLE({ an_enum({ "!*t", "*t" }), NUMBER }), rets = TUPLE({ OS_DATE_TABLE }) }), a_type({ typename = "function", args = TUPLE({ OPT(STRING), OPT(NUMBER) }), rets = TUPLE({ STRING }) }), }, }), diff --git a/tl.tl b/tl.tl index 730712d24..7a8414a4f 100644 --- a/tl.tl +++ b/tl.tl @@ -5068,6 +5068,45 @@ local function init_globals(lax: boolean): {string:Variable}, {string:Type} return t end + local function an_enum(keys: {string}): Type + local t = a_type { + typename = "enum", + enumset = {} + } + for _, k in ipairs(keys) do + t.enumset[k] = true + end + return t + end + + local type TypeConstructor = function({Type}):Type + + local record ArgsRets + ctor: TypeConstructor + args: {Type} + rets: {Type} + end + + local file_reader_poly_types: {ArgsRets} = { + { ctor = VARARG, args = { UNION { NUMBER, an_enum { "*a", "a", "*l", "l", "*L", "L" } } }, rets = { STRING } }, + { ctor = TUPLE, args = { an_enum { "*n", "n" } }, rets = { NUMBER, STRING } }, + { ctor = VARARG, args = { UNION { NUMBER, an_enum { "*a", "a", "*l", "l", "*L", "L", "*n", "n" } } }, rets = { UNION { STRING, NUMBER } } }, + { ctor = VARARG, args = { UNION { NUMBER, STRING } }, rets = { STRING } }, + } + + local function a_file_reader(fn: (function(ctor: TypeConstructor, args: {Type}, rets: {Type}): Type)): Type + local t = a_type { + typename = "poly", + types = {} + } + for _, entry in ipairs(file_reader_poly_types) do + local args = shallow_copy_type(entry.args as Type) as {Type} + local rets = shallow_copy_type(entry.rets as Type) as {Type} + table.insert(t.types, fn(entry.ctor, args, rets)) + end + return t + end + local LOAD_FUNCTION = a_type { typename = "function", args = {}, rets = TUPLE { STRING } } local OS_DATE_TABLE = a_type { @@ -5085,8 +5124,6 @@ local function init_globals(lax: boolean): {string:Variable}, {string:Type} } } - local OS_DATE_TABLE_FORMAT = a_type { typename = "enum", enumset = { ["!*t"] = true, ["*t"] = true } } - local DEBUG_GETINFO_TABLE = a_type { typename = "record", fields = { @@ -5107,16 +5144,8 @@ local function init_globals(lax: boolean): {string:Variable}, {string:Type} } } - local DEBUG_HOOK_EVENT = a_type { - typename = "enum", - enumset = { - ["call"] = true, - ["tail call"] = true, - ["return"] = true, - ["line"] = true, - ["count"] = true, - }, - } + local DEBUG_HOOK_EVENT = an_enum { "call", "tail call", "return", "line", "count" } + local DEBUG_HOOK_FUNCTION = a_type { typename = "function", args = TUPLE { DEBUG_HOOK_EVENT, INTEGER }, @@ -5161,9 +5190,9 @@ local function init_globals(lax: boolean): {string:Variable}, {string:Type} ["collectgarbage"] = a_type { typename = "poly", types = { - a_type { typename = "function", args = TUPLE { a_type { typename = "enum", enumset = { ["collect"] = true, ["count"] = true, ["stop"] = true, ["restart"] = true, } } }, rets = TUPLE { NUMBER } }, - a_type { typename = "function", args = TUPLE { a_type { typename = "enum", enumset = { ["step"] = true, ["setpause"] = true, ["setstepmul"] = true } }, NUMBER }, rets = TUPLE { NUMBER } }, - a_type { typename = "function", args = TUPLE { a_type { typename = "enum", enumset = { ["isrunning"] = true } } }, rets = TUPLE { BOOLEAN } }, + a_type { typename = "function", args = TUPLE { an_enum { "collect", "count", "stop", "restart" } }, rets = TUPLE { NUMBER } }, + a_type { typename = "function", args = TUPLE { an_enum { "step", "setpause", "setstepmul" }, NUMBER }, rets = TUPLE { NUMBER } }, + a_type { typename = "function", args = TUPLE { an_enum { "isrunning" } }, rets = TUPLE { BOOLEAN } }, a_type { typename = "function", args = TUPLE { STRING, OPT(NUMBER) }, rets = TUPLE { a_type { typename = "union", types = { BOOLEAN, NUMBER } } } }, } }, @@ -5226,10 +5255,16 @@ local function init_globals(lax: boolean): {string:Variable}, {string:Type} fields = { ["close"] = a_type { typename = "function", args = TUPLE { NOMINAL_FILE }, rets = TUPLE { BOOLEAN, STRING, INTEGER } }, ["flush"] = a_type { typename = "function", args = TUPLE { NOMINAL_FILE }, rets = TUPLE {} }, - ["lines"] = a_type { typename = "function", args = VARARG { NOMINAL_FILE, a_type { typename = "union", types = { STRING, NUMBER } } }, rets = TUPLE { - a_type { typename = "function", args = TUPLE {}, rets = VARARG { STRING } }, - } }, - ["read"] = a_type { typename = "function", args = TUPLE { NOMINAL_FILE, UNION { STRING, NUMBER } }, rets = TUPLE { STRING, STRING } }, + ["lines"] = a_file_reader(function(ctor: (function({Type}):Type), args: {Type}, rets: {Type}): Type + table.insert(args, 1, NOMINAL_FILE) + return a_type { typename = "function", args = ctor(args), rets = TUPLE { + a_type { typename = "function", args = TUPLE {}, rets = ctor(rets) }, + } } + end), + ["read"] = a_file_reader(function(ctor: (function({Type}):Type), args: {Type}, rets: {Type}): Type + table.insert(args, 1, NOMINAL_FILE) + return a_type { typename = "function", args = ctor(args), rets = ctor(rets) } + end), ["seek"] = a_type { typename = "function", args = TUPLE { NOMINAL_FILE, OPT(STRING), OPT(NUMBER) }, rets = TUPLE { INTEGER, STRING } }, ["setvbuf"] = a_type { typename = "function", args = TUPLE { NOMINAL_FILE, STRING, OPT(NUMBER) }, rets = TUPLE {} }, ["write"] = a_type { typename = "function", args = VARARG { NOMINAL_FILE, UNION { STRING, NUMBER } }, rets = TUPLE { NOMINAL_FILE, STRING } }, @@ -5247,7 +5282,7 @@ local function init_globals(lax: boolean): {string:Variable}, {string:Type} ["__gc"] = a_type { typename = "function", args = TUPLE { a }, rets = TUPLE {} }, ["__index"] = ANY, -- FIXME: function | table | anything with an __index metamethod ["__len"] = a_type { typename = "function", args = TUPLE { a }, rets = TUPLE { ANY } }, - ["__mode"] = a_type { typename = "enum", enumset = { ["k"] = true, ["v"] = true, ["kv"] = true, } }, + ["__mode"] = an_enum { "k", "v", "kv" }, ["__newindex"] = ANY, -- FIXME: function | table | anything with a __newindex metamethod ["__pairs"] = a_gfunction(2, function(k: Type, v: Type): Type return { @@ -5365,13 +5400,17 @@ local function init_globals(lax: boolean): {string:Variable}, {string:Type} ["close"] = a_type { typename = "function", args = TUPLE { OPT(NOMINAL_FILE) }, rets = TUPLE { BOOLEAN, STRING } }, ["flush"] = a_type { typename = "function", args = TUPLE {}, rets = TUPLE {} }, ["input"] = a_type { typename = "function", args = TUPLE { OPT(UNION { STRING, NOMINAL_FILE }) }, rets = TUPLE { NOMINAL_FILE } }, - ["lines"] = a_type { typename = "function", args = VARARG { OPT(STRING), a_type { typename = "union", types = { STRING, NUMBER } } }, rets = TUPLE { - a_type { typename = "function", args = TUPLE {}, rets = VARARG { STRING } }, - } }, + ["lines"] = a_file_reader(function(ctor: TypeConstructor, args: {Type}, rets: {Type}): Type + return a_type { typename = "function", args = ctor(args), rets = TUPLE { + a_type { typename = "function", args = TUPLE {}, rets = ctor(rets) }, + } } + end), ["open"] = a_type { typename = "function", args = TUPLE { STRING, STRING }, rets = TUPLE { NOMINAL_FILE, STRING } }, ["output"] = a_type { typename = "function", args = TUPLE { OPT(UNION { STRING, NOMINAL_FILE }) }, rets = TUPLE { NOMINAL_FILE } }, ["popen"] = a_type { typename = "function", args = TUPLE { STRING, STRING }, rets = TUPLE { NOMINAL_FILE, STRING } }, - ["read"] = a_type { typename = "function", args = TUPLE { UNION { STRING, NUMBER } }, rets = TUPLE { STRING, STRING } }, + ["read"] = a_file_reader(function(ctor: TypeConstructor, args: {Type}, rets: {Type}): Type + return a_type { typename = "function", args = ctor(args), rets = ctor(rets) } + end), ["stderr"] = NOMINAL_FILE, ["stdin"] = NOMINAL_FILE, ["stdout"] = NOMINAL_FILE, @@ -5462,7 +5501,7 @@ local function init_globals(lax: boolean): {string:Variable}, {string:Type} typename = "poly", types = { a_type { typename = "function", args = TUPLE {}, rets = TUPLE { STRING } }, - a_type { typename = "function", args = TUPLE { OS_DATE_TABLE_FORMAT, NUMBER }, rets = TUPLE { OS_DATE_TABLE } }, + a_type { typename = "function", args = TUPLE { an_enum { "!*t", "*t" }, NUMBER }, rets = TUPLE { OS_DATE_TABLE } }, a_type { typename = "function", args = TUPLE { OPT(STRING), OPT(NUMBER) }, rets = TUPLE { STRING } }, } },