• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lunarmodules / Penlight / 583

11 Aug 2025 02:27PM UTC coverage: 89.269% (+0.4%) from 88.871%
583

Pull #498

appveyor

web-flow
fix(*): some more Lua 5.5 fixes for constant loop variables (#499)
Pull Request #498: fix(*): some Lua 5.5 fixes for constant loop variables

23 of 24 new or added lines in 4 files covered. (95.83%)

79 existing lines in 14 files now uncovered.

5482 of 6141 relevant lines covered (89.27%)

165.45 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.2
/lua/pl/comprehension.lua
1
--- List comprehensions implemented in Lua.
2
--
3
-- See the [wiki page](http://lua-users.org/wiki/ListComprehensions)
4
--
5
--    local C= require 'pl.comprehension' . new()
6
--
7
--    C ('x for x=1,10') ()
8
--    ==> {1,2,3,4,5,6,7,8,9,10}
9
--    C 'x^2 for x=1,4' ()
10
--    ==> {1,4,9,16}
11
--    C '{x,x^2} for x=1,4' ()
12
--    ==> {{1,1},{2,4},{3,9},{4,16}}
13
--    C '2*x for x' {1,2,3}
14
--    ==> {2,4,6}
15
--    dbl = C '2*x for x'
16
--    dbl {10,20,30}
17
--    ==> {20,40,60}
18
--    C 'x for x if x % 2 == 0' {1,2,3,4,5}
19
--    ==> {2,4}
20
--    C '{x,y} for x = 1,2 for y = 1,2' ()
21
--    ==> {{1,1},{1,2},{2,1},{2,2}}
22
--    C '{x,y} for x for y' ({1,2},{10,20})
23
--    ==> {{1,10},{1,20},{2,10},{2,20}}
24
--    assert(C 'sum(x^2 for x)' {2,3,4} == 2^2+3^2+4^2)
25
--
26
-- (c) 2008 David Manura. Licensed under the same terms as Lua (MIT license).
27
--
28
-- Dependencies: `pl.utils`, `pl.luabalanced`
29
--
30
-- See @{07-functional.md.List_Comprehensions|the Guide}
31
-- @module pl.comprehension
32

33
local utils = require 'pl.utils'
4✔
34

35
local status,lb = pcall(require, "pl.luabalanced")
4✔
36
if not status then
4✔
37
    lb = require 'luabalanced'
×
38
end
39

40
local math_max = math.max
4✔
41
local table_concat = table.concat
4✔
42

43
-- fold operations
44
-- http://en.wikipedia.org/wiki/Fold_(higher-order_function)
45
local ops = {
4✔
46
  list = {init=' {} ', accum=' __result[#__result+1] = (%s) '},
4✔
47
  table = {init=' {} ', accum=' local __k, __v = %s __result[__k] = __v '},
4✔
48
  sum = {init=' 0 ', accum=' __result = __result + (%s) '},
4✔
49
  min = {init=' nil ', accum=' local __tmp = %s ' ..
4✔
50
                             ' if __result then if __tmp < __result then ' ..
7✔
51
                             '__result = __tmp end else __result = __tmp end '},
7✔
52
  max = {init=' nil ', accum=' local __tmp = %s ' ..
4✔
53
                             ' if __result then if __tmp > __result then ' ..
7✔
54
                             '__result = __tmp end else __result = __tmp end '},
7✔
55
}
56

57

58
-- Parses comprehension string expr.
59
-- Returns output expression list <out> string, array of for types
60
-- ('=', 'in' or nil) <fortypes>, array of input variable name
61
-- strings <invarlists>, array of input variable value strings
62
-- <invallists>, array of predicate expression strings <preds>,
63
-- operation name string <opname>, and number of placeholder
64
-- parameters <max_param>.
65
--
66
-- The is equivalent to the mathematical set-builder notation:
67
--
68
--   <opname> { <out> | <invarlist> in <invallist> , <preds> }
69
--
70
-- @usage   "x^2 for x"                 -- array values
71
-- @usage  "x^2 for x=1,10,2"          -- numeric for
72
-- @usage  "k^v for k,v in pairs(_1)"  -- iterator for
73
-- @usage  "(x+y)^2 for x for y if x > y"  -- nested
74
--
75
local function parse_comprehension(expr)
76
  local pos = 1
116✔
77

78
  -- extract opname (if exists)
79
  local opname
80
  local tok, post = expr:match('^%s*([%a_][%w_]*)%s*%(()', pos)
116✔
81
  local pose = #expr + 1
116✔
82
  if tok then
116✔
83
    local tok2, posb = lb.match_bracketed(expr, post-1)
80✔
84
    assert(tok2, 'syntax error')
80✔
85
    if expr:match('^%s*$', posb) then
80✔
86
      opname = tok
80✔
87
      pose = posb - 1
80✔
88
      pos = post
80✔
89
    end
90
  end
91
  opname = opname or "list"
116✔
92

93
  -- extract out expression list
94
  local out; out, pos = lb.match_explist(expr, pos)
116✔
95
  assert(out, "syntax error: missing expression list")
116✔
96
  out = table_concat(out, ', ')
116✔
97

98
  -- extract "for" clauses
99
  local fortypes = {}
116✔
100
  local invarlists = {}
116✔
101
  local invallists = {}
116✔
UNCOV
102
  while 1 do
×
103
    local post = expr:match('^%s*for%s+()', pos)
260✔
104
    if not post then break end
260✔
105
    pos = post
148✔
106

107
    -- extract input vars
108
    local iv; iv, pos = lb.match_namelist(expr, pos)
148✔
109
    assert(#iv > 0, 'syntax error: zero variables')
148✔
110
    for _,ident in ipairs(iv) do
304✔
111
      assert(not ident:match'^__',
320✔
112
             "identifier " .. ident .. " may not contain __ prefix")
160✔
113
    end
114
    invarlists[#invarlists+1] = iv
144✔
115

116
    -- extract '=' or 'in' (optional)
117
    local fortype, post = expr:match('^(=)%s*()', pos)
144✔
118
    if not fortype then fortype, post = expr:match('^(in)%s+()', pos) end
144✔
119
    if fortype then
144✔
120
      pos = post
36✔
121
      -- extract input value range
122
      local il; il, pos = lb.match_explist(expr, pos)
36✔
123
      assert(#il > 0, 'syntax error: zero expressions')
36✔
124
      assert(fortype ~= '=' or #il == 2 or #il == 3,
72✔
125
             'syntax error: numeric for requires 2 or three expressions')
36✔
126
      fortypes[#invarlists] = fortype
36✔
127
      invallists[#invarlists] = il
36✔
128
    else
129
      fortypes[#invarlists] = false
108✔
130
      invallists[#invarlists] = false
108✔
131
    end
132
  end
133
  assert(#invarlists > 0, 'syntax error: missing "for" clause')
112✔
134

135
  -- extract "if" clauses
136
  local preds = {}
112✔
UNCOV
137
  while 1 do
×
138
    local post = expr:match('^%s*if%s+()', pos)
140✔
139
    if not post then break end
140✔
140
    pos = post
28✔
141
    local pred; pred, pos = lb.match_expression(expr, pos)
28✔
142
    assert(pred, 'syntax error: predicated expression not found')
28✔
143
    preds[#preds+1] = pred
28✔
144
  end
145

146
  -- extract number of parameter variables (name matching "_%d+")
147
  local stmp = ''; lb.gsub(expr, function(u, sin)  -- strip comments/strings
224✔
148
    if u == 'e' then stmp = stmp .. ' ' .. sin .. ' ' end
140✔
149
  end)
150
  local max_param = 0; stmp:gsub('[%a_][%w_]*', function(s)
224✔
151
    local s = s:match('^_(%d+)$')
684✔
152
    if s then max_param = math_max(max_param, tonumber(s)) end
684✔
153
  end)
154

155
  if pos ~= pose then
112✔
156
    assert(false, "syntax error: unrecognized " .. expr:sub(pos))
×
157
  end
158

159
  --DEBUG:
160
  --print('----\n', string.format("%q", expr), string.format("%q", out), opname)
161
  --for k,v in ipairs(invarlists) do print(k,v, invallists[k]) end
162
  --for k,v in ipairs(preds) do print(k,v) end
163

164
  return out, fortypes, invarlists, invallists, preds, opname, max_param
112✔
165
end
166

167

168
-- Create Lua code string representing comprehension.
169
-- Arguments are in the form returned by parse_comprehension.
170
local function code_comprehension(
171
    out, fortypes, invarlists, invallists, preds, opname, max_param
172
)
173
  local op = assert(ops[opname])
112✔
174
  local code = op.accum:gsub('%%s',  out)
112✔
175

176
  for i=#preds,1,-1 do local pred = preds[i]
163✔
177
    code = ' if ' .. pred .. ' then ' .. code .. ' end '
28✔
178
  end
179
  for i=#invarlists,1,-1 do
256✔
180
    if not fortypes[i] then
144✔
181
      local arrayname = '__in' .. i
108✔
182
      local idx = '__idx' .. i
108✔
183
      code =
×
184
        ' for ' .. idx .. ' = 1, #' .. arrayname .. ' do ' ..
108✔
185
        ' local ' .. invarlists[i][1] .. ' = ' .. arrayname .. '['..idx..'] ' ..
108✔
186
        code .. ' end '
108✔
187
    else
188
      code =
×
189
        ' for ' ..
36✔
190
        table_concat(invarlists[i], ', ') ..
36✔
191
        ' ' .. fortypes[i] .. ' ' ..
36✔
192
        table_concat(invallists[i], ', ') ..
36✔
193
        ' do ' .. code .. ' end '
36✔
194
    end
195
  end
196
  code = ' local __result = ( ' .. op.init .. ' ) ' .. code
112✔
197
  return code
112✔
198
end
199

200

201
-- Convert code string represented by code_comprehension
202
-- into Lua function.  Also must pass ninputs = #invarlists,
203
-- max_param, and invallists (from parse_comprehension).
204
-- Uses environment env.
205
local function wrap_comprehension(code, ninputs, max_param, invallists, env)
206
  assert(ninputs > 0)
112✔
207
  local ts = {}
112✔
208
  for i=1,max_param do
152✔
209
    ts[#ts+1] = '_' .. i
40✔
210
  end
211
  for i=1,ninputs do
256✔
212
    if not invallists[i] then
144✔
213
      local name = '__in' .. i
108✔
214
      ts[#ts+1] = name
108✔
215
    end
216
  end
217
  if #ts > 0 then
112✔
218
    code = ' local ' .. table_concat(ts, ', ') .. ' = ... ' .. code
104✔
219
  end
220
  code = code .. ' return __result '
112✔
221
  --print('DEBUG:', code)
222
  local f, err = utils.load(code,'tmp','t',env)
112✔
223
  if not f then assert(false, err .. ' with generated code ' .. code) end
112✔
224
  return f
112✔
225
end
226

227

228
-- Build Lua function from comprehension string.
229
-- Uses environment env.
230
local function build_comprehension(expr, env)
231
  local out, fortypes, invarlists, invallists, preds, opname, max_param
232
    = parse_comprehension(expr)
116✔
233
  local code = code_comprehension(
224✔
234
    out, fortypes, invarlists, invallists, preds, opname, max_param)
112✔
235
  local f = wrap_comprehension(code, #invarlists, max_param, invallists, env)
112✔
236
  return f
112✔
237
end
238

239

240
-- Creates new comprehension cache.
241
-- Any list comprehension function created are set to the environment
242
-- env (defaults to caller of new).
243
local function new(env)
244
  -- Note: using a single global comprehension cache would have had
245
  -- security implications (e.g. retrieving cached functions created
246
  -- in other environments).
247
  -- The cache lookup function could have instead been written to retrieve
248
  -- the caller's environment, lookup up the cache private to that
249
  -- environment, and then looked up the function in that cache.
250
  -- That would avoid the need for this <new> call to
251
  -- explicitly manage caches; however, that might also have an undue
252
  -- performance penalty.
253

254
  if not env then
8✔
255
    env = utils.getfenv(2)
4✔
256
  end
257

258
  local mt = {}
8✔
259
  local cache = setmetatable({}, mt)
8✔
260

261
  -- Index operator builds, caches, and returns Lua function
262
  -- corresponding to comprehension expression string.
263
  --
264
  -- Example: f = comprehension['x^2 for x']
265
  --
266
  function mt:__index(expr)
8✔
267
    local f = build_comprehension(expr, env)
116✔
268
    self[expr] = f  -- cache
112✔
269
    return f
112✔
270
  end
271

272
  -- Convenience syntax.
273
  -- Allows comprehension 'x^2 for x' instead of comprehension['x^2 for x'].
274
  mt.__call = mt.__index
8✔
275

276
  cache.new = new
8✔
277

278
  return cache
8✔
279
end
280

281

282
local comprehension = {}
4✔
283
comprehension.new = new
4✔
284

285
return comprehension
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc