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

lunarmodules / Penlight / 21463161078

29 Jan 2026 02:21AM UTC coverage: 89.157% (-1.0%) from 90.15%
21463161078

push

github

web-flow
Merge 95f31fda7 into 678de0ebb

6 of 6 new or added lines in 1 file covered. (100.0%)

44 existing lines in 3 files now uncovered.

5476 of 6142 relevant lines covered (89.16%)

377.42 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'
9✔
34

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

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

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

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

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

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

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

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

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

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

155
  if pos ~= pose then
252✔
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
252✔
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])
252✔
174
  local code = op.accum:gsub('%%s',  out)
252✔
175

176
  for i=#preds,1,-1 do local pred = preds[i]
363✔
177
    code = ' if ' .. pred .. ' then ' .. code .. ' end '
63✔
178
  end
179
  for i=#invarlists,1,-1 do
576✔
180
    if not fortypes[i] then
324✔
181
      local arrayname = '__in' .. i
243✔
182
      local idx = '__idx' .. i
243✔
183
      code =
×
184
        ' for ' .. idx .. ' = 1, #' .. arrayname .. ' do ' ..
243✔
185
        ' local ' .. invarlists[i][1] .. ' = ' .. arrayname .. '['..idx..'] ' ..
243✔
186
        code .. ' end '
243✔
187
    else
188
      code =
×
189
        ' for ' ..
81✔
190
        table_concat(invarlists[i], ', ') ..
81✔
191
        ' ' .. fortypes[i] .. ' ' ..
81✔
192
        table_concat(invallists[i], ', ') ..
81✔
193
        ' do ' .. code .. ' end '
81✔
194
    end
195
  end
196
  code = ' local __result = ( ' .. op.init .. ' ) ' .. code
252✔
197
  return code
252✔
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)
252✔
207
  local ts = {}
252✔
208
  for i=1,max_param do
342✔
209
    ts[#ts+1] = '_' .. i
90✔
210
  end
211
  for i=1,ninputs do
576✔
212
    if not invallists[i] then
324✔
213
      local name = '__in' .. i
243✔
214
      ts[#ts+1] = name
243✔
215
    end
216
  end
217
  if #ts > 0 then
252✔
218
    code = ' local ' .. table_concat(ts, ', ') .. ' = ... ' .. code
234✔
219
  end
220
  code = code .. ' return __result '
252✔
221
  --print('DEBUG:', code)
222
  local f, err = utils.load(code,'tmp','t',env)
252✔
223
  if not f then assert(false, err .. ' with generated code ' .. code) end
252✔
224
  return f
252✔
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)
261✔
233
  local code = code_comprehension(
504✔
234
    out, fortypes, invarlists, invallists, preds, opname, max_param)
252✔
235
  local f = wrap_comprehension(code, #invarlists, max_param, invallists, env)
252✔
236
  return f
252✔
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
18✔
255
    env = utils.getfenv(2)
9✔
256
  end
257

258
  local mt = {}
18✔
259
  local cache = setmetatable({}, mt)
18✔
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)
18✔
267
    local f = build_comprehension(expr, env)
261✔
268
    self[expr] = f  -- cache
252✔
269
    return f
252✔
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
18✔
275

276
  cache.new = new
18✔
277

278
  return cache
18✔
279
end
280

281

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

285
return comprehension
9✔
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

© 2026 Coveralls, Inc