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

lunarmodules / Penlight / 16815657215

07 Aug 2025 08:56PM UTC coverage: 88.858% (-0.01%) from 88.871%
16815657215

push

github

Tieske
fix(*): some Lua 5.5 fixes for constant loop variables

see #497

12 of 13 new or added lines in 4 files covered. (92.31%)

9 existing lines in 3 files now uncovered.

5455 of 6139 relevant lines covered (88.86%)

256.08 hits per line

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

96.8
/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'
6✔
34

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

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

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

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

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

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

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

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

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

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

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

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

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

276
  cache.new = new
12✔
277

278
  return cache
12✔
279
end
280

281

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

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