Skip to content

Commit

Permalink
standard library: support "n"/"*n" in file reader functions
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
hishamhm committed Nov 6, 2023
1 parent 266e712 commit e82af91
Show file tree
Hide file tree
Showing 3 changed files with 264 additions and 66 deletions.
152 changes: 136 additions & 16 deletions spec/stdlib/io_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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()
Expand All @@ -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())
Expand All @@ -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)
Expand Down
89 changes: 64 additions & 25 deletions tl.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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 = {
Expand All @@ -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 }),
Expand Down Expand Up @@ -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 } }) }) }),
},
}),
Expand Down Expand Up @@ -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 }) }),
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 }) }),
},
}),
Expand Down
Loading

0 comments on commit e82af91

Please sign in to comment.