Skip to content

Commit e82af91

Browse files
committed
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.
1 parent 266e712 commit e82af91

File tree

3 files changed

+264
-66
lines changed

3 files changed

+264
-66
lines changed

spec/stdlib/io_spec.lua

Lines changed: 136 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,47 @@ local util = require("spec.util")
22

33
describe("io", function()
44

5+
describe("read", function()
6+
it("with no arguments", util.check([[
7+
local l = io.read()
8+
print(l:upper())
9+
]]))
10+
11+
it("with a bytes format argument", util.check([[
12+
local l = io.read(100)
13+
print(l:upper())
14+
]]))
15+
16+
it("with a string format argument", util.check([[
17+
local l = io.read("*a")
18+
print(l:upper())
19+
]]))
20+
21+
it("with a numeric format", util.check([[
22+
local n = io.read("n")
23+
print(n * 2)
24+
local m = io.read("*n")
25+
print(n + m)
26+
]]))
27+
28+
it("with multiple formats", util.check([[
29+
local a, b, c = io.read("l", 12, 13)
30+
print(a:upper())
31+
print(b:upper())
32+
print(c:upper())
33+
]]))
34+
35+
it("resolves the type of mixed numeric/string formats as unions for now", util.check([[
36+
local a, b = io.read("n", 12, 13)
37+
if a is number then
38+
print(a * 2)
39+
end
40+
if b is string then
41+
print(b:upper())
42+
end
43+
]]))
44+
end)
45+
546
describe("lines", function()
647
it("with no arguments", util.check([[
748
for l in io.lines() do
@@ -15,23 +56,43 @@ describe("io", function()
1556
end
1657
]]))
1758

18-
it("with a format argument", util.check([[
59+
it("with a bytes format argument", util.check([[
1960
for c in io.lines("filename.txt", 1) do
2061
print(c:upper())
2162
end
2263
]]))
2364

24-
it("with multiple formats", util.check([[
65+
it("with a string format argument", util.check([[
66+
for c in io.lines("filename.txt", "*l") do
67+
print(c:upper())
68+
end
69+
]]))
70+
71+
it("with multiple string formats", util.check([[
2572
for a, b in io.lines("filename.txt", "l", 12) do
2673
print(a:upper())
2774
print(b:upper())
2875
end
2976
]]))
3077

31-
pending("resolves the type of numeric formats", util.check([[
32-
for a, b in io.lines("filename.txt", "n", 12) do
33-
print(n * 2)
34-
print(b:upper())
78+
it("with a numeric format", util.check([[
79+
for a in io.lines("n") do
80+
print(a * 2)
81+
end
82+
83+
for a in io.lines("*n") do
84+
print(a * 2)
85+
end
86+
]]))
87+
88+
it("resolves the type of mixed numeric/string formats as unions for now", util.check([[
89+
for a, b in io.lines("n", 12) do
90+
if a is number then
91+
print(a * 2)
92+
end
93+
if b is string then
94+
print(b:upper())
95+
end
3596
end
3697
]]))
3798
end)
@@ -47,7 +108,7 @@ describe("io", function()
47108

48109
describe("read", function()
49110
it("accepts a union (#317)", util.check([[
50-
local function loadFile(textFile: string, amount: string | number): string, FILE
111+
local function loadFile(textFile: string, amount: string | integer): string, FILE
51112
local file = io.open(textFile, "r")
52113
if not file then error("ftcsv: File not found at " .. textFile) end
53114
local lines: string
@@ -58,6 +119,51 @@ describe("io", function()
58119
return lines, file
59120
end
60121
]]))
122+
123+
it("with no arguments", util.check([[
124+
local file = io.open("filename.txt")
125+
local l = file:read()
126+
print(l:upper())
127+
]]))
128+
129+
it("with a bytes format argument", util.check([[
130+
local file = io.open("filename.txt")
131+
local l = file:read(100)
132+
print(l:upper())
133+
]]))
134+
135+
it("with a string format argument", util.check([[
136+
local file = io.open("filename.txt")
137+
local l = file:read("*a")
138+
print(l:upper())
139+
]]))
140+
141+
it("with a numeric format", util.check([[
142+
local file = io.open("filename.txt")
143+
local n = file:read("n")
144+
print(n * 2)
145+
local m = file:read("*n")
146+
print(n + m)
147+
]]))
148+
149+
it("with multiple formats", util.check([[
150+
local file = io.open("filename.txt")
151+
local a, b, c = file:read("l", 12, 13)
152+
print(a:upper())
153+
print(b:upper())
154+
print(c:upper())
155+
]]))
156+
157+
it("resolves the type of mixed numeric/string formats as unions for now", util.check([[
158+
local file = io.open("filename.txt")
159+
local a, b = file:read("n", 12, 13)
160+
if a is number then
161+
print(a * 2)
162+
end
163+
if b is string then
164+
print(b:upper())
165+
end
166+
]]))
61167
end)
62168

63169
describe("lines", function()
@@ -67,18 +173,28 @@ describe("io", function()
67173
end
68174
]]))
69175

70-
it("with a filename argument", util.check([[
71-
for l in io.popen("ls"):lines("filename.txt") do
72-
print(l:upper())
176+
it("with a bytes format argument", util.check([[
177+
for c in io.popen("ls"):lines("filename.txt", 1) do
178+
print(c:upper())
73179
end
74180
]]))
75181

76-
it("with a format argument", util.check([[
77-
for c in io.popen("ls"):lines("filename.txt", 1) do
182+
it("with a string format argument", util.check([[
183+
for c in io.popen("ls"):lines("*l") do
78184
print(c:upper())
79185
end
80186
]]))
81187

188+
it("with a numeric format", util.check([[
189+
for a in io.popen("ls"):lines("n") do
190+
print(a * 2)
191+
end
192+
193+
for a in io.popen("ls"):lines("*n") do
194+
print(a * 2)
195+
end
196+
]]))
197+
82198
it("with multiple formats", util.check([[
83199
for a, b, c in io.popen("ls"):lines("filename.txt", "l", 12, 13) do
84200
print(a:upper())
@@ -87,10 +203,14 @@ describe("io", function()
87203
end
88204
]]))
89205

90-
pending("resolves the type of numeric formats", util.check([[
91-
for a, b in io.popen("ls"):lines("filename.txt", "n", 12) do
92-
print(n * 2)
93-
print(b:upper())
206+
it("resolves the type of mixed numeric/string formats as unions for now", util.check([[
207+
for a, b in io.popen("ls"):lines("n", 12) do
208+
if a is number then
209+
print(a * 2)
210+
end
211+
if b is string then
212+
print(b:upper())
213+
end
94214
end
95215
]]))
96216
end)

tl.lua

Lines changed: 64 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5068,6 +5068,45 @@ local function init_globals(lax)
50685068
return t
50695069
end
50705070

5071+
local function an_enum(keys)
5072+
local t = a_type({
5073+
typename = "enum",
5074+
enumset = {},
5075+
})
5076+
for _, k in ipairs(keys) do
5077+
t.enumset[k] = true
5078+
end
5079+
return t
5080+
end
5081+
5082+
5083+
5084+
5085+
5086+
5087+
5088+
5089+
5090+
local file_reader_poly_types = {
5091+
{ ctor = VARARG, args = { UNION({ NUMBER, an_enum({ "*a", "a", "*l", "l", "*L", "L" }) }) }, rets = { STRING } },
5092+
{ ctor = TUPLE, args = { an_enum({ "*n", "n" }) }, rets = { NUMBER, STRING } },
5093+
{ ctor = VARARG, args = { UNION({ NUMBER, an_enum({ "*a", "a", "*l", "l", "*L", "L", "*n", "n" }) }) }, rets = { UNION({ STRING, NUMBER }) } },
5094+
{ ctor = VARARG, args = { UNION({ NUMBER, STRING }) }, rets = { STRING } },
5095+
}
5096+
5097+
local function a_file_reader(fn)
5098+
local t = a_type({
5099+
typename = "poly",
5100+
types = {},
5101+
})
5102+
for _, entry in ipairs(file_reader_poly_types) do
5103+
local args = shallow_copy_type(entry.args)
5104+
local rets = shallow_copy_type(entry.rets)
5105+
table.insert(t.types, fn(entry.ctor, args, rets))
5106+
end
5107+
return t
5108+
end
5109+
50715110
local LOAD_FUNCTION = a_type({ typename = "function", args = {}, rets = TUPLE({ STRING }) })
50725111

50735112
local OS_DATE_TABLE = a_type({
@@ -5085,8 +5124,6 @@ local function init_globals(lax)
50855124
},
50865125
})
50875126

5088-
local OS_DATE_TABLE_FORMAT = a_type({ typename = "enum", enumset = { ["!*t"] = true, ["*t"] = true } })
5089-
50905127
local DEBUG_GETINFO_TABLE = a_type({
50915128
typename = "record",
50925129
fields = {
@@ -5107,16 +5144,8 @@ local function init_globals(lax)
51075144
},
51085145
})
51095146

5110-
local DEBUG_HOOK_EVENT = a_type({
5111-
typename = "enum",
5112-
enumset = {
5113-
["call"] = true,
5114-
["tail call"] = true,
5115-
["return"] = true,
5116-
["line"] = true,
5117-
["count"] = true,
5118-
},
5119-
})
5147+
local DEBUG_HOOK_EVENT = an_enum({ "call", "tail call", "return", "line", "count" })
5148+
51205149
local DEBUG_HOOK_FUNCTION = a_type({
51215150
typename = "function",
51225151
args = TUPLE({ DEBUG_HOOK_EVENT, INTEGER }),
@@ -5161,9 +5190,9 @@ local function init_globals(lax)
51615190
["collectgarbage"] = a_type({
51625191
typename = "poly",
51635192
types = {
5164-
a_type({ typename = "function", args = TUPLE({ a_type({ typename = "enum", enumset = { ["collect"] = true, ["count"] = true, ["stop"] = true, ["restart"] = true } }) }), rets = TUPLE({ NUMBER }) }),
5165-
a_type({ typename = "function", args = TUPLE({ a_type({ typename = "enum", enumset = { ["step"] = true, ["setpause"] = true, ["setstepmul"] = true } }), NUMBER }), rets = TUPLE({ NUMBER }) }),
5166-
a_type({ typename = "function", args = TUPLE({ a_type({ typename = "enum", enumset = { ["isrunning"] = true } }) }), rets = TUPLE({ BOOLEAN }) }),
5193+
a_type({ typename = "function", args = TUPLE({ an_enum({ "collect", "count", "stop", "restart" }) }), rets = TUPLE({ NUMBER }) }),
5194+
a_type({ typename = "function", args = TUPLE({ an_enum({ "step", "setpause", "setstepmul" }), NUMBER }), rets = TUPLE({ NUMBER }) }),
5195+
a_type({ typename = "function", args = TUPLE({ an_enum({ "isrunning" }) }), rets = TUPLE({ BOOLEAN }) }),
51675196
a_type({ typename = "function", args = TUPLE({ STRING, OPT(NUMBER) }), rets = TUPLE({ a_type({ typename = "union", types = { BOOLEAN, NUMBER } }) }) }),
51685197
},
51695198
}),
@@ -5226,10 +5255,16 @@ local function init_globals(lax)
52265255
fields = {
52275256
["close"] = a_type({ typename = "function", args = TUPLE({ NOMINAL_FILE }), rets = TUPLE({ BOOLEAN, STRING, INTEGER }) }),
52285257
["flush"] = a_type({ typename = "function", args = TUPLE({ NOMINAL_FILE }), rets = TUPLE({}) }),
5229-
["lines"] = a_type({ typename = "function", args = VARARG({ NOMINAL_FILE, a_type({ typename = "union", types = { STRING, NUMBER } }) }), rets = TUPLE({
5230-
a_type({ typename = "function", args = TUPLE({}), rets = VARARG({ STRING }) }),
5231-
}), }),
5232-
["read"] = a_type({ typename = "function", args = TUPLE({ NOMINAL_FILE, UNION({ STRING, NUMBER }) }), rets = TUPLE({ STRING, STRING }) }),
5258+
["lines"] = a_file_reader(function(ctor, args, rets)
5259+
table.insert(args, 1, NOMINAL_FILE)
5260+
return a_type({ typename = "function", args = ctor(args), rets = TUPLE({
5261+
a_type({ typename = "function", args = TUPLE({}), rets = ctor(rets) }),
5262+
}), })
5263+
end),
5264+
["read"] = a_file_reader(function(ctor, args, rets)
5265+
table.insert(args, 1, NOMINAL_FILE)
5266+
return a_type({ typename = "function", args = ctor(args), rets = ctor(rets) })
5267+
end),
52335268
["seek"] = a_type({ typename = "function", args = TUPLE({ NOMINAL_FILE, OPT(STRING), OPT(NUMBER) }), rets = TUPLE({ INTEGER, STRING }) }),
52345269
["setvbuf"] = a_type({ typename = "function", args = TUPLE({ NOMINAL_FILE, STRING, OPT(NUMBER) }), rets = TUPLE({}) }),
52355270
["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)
52475282
["__gc"] = a_type({ typename = "function", args = TUPLE({ a }), rets = TUPLE({}) }),
52485283
["__index"] = ANY,
52495284
["__len"] = a_type({ typename = "function", args = TUPLE({ a }), rets = TUPLE({ ANY }) }),
5250-
["__mode"] = a_type({ typename = "enum", enumset = { ["k"] = true, ["v"] = true, ["kv"] = true } }),
5285+
["__mode"] = an_enum({ "k", "v", "kv" }),
52515286
["__newindex"] = ANY,
52525287
["__pairs"] = a_gfunction(2, function(k, v)
52535288
return {
@@ -5365,13 +5400,17 @@ local function init_globals(lax)
53655400
["close"] = a_type({ typename = "function", args = TUPLE({ OPT(NOMINAL_FILE) }), rets = TUPLE({ BOOLEAN, STRING }) }),
53665401
["flush"] = a_type({ typename = "function", args = TUPLE({}), rets = TUPLE({}) }),
53675402
["input"] = a_type({ typename = "function", args = TUPLE({ OPT(UNION({ STRING, NOMINAL_FILE })) }), rets = TUPLE({ NOMINAL_FILE }) }),
5368-
["lines"] = a_type({ typename = "function", args = VARARG({ OPT(STRING), a_type({ typename = "union", types = { STRING, NUMBER } }) }), rets = TUPLE({
5369-
a_type({ typename = "function", args = TUPLE({}), rets = VARARG({ STRING }) }),
5370-
}), }),
5403+
["lines"] = a_file_reader(function(ctor, args, rets)
5404+
return a_type({ typename = "function", args = ctor(args), rets = TUPLE({
5405+
a_type({ typename = "function", args = TUPLE({}), rets = ctor(rets) }),
5406+
}), })
5407+
end),
53715408
["open"] = a_type({ typename = "function", args = TUPLE({ STRING, STRING }), rets = TUPLE({ NOMINAL_FILE, STRING }) }),
53725409
["output"] = a_type({ typename = "function", args = TUPLE({ OPT(UNION({ STRING, NOMINAL_FILE })) }), rets = TUPLE({ NOMINAL_FILE }) }),
53735410
["popen"] = a_type({ typename = "function", args = TUPLE({ STRING, STRING }), rets = TUPLE({ NOMINAL_FILE, STRING }) }),
5374-
["read"] = a_type({ typename = "function", args = TUPLE({ UNION({ STRING, NUMBER }) }), rets = TUPLE({ STRING, STRING }) }),
5411+
["read"] = a_file_reader(function(ctor, args, rets)
5412+
return a_type({ typename = "function", args = ctor(args), rets = ctor(rets) })
5413+
end),
53755414
["stderr"] = NOMINAL_FILE,
53765415
["stdin"] = NOMINAL_FILE,
53775416
["stdout"] = NOMINAL_FILE,
@@ -5462,7 +5501,7 @@ local function init_globals(lax)
54625501
typename = "poly",
54635502
types = {
54645503
a_type({ typename = "function", args = TUPLE({}), rets = TUPLE({ STRING }) }),
5465-
a_type({ typename = "function", args = TUPLE({ OS_DATE_TABLE_FORMAT, NUMBER }), rets = TUPLE({ OS_DATE_TABLE }) }),
5504+
a_type({ typename = "function", args = TUPLE({ an_enum({ "!*t", "*t" }), NUMBER }), rets = TUPLE({ OS_DATE_TABLE }) }),
54665505
a_type({ typename = "function", args = TUPLE({ OPT(STRING), OPT(NUMBER) }), rets = TUPLE({ STRING }) }),
54675506
},
54685507
}),

0 commit comments

Comments
 (0)