mirror of
https://github.com/FlightControl-Master/MOOSE.git
synced 2025-08-15 10:47:21 +00:00
Add the precompiled Lua 5.1, in order to be used by the install script.
Note tha LF had to be converted to CRLF. Doesn't seem to ba an issue though.
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
return [[html {
|
||||
color: #000;
|
||||
background: #FFF;
|
||||
}
|
||||
body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
fieldset,img {
|
||||
border: 0;
|
||||
}
|
||||
address,caption,cite,code,dfn,em,strong,th,var,optgroup {
|
||||
font-style: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
del,ins {
|
||||
text-decoration: none;
|
||||
}
|
||||
li {
|
||||
list-style: bullet;
|
||||
margin-left: 20px;
|
||||
}
|
||||
caption,th {
|
||||
text-align: left;
|
||||
}
|
||||
h1,h2,h3,h4,h5,h6 {
|
||||
font-size: 100%;
|
||||
font-weight: bold;
|
||||
}
|
||||
q:before,q:after {
|
||||
content: '';
|
||||
}
|
||||
abbr,acronym {
|
||||
border: 0;
|
||||
font-variant: normal;
|
||||
}
|
||||
sup {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
sub {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
legend {
|
||||
color: #000;
|
||||
}
|
||||
input,button,textarea,select,optgroup,option {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
font-style: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
input,button,textarea,select {*font-size:100%;
|
||||
}
|
||||
/* END RESET */
|
||||
|
||||
body {
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
font-family: arial, helvetica, geneva, sans-serif;
|
||||
background-color: #ffffff; margin: 0px;
|
||||
}
|
||||
|
||||
code, tt { font-family: monospace; }
|
||||
|
||||
body, p, td, th { font-size: .95em; line-height: 1.2em;}
|
||||
|
||||
p, ul { margin: 10px 0 0 10px;}
|
||||
|
||||
strong { font-weight: bold;}
|
||||
|
||||
em { font-style: italic;}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5em;
|
||||
margin: 25px 0 20px 0;
|
||||
}
|
||||
h2, h3, h4 { margin: 15px 0 10px 0; }
|
||||
h2 { font-size: 1.25em; }
|
||||
h3 { font-size: 1.15em; }
|
||||
h4 { font-size: 1.06em; }
|
||||
|
||||
a:link { font-weight: bold; color: #004080; text-decoration: none; }
|
||||
a:visited { font-weight: bold; color: #006699; text-decoration: none; }
|
||||
a:link:hover { text-decoration: underline; }
|
||||
|
||||
hr {
|
||||
color:#cccccc;
|
||||
background: #00007f;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
blockquote { margin-left: 3em; }
|
||||
|
||||
ul { list-style-type: disc; }
|
||||
|
||||
p.name {
|
||||
font-family: "Andale Mono", monospace;
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
p:first-child {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
pre.example {
|
||||
background-color: rgb(245, 245, 245);
|
||||
border: 1px solid silver;
|
||||
padding: 10px;
|
||||
margin: 10px 0 10px 0;
|
||||
font-family: "Andale Mono", monospace;
|
||||
font-size: .85em;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: rgb(245, 245, 245);
|
||||
border: 1px solid silver;
|
||||
padding: 10px;
|
||||
margin: 10px 0 10px 0;
|
||||
font-family: "Andale Mono", monospace;
|
||||
}
|
||||
|
||||
|
||||
table.index { border: 1px #00007f; }
|
||||
table.index td { text-align: left; vertical-align: top; }
|
||||
|
||||
#container {
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
#product {
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #cccccc;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
#product big {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
#main {
|
||||
background-color: #f0f0f0;
|
||||
border-left: 2px solid #cccccc;
|
||||
}
|
||||
|
||||
#navigation {
|
||||
float: left;
|
||||
width: 18em;
|
||||
vertical-align: top;
|
||||
background-color: #f0f0f0;
|
||||
overflow: scroll;
|
||||
position: fixed;
|
||||
height:100%;
|
||||
}
|
||||
|
||||
#navigation h2 {
|
||||
background-color:#e7e7e7;
|
||||
font-size:1.1em;
|
||||
color:#000000;
|
||||
text-align: left;
|
||||
padding:0.2em;
|
||||
border-top:1px solid #dddddd;
|
||||
border-bottom:1px solid #dddddd;
|
||||
}
|
||||
|
||||
#navigation ul
|
||||
{
|
||||
font-size:1em;
|
||||
list-style-type: none;
|
||||
margin: 1px 1px 10px 1px;
|
||||
}
|
||||
|
||||
#navigation li {
|
||||
text-indent: -1em;
|
||||
display: block;
|
||||
margin: 3px 0px 0px 22px;
|
||||
}
|
||||
|
||||
#navigation li li a {
|
||||
margin: 0px 3px 0px -1em;
|
||||
}
|
||||
|
||||
#content {
|
||||
margin-left: 18em;
|
||||
padding: 1em;
|
||||
border-left: 2px solid #cccccc;
|
||||
border-right: 2px solid #cccccc;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
#about {
|
||||
clear: both;
|
||||
padding: 5px;
|
||||
border-top: 2px solid #cccccc;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
font: 12pt "Times New Roman", "TimeNR", Times, serif;
|
||||
}
|
||||
a { font-weight: bold; color: #004080; text-decoration: underline; }
|
||||
|
||||
#main {
|
||||
background-color: #ffffff;
|
||||
border-left: 0px;
|
||||
}
|
||||
|
||||
#container {
|
||||
margin-left: 2%;
|
||||
margin-right: 2%;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
#content {
|
||||
padding: 1em;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
#navigation {
|
||||
display: none;
|
||||
}
|
||||
pre.example {
|
||||
font-family: "Andale Mono", monospace;
|
||||
font-size: 10pt;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
}
|
||||
|
||||
table.module_list {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #cccccc;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.module_list td {
|
||||
border-width: 1px;
|
||||
padding: 3px;
|
||||
border-style: solid;
|
||||
border-color: #cccccc;
|
||||
}
|
||||
table.module_list td.name { background-color: #f0f0f0; }
|
||||
table.module_list td.summary { width: 100%; }
|
||||
|
||||
|
||||
table.function_list {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #cccccc;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.function_list td {
|
||||
border-width: 1px;
|
||||
padding: 3px;
|
||||
border-style: solid;
|
||||
border-color: #cccccc;
|
||||
}
|
||||
table.function_list td.name { background-color: #f0f0f0; }
|
||||
table.function_list td.summary { width: 100%; }
|
||||
|
||||
dl.table dt, dl.function dt {border-top: 1px solid #ccc; padding-top: 1em;}
|
||||
dl.table dd, dl.function dd {padding-bottom: 1em; margin: 10px 0 0 20px;}
|
||||
dl.table h3, dl.function h3 {font-size: .95em;}
|
||||
|
||||
]]
|
||||
@@ -0,0 +1,87 @@
|
||||
--------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2012-2014 Sierra Wireless.
|
||||
-- All rights reserved. This program and the accompanying materials
|
||||
-- are made available under the terms of the Eclipse Public License v1.0
|
||||
-- which accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Kevin KIN-FOO <kkinfoo@sierrawireless.com>
|
||||
-- - initial API and implementation and initial documentation
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Load documentation generator and update its path
|
||||
--
|
||||
local templateengine = require 'templateengine'
|
||||
for name, def in pairs( require 'template.utils' ) do
|
||||
templateengine.env [ name ] = def
|
||||
end
|
||||
|
||||
-- Load documentation extractor and set handled languages
|
||||
local lddextractor = require 'lddextractor'
|
||||
|
||||
local M = {}
|
||||
M.defaultsitemainpagename = 'index'
|
||||
|
||||
function M.generatedocforfiles(filenames, cssname,noheuristic)
|
||||
if not filenames then return nil, 'No files provided.' end
|
||||
--
|
||||
-- Generate API model elements for all files
|
||||
--
|
||||
local generatedfiles = {}
|
||||
local wrongfiles = {}
|
||||
for _, filename in pairs( filenames ) do
|
||||
-- Load file content
|
||||
local file, error = io.open(filename, 'r')
|
||||
if not file then return nil, 'Unable to read "'..filename..'"\n'..err end
|
||||
local code = file:read('*all')
|
||||
file:close()
|
||||
-- Get module for current file
|
||||
local apimodule, err = lddextractor.generateapimodule(filename, code,noheuristic)
|
||||
|
||||
-- Handle modules with module name
|
||||
if apimodule and apimodule.name then
|
||||
generatedfiles[ apimodule.name ] = apimodule
|
||||
elseif not apimodule then
|
||||
-- Track faulty files
|
||||
table.insert(wrongfiles, 'Unable to extract comments from "'..filename..'".\n'..err)
|
||||
elseif not apimodule.name then
|
||||
-- Do not generate documentation for unnamed modules
|
||||
table.insert(wrongfiles, 'Unable to create documentation for "'..filename..'", no module name provided.')
|
||||
end
|
||||
end
|
||||
--
|
||||
-- Defining index, which will summarize all modules
|
||||
--
|
||||
local index = {
|
||||
modules = generatedfiles,
|
||||
name = M.defaultsitemainpagename,
|
||||
tag='index'
|
||||
}
|
||||
generatedfiles[ M.defaultsitemainpagename ] = index
|
||||
|
||||
--
|
||||
-- Define page cursor
|
||||
--
|
||||
local page = {
|
||||
currentmodule = nil,
|
||||
headers = { [[<link rel="stylesheet" href="]].. cssname ..[[" type="text/css"/>]] },
|
||||
modules = generatedfiles,
|
||||
tag = 'page'
|
||||
}
|
||||
|
||||
--
|
||||
-- Iterate over modules, generating complete doc pages
|
||||
--
|
||||
for _, module in pairs( generatedfiles ) do
|
||||
-- Update current cursor page
|
||||
page.currentmodule = module
|
||||
-- Generate page
|
||||
local content, error = templateengine.applytemplate(page)
|
||||
if not content then return nil, error end
|
||||
module.body = content
|
||||
end
|
||||
return generatedfiles, wrongfiles
|
||||
end
|
||||
return M
|
||||
@@ -0,0 +1,102 @@
|
||||
--------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2012-2014 Sierra Wireless.
|
||||
-- All rights reserved. This program and the accompanying materials
|
||||
-- are made available under the terms of the Eclipse Public License v1.0
|
||||
-- which accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Kevin KIN-FOO <kkinfoo@sierrawireless.com>
|
||||
-- - initial API and implementation and initial documentation
|
||||
--------------------------------------------------------------------------------
|
||||
local M = {}
|
||||
require 'metalua.loader'
|
||||
local compiler = require 'metalua.compiler'
|
||||
local mlc = compiler.new()
|
||||
local Q = require 'metalua.treequery'
|
||||
|
||||
-- Enable to retrieve all Javadoc-like comments from C code
|
||||
function M.c(code)
|
||||
if not code then return nil, 'No code provided' end
|
||||
local comments = {}
|
||||
-- Loop over comments stripping cosmetic '*'
|
||||
for comment in code:gmatch('%s*/%*%*+(.-)%*+/') do
|
||||
-- All Lua special comment are prefixed with an '-',
|
||||
-- so we also comment C comment to make them compliant
|
||||
table.insert(comments, '-'..comment)
|
||||
end
|
||||
return comments
|
||||
end
|
||||
|
||||
-- Enable to retrieve "---" comments from Lua code
|
||||
function M.lua( code )
|
||||
if not code then return nil, 'No code provided' end
|
||||
|
||||
-- manage shebang
|
||||
if code then code = code:gsub("^(#.-\n)", function (s) return string.rep(' ',string.len(s)) end) end
|
||||
|
||||
-- check for errors
|
||||
local f, err = loadstring(code,'source_to_check')
|
||||
if not f then
|
||||
return nil, 'Syntax error.\n' .. err
|
||||
end
|
||||
|
||||
-- Get ast from file
|
||||
local status, ast = pcall(mlc.src_to_ast, mlc, code)
|
||||
--
|
||||
-- Detect parsing errors
|
||||
--
|
||||
if not status then
|
||||
return nil, 'There might be a syntax error.\n' .. ast
|
||||
end
|
||||
|
||||
--
|
||||
-- Extract commented nodes from AST
|
||||
--
|
||||
|
||||
-- Function enabling commented node selection
|
||||
local function acceptcommentednode(node)
|
||||
return node.lineinfo and ( node.lineinfo.last.comments or node.lineinfo.first.comments )
|
||||
end
|
||||
|
||||
-- Fetch commented node from AST
|
||||
local commentednodes = Q(ast):filter( acceptcommentednode ):list()
|
||||
|
||||
-- Comment cache to avoid selecting same comment twice
|
||||
local commentcache = {}
|
||||
-- Will contain selected comments
|
||||
local comments = {}
|
||||
|
||||
-- Loop over commented nodes
|
||||
for _, node in ipairs( commentednodes ) do
|
||||
|
||||
-- A node can is relateds to comment before and after itself,
|
||||
-- the following gathers them.
|
||||
local commentlists = {}
|
||||
if node.lineinfo and node.lineinfo.first.comments then
|
||||
table.insert(commentlists, node.lineinfo.first.comments)
|
||||
end
|
||||
if node.lineinfo and node.lineinfo.last.comments then
|
||||
table.insert(commentlists, node.lineinfo.last.comments)
|
||||
end
|
||||
-- Now that we have comments before and fater the node,
|
||||
-- collect them in a single table
|
||||
for _, list in ipairs( commentlists ) do
|
||||
for _, commenttable in ipairs(list) do
|
||||
-- Only select special comments
|
||||
local firstcomment = #commenttable > 0 and #commenttable[1] > 0 and commenttable[1]
|
||||
if firstcomment:sub(1, 1) == '-' then
|
||||
for _, comment in ipairs( commenttable ) do
|
||||
-- Only comments which were not already collected
|
||||
if not commentcache[comment] then
|
||||
commentcache[comment] = true
|
||||
table.insert(comments, comment)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return comments
|
||||
end
|
||||
return M
|
||||
@@ -0,0 +1,130 @@
|
||||
--------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2012-2014 Sierra Wireless.
|
||||
-- All rights reserved. This program and the accompanying materials
|
||||
-- are made available under the terms of the Eclipse Public License v1.0
|
||||
-- which accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Kevin KIN-FOO <kkinfoo@sierrawireless.com>
|
||||
-- - initial API and implementation and initial documentation
|
||||
--------------------------------------------------------------------------------
|
||||
local lfs = require 'lfs'
|
||||
local M = {}
|
||||
local function iswindows()
|
||||
local p = io.popen("echo %os%")
|
||||
if not p then
|
||||
return false
|
||||
end
|
||||
local result =p:read("*l")
|
||||
p:close()
|
||||
return result == "Windows_NT"
|
||||
end
|
||||
M.separator = iswindows() and [[\]] or [[/]]
|
||||
---
|
||||
-- Will recursively browse given directories and list files encountered
|
||||
-- @param tab Table, list where files will be added
|
||||
-- @param dirorfiles list of path to browse in order to build list.
|
||||
-- Files from this list will be added to <code>tab</code> list.
|
||||
-- @return <code>tab</code> list, table containing all files from directories
|
||||
-- and files contained in <code>dirorfile</code>
|
||||
local function appendfiles(tab, dirorfile)
|
||||
|
||||
-- Nothing to process
|
||||
if #dirorfile < 1 then return tab end
|
||||
|
||||
-- Append all files to list
|
||||
local dirs = {}
|
||||
for _, path in ipairs( dirorfile ) do
|
||||
-- Determine element nature
|
||||
local elementnature = lfs.attributes (path, "mode")
|
||||
|
||||
-- Handle files
|
||||
if elementnature == 'file' then
|
||||
table.insert(tab, path)
|
||||
else if elementnature == 'directory' then
|
||||
|
||||
-- Check if folder is accessible
|
||||
local status, error = pcall(lfs.dir, path)
|
||||
if not status then return nil, error end
|
||||
|
||||
--
|
||||
-- Handle folders
|
||||
--
|
||||
for diskelement in lfs.dir(path) do
|
||||
|
||||
-- Format current file name
|
||||
local currentfilename
|
||||
if path:sub(#path) == M.separator then
|
||||
currentfilename = path .. diskelement
|
||||
else
|
||||
currentfilename = path .. M.separator .. diskelement
|
||||
end
|
||||
|
||||
-- Handle folder elements
|
||||
local nature, err = lfs.attributes (currentfilename, "mode")
|
||||
-- Append file to current list
|
||||
if nature == 'file' then
|
||||
table.insert(tab, currentfilename)
|
||||
elseif nature == 'directory' then
|
||||
-- Avoid current and parent directory in order to avoid
|
||||
-- endless recursion
|
||||
if diskelement ~= '.' and diskelement ~= '..' then
|
||||
-- Handle subfolders
|
||||
table.insert(dirs, currentfilename)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- If we only encountered files, going deeper is useless
|
||||
if #dirs == 0 then return tab end
|
||||
-- Append files from encountered directories
|
||||
return appendfiles(tab, dirs)
|
||||
end
|
||||
---
|
||||
-- Provide a list of files from a directory
|
||||
-- @param list Table of directories to browse
|
||||
-- @return table of string, path to files contained in given directories
|
||||
function M.filelist(list)
|
||||
if not list then return nil, 'No directory list provided' end
|
||||
return appendfiles({}, list)
|
||||
end
|
||||
function M.checkdirectory( dirlist )
|
||||
if not dirlist then return false end
|
||||
local missingdirs = {}
|
||||
for _, filename in ipairs( dirlist ) do
|
||||
if not lfs.attributes(filename, 'mode') then
|
||||
table.insert(missingdirs, filename)
|
||||
end
|
||||
end
|
||||
if #missingdirs > 0 then
|
||||
return false, missingdirs
|
||||
end
|
||||
return true
|
||||
end
|
||||
function M.fill(filename, content)
|
||||
--
|
||||
-- Ensure parent directory exists
|
||||
--
|
||||
local parent = filename:gmatch([[(.*)]] .. M.separator ..[[(.+)]])()
|
||||
local parentnature = lfs.attributes(parent, 'mode')
|
||||
-- Create parent directory while absent
|
||||
if not parentnature then
|
||||
lfs.mkdir( parent )
|
||||
elseif parentnature ~= 'directory' then
|
||||
-- Notify that disk element already exists
|
||||
return nil, parent..' is a '..parentnature..'.'
|
||||
end
|
||||
|
||||
-- Create actual file
|
||||
local file, error = io.open(filename, 'w')
|
||||
if not file then
|
||||
return nil, error
|
||||
end
|
||||
file:write( content )
|
||||
file:close()
|
||||
return true
|
||||
end
|
||||
return M
|
||||
@@ -0,0 +1,113 @@
|
||||
--------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2012-2014 Sierra Wireless.
|
||||
-- All rights reserved. This program and the accompanying materials
|
||||
-- are made available under the terms of the Eclipse Public License v1.0
|
||||
-- which accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Kevin KIN-FOO <kkinfoo@sierrawireless.com>
|
||||
-- - initial API and implementation and initial documentation
|
||||
--------------------------------------------------------------------------------
|
||||
require 'metalua.loader'
|
||||
local compiler = require 'metalua.compiler'
|
||||
local mlc = compiler.new()
|
||||
local M = {}
|
||||
|
||||
--
|
||||
-- Define default supported languages
|
||||
--
|
||||
M.supportedlanguages = {}
|
||||
local extractors = require 'extractors'
|
||||
|
||||
-- Support Lua comment extracting
|
||||
M.supportedlanguages['lua'] = extractors.lua
|
||||
|
||||
-- Support C comment extracting
|
||||
for _,c in ipairs({'c', 'cpp', 'c++'}) do
|
||||
M.supportedlanguages[c] = extractors.c
|
||||
end
|
||||
|
||||
-- Extract comment from code,
|
||||
-- type of code is deduced from filename extension
|
||||
function M.extract(filename, code)
|
||||
-- Check parameters
|
||||
if not code then return nil, 'No code provided' end
|
||||
if type(filename) ~= "string" then
|
||||
return nil, 'No string for file name provided'
|
||||
end
|
||||
|
||||
-- Extract file extension
|
||||
local fileextension = filename:gmatch('.*%.(.*)')()
|
||||
if not fileextension then
|
||||
return nil, 'File '..filename..' has no extension, could not determine how to extract documentation.'
|
||||
end
|
||||
|
||||
-- Check if it is possible to extract documentation from these files
|
||||
local extractor = M.supportedlanguages[ fileextension ]
|
||||
if not extractor then
|
||||
return nil, 'Unable to extract documentation from '.. fileextension .. ' file.'
|
||||
end
|
||||
return extractor( code )
|
||||
end
|
||||
-- Generate a file gathering only comments from given code
|
||||
function M.generatecommentfile(filename, code)
|
||||
local comments, error = M.extract(filename, code)
|
||||
if not comments then
|
||||
return nil, 'Unable to generate comment file.\n'..error
|
||||
end
|
||||
local filecontent = {}
|
||||
for _, comment in ipairs( comments ) do
|
||||
table.insert(filecontent, "--[[")
|
||||
table.insert(filecontent, comment)
|
||||
table.insert(filecontent, "\n]]\n\n")
|
||||
end
|
||||
return table.concat(filecontent)..'return nil\n'
|
||||
end
|
||||
-- Create API Model module from a 'comment only' lua file
|
||||
function M.generateapimodule(filename, code,noheuristic)
|
||||
if not filename then return nil, 'No file name given.' end
|
||||
if not code then return nil, 'No code provided.' end
|
||||
if type(filename) ~= "string" then return nil, 'No string for file name provided' end
|
||||
|
||||
-- for non lua file get comment file
|
||||
if filename:gmatch('.*%.(.*)')() ~= 'lua' then
|
||||
local err
|
||||
code, err = M.generatecommentfile(filename, code)
|
||||
if not code then
|
||||
return nil, 'Unable to create api module for "'..filename..'".\n'..err
|
||||
end
|
||||
else
|
||||
|
||||
-- manage shebang
|
||||
if code then code = code:gsub("^(#.-\n)", function (s) return string.rep(' ',string.len(s)) end) end
|
||||
|
||||
-- check for errors
|
||||
local f, err = loadstring(code,'source_to_check')
|
||||
if not f then
|
||||
return nil, 'File'..filename..'contains syntax error.\n' .. err
|
||||
end
|
||||
end
|
||||
|
||||
local status, ast = pcall(mlc.src_to_ast, mlc, code)
|
||||
if not status then
|
||||
return nil, 'Unable to compute ast for "'..filename..'".\n'..ast
|
||||
end
|
||||
|
||||
-- Extract module name as the filename without extension
|
||||
local modulename
|
||||
local matcher = string.gmatch(filename,'.*/(.*)%..*$')
|
||||
if matcher then modulename = matcher() end
|
||||
|
||||
-- Create api model
|
||||
local apimodelbuilder = require 'models.apimodelbuilder'
|
||||
local _file, comment2apiobj = apimodelbuilder.createmoduleapi(ast, modulename)
|
||||
|
||||
-- Create internal model
|
||||
if not noheuristic then
|
||||
local internalmodelbuilder = require "models.internalmodelbuilder"
|
||||
local _internalcontent = internalmodelbuilder.createinternalcontent(ast,_file,comment2apiobj, modulename)
|
||||
end
|
||||
return _file
|
||||
end
|
||||
return M
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,181 @@
|
||||
---------------------------------------------------------------------------
|
||||
-- Copyright (c) 2006-2013 Fabien Fleutot and others.
|
||||
--
|
||||
-- All rights reserved.
|
||||
--
|
||||
-- This program and the accompanying materials are made available
|
||||
-- under the terms of the Eclipse Public License v1.0 which
|
||||
-- accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- This program and the accompanying materials are also made available
|
||||
-- under the terms of the MIT public license which accompanies this
|
||||
-- distribution, and is available at http://www.lua.org/license.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Fabien Fleutot - API and implementation
|
||||
--
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
--
|
||||
-- Convert between various code representation formats. Atomic
|
||||
-- converters are written in extenso, others are composed automatically
|
||||
-- by chaining the atomic ones together in a closure.
|
||||
--
|
||||
-- Supported formats are:
|
||||
--
|
||||
-- * srcfile: the name of a file containing sources.
|
||||
-- * src: these sources as a single string.
|
||||
-- * lexstream: a stream of lexemes.
|
||||
-- * ast: an abstract syntax tree.
|
||||
-- * proto: a (Yueliang) struture containing a high level
|
||||
-- representation of bytecode. Largely based on the
|
||||
-- Proto structure in Lua's VM
|
||||
-- * bytecode: a string dump of the function, as taken by
|
||||
-- loadstring() and produced by string.dump().
|
||||
-- * function: an executable lua function in RAM.
|
||||
--
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local checks = require 'checks'
|
||||
|
||||
local M = { }
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Order of the transformations. if 'a' is on the left of 'b', then a 'a' can
|
||||
-- be transformed into a 'b' (but not the other way around).
|
||||
-- M.sequence goes for numbers to format names, M.order goes from format
|
||||
-- names to numbers.
|
||||
--------------------------------------------------------------------------------
|
||||
M.sequence = {
|
||||
'srcfile', 'src', 'lexstream', 'ast', 'proto', 'bytecode', 'function' }
|
||||
|
||||
local arg_types = {
|
||||
srcfile = { 'string', '?string' },
|
||||
src = { 'string', '?string' },
|
||||
lexstream = { 'lexer.stream', '?string' },
|
||||
ast = { 'table', '?string' },
|
||||
proto = { 'table', '?string' },
|
||||
bytecode = { 'string', '?string' },
|
||||
}
|
||||
|
||||
if false then
|
||||
-- if defined, runs on every newly-generated AST
|
||||
function M.check_ast(ast)
|
||||
local function rec(x, n, parent)
|
||||
if not x.lineinfo and parent.lineinfo then
|
||||
local pp = require 'metalua.pprint'
|
||||
pp.printf("WARNING: Missing lineinfo in child #%s `%s{...} of node at %s",
|
||||
n, x.tag or '', tostring(parent.lineinfo))
|
||||
end
|
||||
for i, child in ipairs(x) do
|
||||
if type(child)=='table' then rec(child, i, x) end
|
||||
end
|
||||
end
|
||||
rec(ast, -1, { })
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
M.order= { }; for a,b in pairs(M.sequence) do M.order[b]=a end
|
||||
|
||||
local CONV = { } -- conversion metatable __index
|
||||
|
||||
function CONV :srcfile_to_src(x, name)
|
||||
checks('metalua.compiler', 'string', '?string')
|
||||
name = name or '@'..x
|
||||
local f, msg = io.open (x, 'rb')
|
||||
if not f then error(msg) end
|
||||
local r, msg = f :read '*a'
|
||||
if not r then error("Cannot read file '"..x.."': "..msg) end
|
||||
f :close()
|
||||
return r, name
|
||||
end
|
||||
|
||||
function CONV :src_to_lexstream(src, name)
|
||||
checks('metalua.compiler', 'string', '?string')
|
||||
local r = self.parser.lexer :newstream (src, name)
|
||||
return r, name
|
||||
end
|
||||
|
||||
function CONV :lexstream_to_ast(lx, name)
|
||||
checks('metalua.compiler', 'lexer.stream', '?string')
|
||||
local r = self.parser.chunk(lx)
|
||||
r.source = name
|
||||
if M.check_ast then M.check_ast (r) end
|
||||
return r, name
|
||||
end
|
||||
|
||||
local bytecode_compiler = nil -- cache to avoid repeated `pcall(require(...))`
|
||||
local function get_bytecode_compiler()
|
||||
if bytecode_compiler then return bytecode_compiler else
|
||||
local status, result = pcall(require, 'metalua.compiler.bytecode')
|
||||
if status then
|
||||
bytecode_compiler = result
|
||||
return result
|
||||
elseif string.match(result, "not found") then
|
||||
error "Compilation only available with full Metalua"
|
||||
else error (result) end
|
||||
end
|
||||
end
|
||||
|
||||
function CONV :ast_to_proto(ast, name)
|
||||
checks('metalua.compiler', 'table', '?string')
|
||||
return get_bytecode_compiler().ast_to_proto(ast, name), name
|
||||
end
|
||||
|
||||
function CONV :proto_to_bytecode(proto, name)
|
||||
return get_bytecode_compiler().proto_to_bytecode(proto), name
|
||||
end
|
||||
|
||||
function CONV :bytecode_to_function(bc, name)
|
||||
checks('metalua.compiler', 'string', '?string')
|
||||
return loadstring(bc, name)
|
||||
end
|
||||
|
||||
-- Create all sensible combinations
|
||||
for i=1,#M.sequence do
|
||||
local src = M.sequence[i]
|
||||
for j=i+2, #M.sequence do
|
||||
local dst = M.sequence[j]
|
||||
local dst_name = src.."_to_"..dst
|
||||
local my_arg_types = arg_types[src]
|
||||
local functions = { }
|
||||
for k=i, j-1 do
|
||||
local name = M.sequence[k].."_to_"..M.sequence[k+1]
|
||||
local f = assert(CONV[name], name)
|
||||
table.insert (functions, f)
|
||||
end
|
||||
CONV[dst_name] = function(self, a, b)
|
||||
checks('metalua.compiler', unpack(my_arg_types))
|
||||
for _, f in ipairs(functions) do
|
||||
a, b = f(self, a, b)
|
||||
end
|
||||
return a, b
|
||||
end
|
||||
--printf("Created M.%s out of %s", dst_name, table.concat(n, ', '))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- This one goes in the "wrong" direction, cannot be composed.
|
||||
--------------------------------------------------------------------------------
|
||||
function CONV :function_to_bytecode(...) return string.dump(...) end
|
||||
|
||||
function CONV :ast_to_src(...)
|
||||
require 'metalua.loader' -- ast_to_string isn't written in plain lua
|
||||
return require 'metalua.compiler.ast_to_src' (...)
|
||||
end
|
||||
|
||||
local MT = { __index=CONV, __type='metalua.compiler' }
|
||||
|
||||
function M.new()
|
||||
local parser = require 'metalua.compiler.parser' .new()
|
||||
local self = { parser = parser }
|
||||
setmetatable(self, MT)
|
||||
return self
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,682 @@
|
||||
-------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2006-2013 Fabien Fleutot and others.
|
||||
--
|
||||
-- All rights reserved.
|
||||
--
|
||||
-- This program and the accompanying materials are made available
|
||||
-- under the terms of the Eclipse Public License v1.0 which
|
||||
-- accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- This program and the accompanying materials are also made available
|
||||
-- under the terms of the MIT public license which accompanies this
|
||||
-- distribution, and is available at http://www.lua.org/license.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Fabien Fleutot - API and implementation
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
-{ extension ('match', ...) }
|
||||
|
||||
local M = { }
|
||||
M.__index = M
|
||||
M.__call = |self, ...| self:run(...)
|
||||
|
||||
local pp=require 'metalua.pprint'
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Instanciate a new AST->source synthetizer
|
||||
--------------------------------------------------------------------------------
|
||||
function M.new ()
|
||||
local self = {
|
||||
_acc = { }, -- Accumulates pieces of source as strings
|
||||
current_indent = 0, -- Current level of line indentation
|
||||
indent_step = " " -- Indentation symbol, normally spaces or '\t'
|
||||
}
|
||||
return setmetatable (self, M)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Run a synthetizer on the `ast' arg and return the source as a string.
|
||||
-- Can also be used as a static method `M.run (ast)'; in this case,
|
||||
-- a temporary Metizer is instanciated on the fly.
|
||||
--------------------------------------------------------------------------------
|
||||
function M:run (ast)
|
||||
if not ast then
|
||||
self, ast = M.new(), self
|
||||
end
|
||||
self._acc = { }
|
||||
self:node (ast)
|
||||
return table.concat (self._acc)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Accumulate a piece of source file in the synthetizer.
|
||||
--------------------------------------------------------------------------------
|
||||
function M:acc (x)
|
||||
if x then table.insert (self._acc, x) end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Accumulate an indented newline.
|
||||
-- Jumps an extra line if indentation is 0, so that
|
||||
-- toplevel definitions are separated by an extra empty line.
|
||||
--------------------------------------------------------------------------------
|
||||
function M:nl ()
|
||||
if self.current_indent == 0 then self:acc "\n" end
|
||||
self:acc ("\n" .. self.indent_step:rep (self.current_indent))
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Increase indentation and accumulate a new line.
|
||||
--------------------------------------------------------------------------------
|
||||
function M:nlindent ()
|
||||
self.current_indent = self.current_indent + 1
|
||||
self:nl ()
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Decrease indentation and accumulate a new line.
|
||||
--------------------------------------------------------------------------------
|
||||
function M:nldedent ()
|
||||
self.current_indent = self.current_indent - 1
|
||||
self:acc ("\n" .. self.indent_step:rep (self.current_indent))
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Keywords, which are illegal as identifiers.
|
||||
--------------------------------------------------------------------------------
|
||||
local keywords_list = {
|
||||
"and", "break", "do", "else", "elseif",
|
||||
"end", "false", "for", "function", "if",
|
||||
"in", "local", "nil", "not", "or",
|
||||
"repeat", "return", "then", "true", "until",
|
||||
"while" }
|
||||
local keywords = { }
|
||||
for _, kw in pairs(keywords_list) do keywords[kw]=true end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Return true iff string `id' is a legal identifier name.
|
||||
--------------------------------------------------------------------------------
|
||||
local function is_ident (id)
|
||||
return string['match'](id, "^[%a_][%w_]*$") and not keywords[id]
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Return true iff ast represents a legal function name for
|
||||
-- syntax sugar ``function foo.bar.gnat() ... end'':
|
||||
-- a series of nested string indexes, with an identifier as
|
||||
-- the innermost node.
|
||||
--------------------------------------------------------------------------------
|
||||
local function is_idx_stack (ast)
|
||||
match ast with
|
||||
| `Id{ _ } -> return true
|
||||
| `Index{ left, `String{ _ } } -> return is_idx_stack (left)
|
||||
| _ -> return false
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Operator precedences, in increasing order.
|
||||
-- This is not directly used, it's used to generate op_prec below.
|
||||
--------------------------------------------------------------------------------
|
||||
local op_preprec = {
|
||||
{ "or", "and" },
|
||||
{ "lt", "le", "eq", "ne" },
|
||||
{ "concat" },
|
||||
{ "add", "sub" },
|
||||
{ "mul", "div", "mod" },
|
||||
{ "unary", "not", "len" },
|
||||
{ "pow" },
|
||||
{ "index" } }
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- operator --> precedence table, generated from op_preprec.
|
||||
--------------------------------------------------------------------------------
|
||||
local op_prec = { }
|
||||
|
||||
for prec, ops in ipairs (op_preprec) do
|
||||
for _, op in ipairs (ops) do
|
||||
op_prec[op] = prec
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- operator --> source representation.
|
||||
--------------------------------------------------------------------------------
|
||||
local op_symbol = {
|
||||
add = " + ", sub = " - ", mul = " * ",
|
||||
div = " / ", mod = " % ", pow = " ^ ",
|
||||
concat = " .. ", eq = " == ", ne = " ~= ",
|
||||
lt = " < ", le = " <= ", ["and"] = " and ",
|
||||
["or"] = " or ", ["not"] = "not ", len = "# " }
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Accumulate the source representation of AST `node' in
|
||||
-- the synthetizer. Most of the work is done by delegating to
|
||||
-- the method having the name of the AST tag.
|
||||
-- If something can't be converted to normal sources, it's
|
||||
-- instead dumped as a `-{ ... }' splice in the source accumulator.
|
||||
--------------------------------------------------------------------------------
|
||||
function M:node (node)
|
||||
assert (self~=M and self._acc)
|
||||
if node==nil then self:acc'<<error>>'
|
||||
elseif not self.custom_printer or not self.custom_printer (self, node) then
|
||||
if not node.tag then -- tagless (henceunindented) block.
|
||||
self:list (node, self.nl)
|
||||
else
|
||||
local f = M[node.tag]
|
||||
if type (f) == "function" then -- Delegate to tag method.
|
||||
f (self, node, unpack (node))
|
||||
elseif type (f) == "string" then -- tag string.
|
||||
self:acc (f)
|
||||
else -- No appropriate method, fall back to splice dumping.
|
||||
-- This cannot happen in a plain Lua AST.
|
||||
self:acc " -{ "
|
||||
self:acc (pp.tostring (node, {metalua_tag=1, hide_hash=1}), 80)
|
||||
self:acc " }"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M:block(body)
|
||||
if not self.custom_printer or not self.custom_printer (self, body) then
|
||||
self:nlindent ()
|
||||
self:list (body, self.nl)
|
||||
self:nldedent ()
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Convert every node in the AST list `list' passed as 1st arg.
|
||||
-- `sep' is an optional separator to be accumulated between each list element,
|
||||
-- it can be a string or a synth method.
|
||||
-- `start' is an optional number (default == 1), indicating which is the
|
||||
-- first element of list to be converted, so that we can skip the begining
|
||||
-- of a list.
|
||||
--------------------------------------------------------------------------------
|
||||
function M:list (list, sep, start)
|
||||
for i = start or 1, # list do
|
||||
self:node (list[i])
|
||||
if list[i + 1] then
|
||||
if not sep then
|
||||
elseif type (sep) == "function" then sep (self)
|
||||
elseif type (sep) == "string" then self:acc (sep)
|
||||
else error "Invalid list separator" end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
--
|
||||
-- Tag methods.
|
||||
-- ------------
|
||||
--
|
||||
-- Specific AST node dumping methods, associated to their node kinds
|
||||
-- by their name, which is the corresponding AST tag.
|
||||
-- synth:node() is in charge of delegating a node's treatment to the
|
||||
-- appropriate tag method.
|
||||
--
|
||||
-- Such tag methods are called with the AST node as 1st arg.
|
||||
-- As a convenience, the n node's children are passed as args #2 ... n+1.
|
||||
--
|
||||
-- There are several things that could be refactored into common subroutines
|
||||
-- here: statement blocks dumping, function dumping...
|
||||
-- However, given their small size and linear execution
|
||||
-- (they basically perform series of :acc(), :node(), :list(),
|
||||
-- :nl(), :nlindent() and :nldedent() calls), it seems more readable
|
||||
-- to avoid multiplication of such tiny functions.
|
||||
--
|
||||
-- To make sense out of these, you need to know metalua's AST syntax, as
|
||||
-- found in the reference manual or in metalua/doc/ast.txt.
|
||||
--
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
function M:Do (node)
|
||||
self:acc "do"
|
||||
self:block (node)
|
||||
self:acc "end"
|
||||
end
|
||||
|
||||
function M:Set (node)
|
||||
match node with
|
||||
| `Set{ { `Index{ lhs, `String{ method } } },
|
||||
{ `Function{ { `Id "self", ... } == params, body } } }
|
||||
if is_idx_stack (lhs) and is_ident (method) ->
|
||||
-- ``function foo:bar(...) ... end'' --
|
||||
self:acc "function "
|
||||
self:node (lhs)
|
||||
self:acc ":"
|
||||
self:acc (method)
|
||||
self:acc " ("
|
||||
self:list (params, ", ", 2)
|
||||
self:acc ")"
|
||||
self:block (body)
|
||||
self:acc "end"
|
||||
|
||||
| `Set{ { lhs }, { `Function{ params, body } } } if is_idx_stack (lhs) ->
|
||||
-- ``function foo(...) ... end'' --
|
||||
self:acc "function "
|
||||
self:node (lhs)
|
||||
self:acc " ("
|
||||
self:list (params, ", ")
|
||||
self:acc ")"
|
||||
self:block (body)
|
||||
self:acc "end"
|
||||
|
||||
| `Set{ { `Id{ lhs1name } == lhs1, ... } == lhs, rhs }
|
||||
if not is_ident (lhs1name) ->
|
||||
-- ``foo, ... = ...'' when foo is *not* a valid identifier.
|
||||
-- In that case, the spliced 1st variable must get parentheses,
|
||||
-- to be distinguished from a statement splice.
|
||||
-- This cannot happen in a plain Lua AST.
|
||||
self:acc "("
|
||||
self:node (lhs1)
|
||||
self:acc ")"
|
||||
if lhs[2] then -- more than one lhs variable
|
||||
self:acc ", "
|
||||
self:list (lhs, ", ", 2)
|
||||
end
|
||||
self:acc " = "
|
||||
self:list (rhs, ", ")
|
||||
|
||||
| `Set{ lhs, rhs } ->
|
||||
-- ``... = ...'', no syntax sugar --
|
||||
self:list (lhs, ", ")
|
||||
self:acc " = "
|
||||
self:list (rhs, ", ")
|
||||
| `Set{ lhs, rhs, annot } ->
|
||||
-- ``... = ...'', no syntax sugar, annotation --
|
||||
local n = #lhs
|
||||
for i=1,n do
|
||||
local ell, a = lhs[i], annot[i]
|
||||
self:node (ell)
|
||||
if a then
|
||||
self:acc ' #'
|
||||
self:node(a)
|
||||
end
|
||||
if i~=n then self:acc ', ' end
|
||||
end
|
||||
self:acc " = "
|
||||
self:list (rhs, ", ")
|
||||
end
|
||||
end
|
||||
|
||||
function M:While (node, cond, body)
|
||||
self:acc "while "
|
||||
self:node (cond)
|
||||
self:acc " do"
|
||||
self:block (body)
|
||||
self:acc "end"
|
||||
end
|
||||
|
||||
function M:Repeat (node, body, cond)
|
||||
self:acc "repeat"
|
||||
self:block (body)
|
||||
self:acc "until "
|
||||
self:node (cond)
|
||||
end
|
||||
|
||||
function M:If (node)
|
||||
for i = 1, #node-1, 2 do
|
||||
-- for each ``if/then'' and ``elseif/then'' pair --
|
||||
local cond, body = node[i], node[i+1]
|
||||
self:acc (i==1 and "if " or "elseif ")
|
||||
self:node (cond)
|
||||
self:acc " then"
|
||||
self:block (body)
|
||||
end
|
||||
-- odd number of children --> last one is an `else' clause --
|
||||
if #node%2 == 1 then
|
||||
self:acc "else"
|
||||
self:block (node[#node])
|
||||
end
|
||||
self:acc "end"
|
||||
end
|
||||
|
||||
function M:Fornum (node, var, first, last)
|
||||
local body = node[#node]
|
||||
self:acc "for "
|
||||
self:node (var)
|
||||
self:acc " = "
|
||||
self:node (first)
|
||||
self:acc ", "
|
||||
self:node (last)
|
||||
if #node==5 then -- 5 children --> child #4 is a step increment.
|
||||
self:acc ", "
|
||||
self:node (node[4])
|
||||
end
|
||||
self:acc " do"
|
||||
self:block (body)
|
||||
self:acc "end"
|
||||
end
|
||||
|
||||
function M:Forin (node, vars, generators, body)
|
||||
self:acc "for "
|
||||
self:list (vars, ", ")
|
||||
self:acc " in "
|
||||
self:list (generators, ", ")
|
||||
self:acc " do"
|
||||
self:block (body)
|
||||
self:acc "end"
|
||||
end
|
||||
|
||||
function M:Local (node, lhs, rhs, annots)
|
||||
if next (lhs) then
|
||||
self:acc "local "
|
||||
if annots then
|
||||
local n = #lhs
|
||||
for i=1, n do
|
||||
self:node (lhs)
|
||||
local a = annots[i]
|
||||
if a then
|
||||
self:acc ' #'
|
||||
self:node (a)
|
||||
end
|
||||
if i~=n then self:acc ', ' end
|
||||
end
|
||||
else
|
||||
self:list (lhs, ", ")
|
||||
end
|
||||
if rhs[1] then
|
||||
self:acc " = "
|
||||
self:list (rhs, ", ")
|
||||
end
|
||||
else -- Can't create a local statement with 0 variables in plain Lua
|
||||
self:acc (pp.tostring (node, {metalua_tag=1, hide_hash=1, fix_indent=2}))
|
||||
end
|
||||
end
|
||||
|
||||
function M:Localrec (node, lhs, rhs)
|
||||
match node with
|
||||
| `Localrec{ { `Id{name} }, { `Function{ params, body } } }
|
||||
if is_ident (name) ->
|
||||
-- ``local function name() ... end'' --
|
||||
self:acc "local function "
|
||||
self:acc (name)
|
||||
self:acc " ("
|
||||
self:list (params, ", ")
|
||||
self:acc ")"
|
||||
self:block (body)
|
||||
self:acc "end"
|
||||
|
||||
| _ ->
|
||||
-- Other localrec are unprintable ==> splice them --
|
||||
-- This cannot happen in a plain Lua AST. --
|
||||
self:acc "-{ "
|
||||
self:acc (pp.tostring (node, {metalua_tag=1, hide_hash=1, fix_indent=2}))
|
||||
self:acc " }"
|
||||
end
|
||||
end
|
||||
|
||||
function M:Call (node, f)
|
||||
-- single string or table literal arg ==> no need for parentheses. --
|
||||
local parens
|
||||
match node with
|
||||
| `Call{ _, `String{_} }
|
||||
| `Call{ _, `Table{...}} -> parens = false
|
||||
| _ -> parens = true
|
||||
end
|
||||
self:node (f)
|
||||
self:acc (parens and " (" or " ")
|
||||
self:list (node, ", ", 2) -- skip `f'.
|
||||
self:acc (parens and ")")
|
||||
end
|
||||
|
||||
function M:Invoke (node, f, method)
|
||||
-- single string or table literal arg ==> no need for parentheses. --
|
||||
local parens
|
||||
match node with
|
||||
| `Invoke{ _, _, `String{_} }
|
||||
| `Invoke{ _, _, `Table{...}} -> parens = false
|
||||
| _ -> parens = true
|
||||
end
|
||||
self:node (f)
|
||||
self:acc ":"
|
||||
self:acc (method[1])
|
||||
self:acc (parens and " (" or " ")
|
||||
self:list (node, ", ", 3) -- Skip args #1 and #2, object and method name.
|
||||
self:acc (parens and ")")
|
||||
end
|
||||
|
||||
function M:Return (node)
|
||||
self:acc "return "
|
||||
self:list (node, ", ")
|
||||
end
|
||||
|
||||
M.Break = "break"
|
||||
M.Nil = "nil"
|
||||
M.False = "false"
|
||||
M.True = "true"
|
||||
M.Dots = "..."
|
||||
|
||||
function M:Number (node, n)
|
||||
self:acc (tostring (n))
|
||||
end
|
||||
|
||||
function M:String (node, str)
|
||||
-- format "%q" prints '\n' in an umpractical way IMO,
|
||||
-- so this is fixed with the :gsub( ) call.
|
||||
self:acc (string.format ("%q", str):gsub ("\\\n", "\\n"))
|
||||
end
|
||||
|
||||
function M:Function (node, params, body, annots)
|
||||
self:acc "function ("
|
||||
if annots then
|
||||
local n = #params
|
||||
for i=1,n do
|
||||
local p, a = params[i], annots[i]
|
||||
self:node(p)
|
||||
if annots then
|
||||
self:acc " #"
|
||||
self:node(a)
|
||||
end
|
||||
if i~=n then self:acc ', ' end
|
||||
end
|
||||
else
|
||||
self:list (params, ", ")
|
||||
end
|
||||
self:acc ")"
|
||||
self:block (body)
|
||||
self:acc "end"
|
||||
end
|
||||
|
||||
function M:Table (node)
|
||||
if not node[1] then self:acc "{ }" else
|
||||
self:acc "{"
|
||||
if #node > 1 then self:nlindent () else self:acc " " end
|
||||
for i, elem in ipairs (node) do
|
||||
match elem with
|
||||
| `Pair{ `String{ key }, value } if is_ident (key) ->
|
||||
-- ``key = value''. --
|
||||
self:acc (key)
|
||||
self:acc " = "
|
||||
self:node (value)
|
||||
|
||||
| `Pair{ key, value } ->
|
||||
-- ``[key] = value''. --
|
||||
self:acc "["
|
||||
self:node (key)
|
||||
self:acc "] = "
|
||||
self:node (value)
|
||||
|
||||
| _ ->
|
||||
-- ``value''. --
|
||||
self:node (elem)
|
||||
end
|
||||
if node [i+1] then
|
||||
self:acc ","
|
||||
self:nl ()
|
||||
end
|
||||
end
|
||||
if #node > 1 then self:nldedent () else self:acc " " end
|
||||
self:acc "}"
|
||||
end
|
||||
end
|
||||
|
||||
function M:Op (node, op, a, b)
|
||||
-- Transform ``not (a == b)'' into ``a ~= b''. --
|
||||
match node with
|
||||
| `Op{ "not", `Op{ "eq", _a, _b } }
|
||||
| `Op{ "not", `Paren{ `Op{ "eq", _a, _b } } } ->
|
||||
op, a, b = "ne", _a, _b
|
||||
| _ ->
|
||||
end
|
||||
|
||||
if b then -- binary operator.
|
||||
local left_paren, right_paren
|
||||
match a with
|
||||
| `Op{ op_a, ...} if op_prec[op] >= op_prec[op_a] -> left_paren = true
|
||||
| _ -> left_paren = false
|
||||
end
|
||||
|
||||
match b with -- FIXME: might not work with right assoc operators ^ and ..
|
||||
| `Op{ op_b, ...} if op_prec[op] >= op_prec[op_b] -> right_paren = true
|
||||
| _ -> right_paren = false
|
||||
end
|
||||
|
||||
self:acc (left_paren and "(")
|
||||
self:node (a)
|
||||
self:acc (left_paren and ")")
|
||||
|
||||
self:acc (op_symbol [op])
|
||||
|
||||
self:acc (right_paren and "(")
|
||||
self:node (b)
|
||||
self:acc (right_paren and ")")
|
||||
|
||||
else -- unary operator.
|
||||
local paren
|
||||
match a with
|
||||
| `Op{ op_a, ... } if op_prec[op] >= op_prec[op_a] -> paren = true
|
||||
| _ -> paren = false
|
||||
end
|
||||
self:acc (op_symbol[op])
|
||||
self:acc (paren and "(")
|
||||
self:node (a)
|
||||
self:acc (paren and ")")
|
||||
end
|
||||
end
|
||||
|
||||
function M:Paren (node, content)
|
||||
self:acc "("
|
||||
self:node (content)
|
||||
self:acc ")"
|
||||
end
|
||||
|
||||
function M:Index (node, table, key)
|
||||
local paren_table
|
||||
-- Check precedence, see if parens are needed around the table --
|
||||
match table with
|
||||
| `Op{ op, ... } if op_prec[op] < op_prec.index -> paren_table = true
|
||||
| _ -> paren_table = false
|
||||
end
|
||||
|
||||
self:acc (paren_table and "(")
|
||||
self:node (table)
|
||||
self:acc (paren_table and ")")
|
||||
|
||||
match key with
|
||||
| `String{ field } if is_ident (field) ->
|
||||
-- ``table.key''. --
|
||||
self:acc "."
|
||||
self:acc (field)
|
||||
| _ ->
|
||||
-- ``table [key]''. --
|
||||
self:acc "["
|
||||
self:node (key)
|
||||
self:acc "]"
|
||||
end
|
||||
end
|
||||
|
||||
function M:Id (node, name)
|
||||
if is_ident (name) then
|
||||
self:acc (name)
|
||||
else -- Unprintable identifier, fall back to splice representation.
|
||||
-- This cannot happen in a plain Lua AST.
|
||||
self:acc "-{`Id "
|
||||
self:String (node, name)
|
||||
self:acc "}"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
M.TDyn = '*'
|
||||
M.TDynbar = '**'
|
||||
M.TPass = 'pass'
|
||||
M.TField = 'field'
|
||||
M.TIdbar = M.TId
|
||||
M.TReturn = M.Return
|
||||
|
||||
|
||||
function M:TId (node, name) self:acc(name) end
|
||||
|
||||
|
||||
function M:TCatbar(node, te, tebar)
|
||||
self:acc'('
|
||||
self:node(te)
|
||||
self:acc'|'
|
||||
self:tebar(tebar)
|
||||
self:acc')'
|
||||
end
|
||||
|
||||
function M:TFunction(node, p, r)
|
||||
self:tebar(p)
|
||||
self:acc '->'
|
||||
self:tebar(r)
|
||||
end
|
||||
|
||||
function M:TTable (node, default, pairs)
|
||||
self:acc '['
|
||||
self:list (pairs, ', ')
|
||||
if default.tag~='TField' then
|
||||
self:acc '|'
|
||||
self:node (default)
|
||||
end
|
||||
self:acc ']'
|
||||
end
|
||||
|
||||
function M:TPair (node, k, v)
|
||||
self:node (k)
|
||||
self:acc '='
|
||||
self:node (v)
|
||||
end
|
||||
|
||||
function M:TIdbar (node, name)
|
||||
self :acc (name)
|
||||
end
|
||||
|
||||
function M:TCatbar (node, a, b)
|
||||
self:node(a)
|
||||
self:acc ' ++ '
|
||||
self:node(b)
|
||||
end
|
||||
|
||||
function M:tebar(node)
|
||||
if node.tag then self:node(node) else
|
||||
self:acc '('
|
||||
self:list(node, ', ')
|
||||
self:acc ')'
|
||||
end
|
||||
end
|
||||
|
||||
function M:TUnkbar(node, name)
|
||||
self:acc '~~'
|
||||
self:acc (name)
|
||||
end
|
||||
|
||||
function M:TUnk(node, name)
|
||||
self:acc '~'
|
||||
self:acc (name)
|
||||
end
|
||||
|
||||
for name, tag in pairs{ const='TConst', var='TVar', currently='TCurrently', just='TJust' } do
|
||||
M[tag] = function(self, node, te)
|
||||
self:acc (name..' ')
|
||||
self:node(te)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,29 @@
|
||||
--------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2006-2013 Fabien Fleutot and others.
|
||||
--
|
||||
-- All rights reserved.
|
||||
--
|
||||
-- This program and the accompanying materials are made available
|
||||
-- under the terms of the Eclipse Public License v1.0 which
|
||||
-- accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- This program and the accompanying materials are also made available
|
||||
-- under the terms of the MIT public license which accompanies this
|
||||
-- distribution, and is available at http://www.lua.org/license.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Fabien Fleutot - API and implementation
|
||||
--
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local compile = require 'metalua.compiler.bytecode.compile'
|
||||
local ldump = require 'metalua.compiler.bytecode.ldump'
|
||||
|
||||
local M = { }
|
||||
|
||||
M.ast_to_proto = compile.ast_to_proto
|
||||
M.proto_to_bytecode = ldump.dump_string
|
||||
M.proto_to_file = ldump.dump_file
|
||||
|
||||
return M
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,448 @@
|
||||
-------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2005-2013 Kein-Hong Man, Fabien Fleutot and others.
|
||||
--
|
||||
-- All rights reserved.
|
||||
--
|
||||
-- This program and the accompanying materials are made available
|
||||
-- under the terms of the Eclipse Public License v1.0 which
|
||||
-- accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- This program and the accompanying materials are also made available
|
||||
-- under the terms of the MIT public license which accompanies this
|
||||
-- distribution, and is available at http://www.lua.org/license.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Kein-Hong Man - Initial implementation for Lua 5.0, part of Yueliang
|
||||
-- Fabien Fleutot - Port to Lua 5.1, integration with Metalua
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
--[[--------------------------------------------------------------------
|
||||
|
||||
ldump.lua
|
||||
Save bytecodes in Lua
|
||||
This file is part of Yueliang.
|
||||
|
||||
Copyright (c) 2005 Kein-Hong Man <khman@users.sf.net>
|
||||
The COPYRIGHT file describes the conditions
|
||||
under which this software may be distributed.
|
||||
|
||||
------------------------------------------------------------------------
|
||||
|
||||
[FF] Slightly modified, mainly to produce Lua 5.1 bytecode.
|
||||
|
||||
----------------------------------------------------------------------]]
|
||||
|
||||
--[[--------------------------------------------------------------------
|
||||
-- Notes:
|
||||
-- * LUA_NUMBER (double), byte order (little endian) and some other
|
||||
-- header values hard-coded; see other notes below...
|
||||
-- * One significant difference is that instructions are still in table
|
||||
-- form (with OP/A/B/C/Bx fields) and luaP:Instruction() is needed to
|
||||
-- convert them into 4-char strings
|
||||
-- * Deleted:
|
||||
-- luaU:DumpVector: folded into DumpLines, DumpCode
|
||||
-- * Added:
|
||||
-- luaU:endianness() (from lundump.c)
|
||||
-- luaU:make_setS: create a chunk writer that writes to a string
|
||||
-- luaU:make_setF: create a chunk writer that writes to a file
|
||||
-- (lua.h contains a typedef for a Chunkwriter pointer, and
|
||||
-- a Lua-based implementation exists, writer() in lstrlib.c)
|
||||
-- luaU:from_double(x): encode double value for writing
|
||||
-- luaU:from_int(x): encode integer value for writing
|
||||
-- (error checking is limited for these conversion functions)
|
||||
-- (double conversion does not support denormals or NaNs)
|
||||
-- luaU:ttype(o) (from lobject.h)
|
||||
----------------------------------------------------------------------]]
|
||||
|
||||
local luaP = require 'metalua.compiler.bytecode.lopcodes'
|
||||
|
||||
local M = { }
|
||||
|
||||
local format = { }
|
||||
format.header = string.dump(function()end):sub(1, 12)
|
||||
format.little_endian, format.int_size,
|
||||
format.size_t_size, format.instr_size,
|
||||
format.number_size, format.integral = format.header:byte(7, 12)
|
||||
format.little_endian = format.little_endian~=0
|
||||
format.integral = format.integral ~=0
|
||||
|
||||
assert(format.integral or format.number_size==8, "Number format not supported by dumper")
|
||||
assert(format.little_endian, "Big endian architectures not supported by dumper")
|
||||
|
||||
--requires luaP
|
||||
local luaU = { }
|
||||
M.luaU = luaU
|
||||
|
||||
luaU.format = format
|
||||
|
||||
-- constants used by dumper
|
||||
luaU.LUA_TNIL = 0
|
||||
luaU.LUA_TBOOLEAN = 1
|
||||
luaU.LUA_TNUMBER = 3 -- (all in lua.h)
|
||||
luaU.LUA_TSTRING = 4
|
||||
luaU.LUA_TNONE = -1
|
||||
|
||||
-- definitions for headers of binary files
|
||||
--luaU.LUA_SIGNATURE = "\27Lua" -- binary files start with "<esc>Lua"
|
||||
--luaU.VERSION = 81 -- 0x50; last format change was in 5.0
|
||||
--luaU.FORMAT_VERSION = 0 -- 0 is official version. yeah I know I'm a liar.
|
||||
|
||||
-- a multiple of PI for testing native format
|
||||
-- multiplying by 1E7 gives non-trivial integer values
|
||||
--luaU.TEST_NUMBER = 3.14159265358979323846E7
|
||||
|
||||
--[[--------------------------------------------------------------------
|
||||
-- Additional functions to handle chunk writing
|
||||
-- * to use make_setS and make_setF, see test_ldump.lua elsewhere
|
||||
----------------------------------------------------------------------]]
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- works like the lobject.h version except that TObject used in these
|
||||
-- scripts only has a 'value' field, no 'tt' field (native types used)
|
||||
------------------------------------------------------------------------
|
||||
function luaU:ttype(o)
|
||||
local tt = type(o.value)
|
||||
if tt == "number" then return self.LUA_TNUMBER
|
||||
elseif tt == "string" then return self.LUA_TSTRING
|
||||
elseif tt == "nil" then return self.LUA_TNIL
|
||||
elseif tt == "boolean" then return self.LUA_TBOOLEAN
|
||||
else
|
||||
return self.LUA_TNONE -- the rest should not appear
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- create a chunk writer that writes to a string
|
||||
-- * returns the writer function and a table containing the string
|
||||
-- * to get the final result, look in buff.data
|
||||
------------------------------------------------------------------------
|
||||
function luaU:make_setS()
|
||||
local buff = {}
|
||||
buff.data = ""
|
||||
local writer =
|
||||
function(s, buff) -- chunk writer
|
||||
if not s then return end
|
||||
buff.data = buff.data..s
|
||||
end
|
||||
return writer, buff
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- create a chunk writer that writes to a file
|
||||
-- * returns the writer function and a table containing the file handle
|
||||
-- * if a nil is passed, then writer should close the open file
|
||||
------------------------------------------------------------------------
|
||||
function luaU:make_setF(filename)
|
||||
local buff = {}
|
||||
buff.h = io.open(filename, "wb")
|
||||
if not buff.h then return nil end
|
||||
local writer =
|
||||
function(s, buff) -- chunk writer
|
||||
if not buff.h then return end
|
||||
if not s then buff.h:close(); return end
|
||||
buff.h:write(s)
|
||||
end
|
||||
return writer, buff
|
||||
end
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- converts a IEEE754 double number to an 8-byte little-endian string
|
||||
-- * luaU:from_double() and luaU:from_int() are from ChunkBake project
|
||||
-- * supports +/- Infinity, but not denormals or NaNs
|
||||
-----------------------------------------------------------------------
|
||||
function luaU:from_double(x)
|
||||
local function grab_byte(v)
|
||||
return math.floor(v / 256),
|
||||
string.char(math.mod(math.floor(v), 256))
|
||||
end
|
||||
local sign = 0
|
||||
if x < 0 then sign = 1; x = -x end
|
||||
local mantissa, exponent = math.frexp(x)
|
||||
if x == 0 then -- zero
|
||||
mantissa, exponent = 0, 0
|
||||
elseif x == 1/0 then
|
||||
mantissa, exponent = 0, 2047
|
||||
else
|
||||
mantissa = (mantissa * 2 - 1) * math.ldexp(0.5, 53)
|
||||
exponent = exponent + 1022
|
||||
end
|
||||
local v, byte = "" -- convert to bytes
|
||||
x = mantissa
|
||||
for i = 1,6 do
|
||||
x, byte = grab_byte(x); v = v..byte -- 47:0
|
||||
end
|
||||
x, byte = grab_byte(exponent * 16 + x); v = v..byte -- 55:48
|
||||
x, byte = grab_byte(sign * 128 + x); v = v..byte -- 63:56
|
||||
return v
|
||||
end
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- converts a number to a little-endian 32-bit integer string
|
||||
-- * input value assumed to not overflow, can be signed/unsigned
|
||||
-----------------------------------------------------------------------
|
||||
function luaU:from_int(x, size)
|
||||
local v = ""
|
||||
x = math.floor(x)
|
||||
if x >= 0 then
|
||||
for i = 1, size do
|
||||
v = v..string.char(math.mod(x, 256)); x = math.floor(x / 256)
|
||||
end
|
||||
else -- x < 0
|
||||
x = -x
|
||||
local carry = 1
|
||||
for i = 1, size do
|
||||
local c = 255 - math.mod(x, 256) + carry
|
||||
if c == 256 then c = 0; carry = 1 else carry = 0 end
|
||||
v = v..string.char(c); x = math.floor(x / 256)
|
||||
end
|
||||
end
|
||||
return v
|
||||
end
|
||||
|
||||
--[[--------------------------------------------------------------------
|
||||
-- Functions to make a binary chunk
|
||||
-- * many functions have the size parameter removed, since output is
|
||||
-- in the form of a string and some sizes are implicit or hard-coded
|
||||
-- * luaU:DumpVector has been deleted (used in DumpCode & DumpLines)
|
||||
----------------------------------------------------------------------]]
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- dump a block of literal bytes
|
||||
------------------------------------------------------------------------
|
||||
function luaU:DumpLiteral(s, D) self:DumpBlock(s, D) end
|
||||
|
||||
--[[--------------------------------------------------------------------
|
||||
-- struct DumpState:
|
||||
-- L -- lua_State (not used in this script)
|
||||
-- write -- lua_Chunkwriter (chunk writer function)
|
||||
-- data -- void* (chunk writer context or data already written)
|
||||
----------------------------------------------------------------------]]
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- dumps a block of bytes
|
||||
-- * lua_unlock(D.L), lua_lock(D.L) deleted
|
||||
------------------------------------------------------------------------
|
||||
function luaU:DumpBlock(b, D) D.write(b, D.data) end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- dumps a single byte
|
||||
------------------------------------------------------------------------
|
||||
function luaU:DumpByte(y, D)
|
||||
self:DumpBlock(string.char(y), D)
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- dumps a signed integer of size `format.int_size` (for int)
|
||||
------------------------------------------------------------------------
|
||||
function luaU:DumpInt(x, D)
|
||||
self:DumpBlock(self:from_int(x, format.int_size), D)
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- dumps an unsigned integer of size `format.size_t_size` (for size_t)
|
||||
------------------------------------------------------------------------
|
||||
function luaU:DumpSize(x, D)
|
||||
self:DumpBlock(self:from_int(x, format.size_t_size), D)
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- dumps a LUA_NUMBER; can be an int or double depending on the VM.
|
||||
------------------------------------------------------------------------
|
||||
function luaU:DumpNumber(x, D)
|
||||
if format.integral then
|
||||
self:DumpBlock(self:from_int(x, format.number_size), D)
|
||||
else
|
||||
self:DumpBlock(self:from_double(x), D)
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- dumps a Lua string
|
||||
------------------------------------------------------------------------
|
||||
function luaU:DumpString(s, D)
|
||||
if s == nil then
|
||||
self:DumpSize(0, D)
|
||||
else
|
||||
s = s.."\0" -- include trailing '\0'
|
||||
self:DumpSize(string.len(s), D)
|
||||
self:DumpBlock(s, D)
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- dumps instruction block from function prototype
|
||||
------------------------------------------------------------------------
|
||||
function luaU:DumpCode(f, D)
|
||||
local n = f.sizecode
|
||||
self:DumpInt(n, D)
|
||||
--was DumpVector
|
||||
for i = 0, n - 1 do
|
||||
self:DumpBlock(luaP:Instruction(f.code[i]), D)
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- dumps local variable names from function prototype
|
||||
------------------------------------------------------------------------
|
||||
function luaU:DumpLocals(f, D)
|
||||
local n = f.sizelocvars
|
||||
self:DumpInt(n, D)
|
||||
for i = 0, n - 1 do
|
||||
-- Dirty temporary fix:
|
||||
-- `Stat{ } keeps properly count of the number of local vars,
|
||||
-- but fails to keep score of their debug info (names).
|
||||
-- It therefore might happen that #f.localvars < f.sizelocvars, or
|
||||
-- that a variable's startpc and endpc fields are left unset.
|
||||
-- FIXME: This might not be needed anymore, check the bug report
|
||||
-- by J. Belmonte.
|
||||
local var = f.locvars[i]
|
||||
if not var then break end
|
||||
-- printf("[DUMPLOCALS] dumping local var #%i = %s", i, table.tostring(var))
|
||||
self:DumpString(var.varname, D)
|
||||
self:DumpInt(var.startpc or 0, D)
|
||||
self:DumpInt(var.endpc or 0, D)
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- dumps line information from function prototype
|
||||
------------------------------------------------------------------------
|
||||
function luaU:DumpLines(f, D)
|
||||
local n = f.sizelineinfo
|
||||
self:DumpInt(n, D)
|
||||
--was DumpVector
|
||||
for i = 0, n - 1 do
|
||||
self:DumpInt(f.lineinfo[i], D) -- was DumpBlock
|
||||
--print(i, f.lineinfo[i])
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- dump upvalue names from function prototype
|
||||
------------------------------------------------------------------------
|
||||
function luaU:DumpUpvalues(f, D)
|
||||
local n = f.sizeupvalues
|
||||
self:DumpInt(n, D)
|
||||
for i = 0, n - 1 do
|
||||
self:DumpString(f.upvalues[i], D)
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- dump constant pool from function prototype
|
||||
-- * nvalue(o) and tsvalue(o) macros removed
|
||||
------------------------------------------------------------------------
|
||||
function luaU:DumpConstants(f, D)
|
||||
local n = f.sizek
|
||||
self:DumpInt(n, D)
|
||||
for i = 0, n - 1 do
|
||||
local o = f.k[i] -- TObject
|
||||
local tt = self:ttype(o)
|
||||
assert (tt >= 0)
|
||||
self:DumpByte(tt, D)
|
||||
if tt == self.LUA_TNUMBER then
|
||||
self:DumpNumber(o.value, D)
|
||||
elseif tt == self.LUA_TSTRING then
|
||||
self:DumpString(o.value, D)
|
||||
elseif tt == self.LUA_TBOOLEAN then
|
||||
self:DumpByte (o.value and 1 or 0, D)
|
||||
elseif tt == self.LUA_TNIL then
|
||||
else
|
||||
assert(false) -- cannot happen
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function luaU:DumpProtos (f, D)
|
||||
local n = f.sizep
|
||||
assert (n)
|
||||
self:DumpInt(n, D)
|
||||
for i = 0, n - 1 do
|
||||
self:DumpFunction(f.p[i], f.source, D)
|
||||
end
|
||||
end
|
||||
|
||||
function luaU:DumpDebug(f, D)
|
||||
self:DumpLines(f, D)
|
||||
self:DumpLocals(f, D)
|
||||
self:DumpUpvalues(f, D)
|
||||
end
|
||||
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- dump child function prototypes from function prototype
|
||||
--FF completely reworked for 5.1 format
|
||||
------------------------------------------------------------------------
|
||||
function luaU:DumpFunction(f, p, D)
|
||||
-- print "Dumping function:"
|
||||
-- table.print(f, 60)
|
||||
|
||||
local source = f.source
|
||||
if source == p then source = nil end
|
||||
self:DumpString(source, D)
|
||||
self:DumpInt(f.lineDefined, D)
|
||||
self:DumpInt(f.lastLineDefined or 42, D)
|
||||
self:DumpByte(f.nups, D)
|
||||
self:DumpByte(f.numparams, D)
|
||||
self:DumpByte(f.is_vararg, D)
|
||||
self:DumpByte(f.maxstacksize, D)
|
||||
self:DumpCode(f, D)
|
||||
self:DumpConstants(f, D)
|
||||
self:DumpProtos( f, D)
|
||||
self:DumpDebug(f, D)
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- dump Lua header section (some sizes hard-coded)
|
||||
--FF: updated for version 5.1
|
||||
------------------------------------------------------------------------
|
||||
function luaU:DumpHeader(D)
|
||||
self:DumpLiteral(format.header, D)
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- dump function as precompiled chunk
|
||||
-- * w, data are created from make_setS, make_setF
|
||||
--FF: suppressed extraneous [L] param
|
||||
------------------------------------------------------------------------
|
||||
function luaU:dump (Main, w, data)
|
||||
local D = {} -- DumpState
|
||||
D.write = w
|
||||
D.data = data
|
||||
self:DumpHeader(D)
|
||||
self:DumpFunction(Main, nil, D)
|
||||
-- added: for a chunk writer writing to a file, this final call with
|
||||
-- nil data is to indicate to the writer to close the file
|
||||
D.write(nil, D.data)
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- find byte order (from lundump.c)
|
||||
-- * hard-coded to little-endian
|
||||
------------------------------------------------------------------------
|
||||
function luaU:endianness()
|
||||
return 1
|
||||
end
|
||||
|
||||
-- FIXME: ugly concat-base generation in [make_setS], bufferize properly!
|
||||
function M.dump_string (proto)
|
||||
local writer, buff = luaU:make_setS()
|
||||
luaU:dump (proto, writer, buff)
|
||||
return buff.data
|
||||
end
|
||||
|
||||
-- FIXME: [make_setS] sucks, perform synchronous file writing
|
||||
-- Now unused
|
||||
function M.dump_file (proto, filename)
|
||||
local writer, buff = luaU:make_setS()
|
||||
luaU:dump (proto, writer, buff)
|
||||
local file = io.open (filename, "wb")
|
||||
file:write (buff.data)
|
||||
io.close(file)
|
||||
--if UNIX_SHARPBANG then os.execute ("chmod a+x "..filename) end
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,442 @@
|
||||
-------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2005-2013 Kein-Hong Man, Fabien Fleutot and others.
|
||||
--
|
||||
-- All rights reserved.
|
||||
--
|
||||
-- This program and the accompanying materials are made available
|
||||
-- under the terms of the Eclipse Public License v1.0 which
|
||||
-- accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- This program and the accompanying materials are also made available
|
||||
-- under the terms of the MIT public license which accompanies this
|
||||
-- distribution, and is available at http://www.lua.org/license.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Kein-Hong Man - Initial implementation for Lua 5.0, part of Yueliang
|
||||
-- Fabien Fleutot - Port to Lua 5.1, integration with Metalua
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
--[[--------------------------------------------------------------------
|
||||
|
||||
$Id$
|
||||
|
||||
lopcodes.lua
|
||||
Lua 5 virtual machine opcodes in Lua
|
||||
This file is part of Yueliang.
|
||||
|
||||
Copyright (c) 2005 Kein-Hong Man <khman@users.sf.net>
|
||||
The COPYRIGHT file describes the conditions
|
||||
under which this software may be distributed.
|
||||
|
||||
See the ChangeLog for more information.
|
||||
|
||||
------------------------------------------------------------------------
|
||||
|
||||
[FF] Slightly modified, mainly to produce Lua 5.1 bytecode.
|
||||
|
||||
----------------------------------------------------------------------]]
|
||||
|
||||
--[[--------------------------------------------------------------------
|
||||
-- Notes:
|
||||
-- * an Instruction is a table with OP, A, B, C, Bx elements; this
|
||||
-- should allow instruction handling to work with doubles and ints
|
||||
-- * Added:
|
||||
-- luaP:Instruction(i): convert field elements to a 4-char string
|
||||
-- luaP:DecodeInst(x): convert 4-char string into field elements
|
||||
-- * WARNING luaP:Instruction outputs instructions encoded in little-
|
||||
-- endian form and field size and positions are hard-coded
|
||||
----------------------------------------------------------------------]]
|
||||
|
||||
local function debugf() end
|
||||
|
||||
local luaP = { }
|
||||
|
||||
--[[
|
||||
===========================================================================
|
||||
We assume that instructions are unsigned numbers.
|
||||
All instructions have an opcode in the first 6 bits.
|
||||
Instructions can have the following fields:
|
||||
'A' : 8 bits
|
||||
'B' : 9 bits
|
||||
'C' : 9 bits
|
||||
'Bx' : 18 bits ('B' and 'C' together)
|
||||
'sBx' : signed Bx
|
||||
|
||||
A signed argument is represented in excess K; that is, the number
|
||||
value is the unsigned value minus K. K is exactly the maximum value
|
||||
for that argument (so that -max is represented by 0, and +max is
|
||||
represented by 2*max), which is half the maximum for the corresponding
|
||||
unsigned argument.
|
||||
===========================================================================
|
||||
--]]
|
||||
|
||||
luaP.OpMode = {"iABC", "iABx", "iAsBx"} -- basic instruction format
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- size and position of opcode arguments.
|
||||
-- * WARNING size and position is hard-coded elsewhere in this script
|
||||
------------------------------------------------------------------------
|
||||
luaP.SIZE_C = 9
|
||||
luaP.SIZE_B = 9
|
||||
luaP.SIZE_Bx = luaP.SIZE_C + luaP.SIZE_B
|
||||
luaP.SIZE_A = 8
|
||||
|
||||
luaP.SIZE_OP = 6
|
||||
|
||||
luaP.POS_C = luaP.SIZE_OP
|
||||
luaP.POS_B = luaP.POS_C + luaP.SIZE_C
|
||||
luaP.POS_Bx = luaP.POS_C
|
||||
luaP.POS_A = luaP.POS_B + luaP.SIZE_B
|
||||
|
||||
--FF from 5.1
|
||||
luaP.BITRK = 2^(luaP.SIZE_B - 1)
|
||||
function luaP:ISK(x) return x >= self.BITRK end
|
||||
luaP.MAXINDEXRK = luaP.BITRK - 1
|
||||
function luaP:RKASK(x)
|
||||
if x < self.BITRK then return x+self.BITRK else return x end
|
||||
end
|
||||
|
||||
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- limits for opcode arguments.
|
||||
-- we use (signed) int to manipulate most arguments,
|
||||
-- so they must fit in BITS_INT-1 bits (-1 for sign)
|
||||
------------------------------------------------------------------------
|
||||
-- removed "#if SIZE_Bx < BITS_INT-1" test, assume this script is
|
||||
-- running on a Lua VM with double or int as LUA_NUMBER
|
||||
|
||||
luaP.MAXARG_Bx = math.ldexp(1, luaP.SIZE_Bx) - 1
|
||||
luaP.MAXARG_sBx = math.floor(luaP.MAXARG_Bx / 2) -- 'sBx' is signed
|
||||
|
||||
luaP.MAXARG_A = math.ldexp(1, luaP.SIZE_A) - 1
|
||||
luaP.MAXARG_B = math.ldexp(1, luaP.SIZE_B) - 1
|
||||
luaP.MAXARG_C = math.ldexp(1, luaP.SIZE_C) - 1
|
||||
|
||||
-- creates a mask with 'n' 1 bits at position 'p'
|
||||
-- MASK1(n,p) deleted
|
||||
-- creates a mask with 'n' 0 bits at position 'p'
|
||||
-- MASK0(n,p) deleted
|
||||
|
||||
--[[--------------------------------------------------------------------
|
||||
Visual representation for reference:
|
||||
|
||||
31 | | | 0 bit position
|
||||
+-----+-----+-----+----------+
|
||||
| B | C | A | Opcode | iABC format
|
||||
+-----+-----+-----+----------+
|
||||
- 9 - 9 - 8 - 6 - field sizes
|
||||
+-----+-----+-----+----------+
|
||||
| [s]Bx | A | Opcode | iABx | iAsBx format
|
||||
+-----+-----+-----+----------+
|
||||
----------------------------------------------------------------------]]
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- the following macros help to manipulate instructions
|
||||
-- * changed to a table object representation, very clean compared to
|
||||
-- the [nightmare] alternatives of using a number or a string
|
||||
------------------------------------------------------------------------
|
||||
|
||||
-- these accept or return opcodes in the form of string names
|
||||
function luaP:GET_OPCODE(i) return self.ROpCode[i.OP] end
|
||||
function luaP:SET_OPCODE(i, o) i.OP = self.OpCode[o] end
|
||||
|
||||
function luaP:GETARG_A(i) return i.A end
|
||||
function luaP:SETARG_A(i, u) i.A = u end
|
||||
|
||||
function luaP:GETARG_B(i) return i.B end
|
||||
function luaP:SETARG_B(i, b) i.B = b end
|
||||
|
||||
function luaP:GETARG_C(i) return i.C end
|
||||
function luaP:SETARG_C(i, b) i.C = b end
|
||||
|
||||
function luaP:GETARG_Bx(i) return i.Bx end
|
||||
function luaP:SETARG_Bx(i, b) i.Bx = b end
|
||||
|
||||
function luaP:GETARG_sBx(i) return i.Bx - self.MAXARG_sBx end
|
||||
function luaP:SETARG_sBx(i, b) i.Bx = b + self.MAXARG_sBx end
|
||||
|
||||
function luaP:CREATE_ABC(o,a,b,c)
|
||||
return {OP = self.OpCode[o], A = a, B = b, C = c}
|
||||
end
|
||||
|
||||
function luaP:CREATE_ABx(o,a,bc)
|
||||
return {OP = self.OpCode[o], A = a, Bx = bc}
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- Bit shuffling stuffs
|
||||
------------------------------------------------------------------------
|
||||
|
||||
if false and pcall (require, 'bit') then
|
||||
------------------------------------------------------------------------
|
||||
-- Return a 4-char string little-endian encoded form of an instruction
|
||||
------------------------------------------------------------------------
|
||||
function luaP:Instruction(i)
|
||||
--FIXME
|
||||
end
|
||||
else
|
||||
------------------------------------------------------------------------
|
||||
-- Version without bit manipulation library.
|
||||
------------------------------------------------------------------------
|
||||
local p2 = {1,2,4,8,16,32,64,128,256, 512, 1024, 2048, 4096}
|
||||
-- keeps [n] bits from [x]
|
||||
local function keep (x, n) return x % p2[n+1] end
|
||||
-- shifts bits of [x] [n] places to the right
|
||||
local function srb (x,n) return math.floor (x / p2[n+1]) end
|
||||
-- shifts bits of [x] [n] places to the left
|
||||
local function slb (x,n) return x * p2[n+1] end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- Return a 4-char string little-endian encoded form of an instruction
|
||||
------------------------------------------------------------------------
|
||||
function luaP:Instruction(i)
|
||||
-- printf("Instr->string: %s %s", self.opnames[i.OP], table.tostring(i))
|
||||
local c0, c1, c2, c3
|
||||
-- change to OP/A/B/C format if needed
|
||||
if i.Bx then i.C = keep (i.Bx, 9); i.B = srb (i.Bx, 9) end
|
||||
-- c0 = 6B from opcode + 2LSB from A (flushed to MSB)
|
||||
c0 = i.OP + slb (keep (i.A, 2), 6)
|
||||
-- c1 = 6MSB from A + 2LSB from C (flushed to MSB)
|
||||
c1 = srb (i.A, 2) + slb (keep (i.C, 2), 6)
|
||||
-- c2 = 7MSB from C + 1LSB from B (flushed to MSB)
|
||||
c2 = srb (i.C, 2) + slb (keep (i.B, 1), 7)
|
||||
-- c3 = 8MSB from B
|
||||
c3 = srb (i.B, 1)
|
||||
--printf ("Instruction: %s %s", self.opnames[i.OP], tostringv (i))
|
||||
--printf ("Bin encoding: %x %x %x %x", c0, c1, c2, c3)
|
||||
return string.char(c0, c1, c2, c3)
|
||||
end
|
||||
end
|
||||
------------------------------------------------------------------------
|
||||
-- decodes a 4-char little-endian string into an instruction struct
|
||||
------------------------------------------------------------------------
|
||||
function luaP:DecodeInst(x)
|
||||
error "Not implemented"
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- invalid register that fits in 8 bits
|
||||
------------------------------------------------------------------------
|
||||
luaP.NO_REG = luaP.MAXARG_A
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- R(x) - register
|
||||
-- Kst(x) - constant (in constant table)
|
||||
-- RK(x) == if x < MAXSTACK then R(x) else Kst(x-MAXSTACK)
|
||||
------------------------------------------------------------------------
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- grep "ORDER OP" if you change these enums
|
||||
------------------------------------------------------------------------
|
||||
|
||||
--[[--------------------------------------------------------------------
|
||||
Lua virtual machine opcodes (enum OpCode):
|
||||
------------------------------------------------------------------------
|
||||
name args description
|
||||
------------------------------------------------------------------------
|
||||
OP_MOVE A B R(A) := R(B)
|
||||
OP_LOADK A Bx R(A) := Kst(Bx)
|
||||
OP_LOADBOOL A B C R(A) := (Bool)B; if (C) PC++
|
||||
OP_LOADNIL A B R(A) := ... := R(B) := nil
|
||||
OP_GETUPVAL A B R(A) := UpValue[B]
|
||||
OP_GETGLOBAL A Bx R(A) := Gbl[Kst(Bx)]
|
||||
OP_GETTABLE A B C R(A) := R(B)[RK(C)]
|
||||
OP_SETGLOBAL A Bx Gbl[Kst(Bx)] := R(A)
|
||||
OP_SETUPVAL A B UpValue[B] := R(A)
|
||||
OP_SETTABLE A B C R(A)[RK(B)] := RK(C)
|
||||
OP_NEWTABLE A B C R(A) := {} (size = B,C)
|
||||
OP_SELF A B C R(A+1) := R(B); R(A) := R(B)[RK(C)]
|
||||
OP_ADD A B C R(A) := RK(B) + RK(C)
|
||||
OP_SUB A B C R(A) := RK(B) - RK(C)
|
||||
OP_MUL A B C R(A) := RK(B) * RK(C)
|
||||
OP_DIV A B C R(A) := RK(B) / RK(C)
|
||||
OP_POW A B C R(A) := RK(B) ^ RK(C)
|
||||
OP_UNM A B R(A) := -R(B)
|
||||
OP_NOT A B R(A) := not R(B)
|
||||
OP_CONCAT A B C R(A) := R(B).. ... ..R(C)
|
||||
OP_JMP sBx PC += sBx
|
||||
OP_EQ A B C if ((RK(B) == RK(C)) ~= A) then pc++
|
||||
OP_LT A B C if ((RK(B) < RK(C)) ~= A) then pc++
|
||||
OP_LE A B C if ((RK(B) <= RK(C)) ~= A) then pc++
|
||||
OP_TEST A B C if (R(B) <=> C) then R(A) := R(B) else pc++
|
||||
OP_CALL A B C R(A), ... ,R(A+C-2) := R(A)(R(A+1), ... ,R(A+B-1))
|
||||
OP_TAILCALL A B C return R(A)(R(A+1), ... ,R(A+B-1))
|
||||
OP_RETURN A B return R(A), ... ,R(A+B-2) (see note)
|
||||
OP_FORLOOP A sBx R(A)+=R(A+2); if R(A) <?= R(A+1) then PC+= sBx
|
||||
OP_TFORLOOP A C R(A+2), ... ,R(A+2+C) := R(A)(R(A+1), R(A+2));
|
||||
if R(A+2) ~= nil then pc++
|
||||
OP_TFORPREP A sBx if type(R(A)) == table then R(A+1):=R(A), R(A):=next;
|
||||
PC += sBx
|
||||
OP_SETLIST A Bx R(A)[Bx-Bx%FPF+i] := R(A+i), 1 <= i <= Bx%FPF+1
|
||||
OP_SETLISTO A Bx (see note)
|
||||
OP_CLOSE A close all variables in the stack up to (>=) R(A)
|
||||
OP_CLOSURE A Bx R(A) := closure(KPROTO[Bx], R(A), ... ,R(A+n))
|
||||
----------------------------------------------------------------------]]
|
||||
|
||||
luaP.opnames = {} -- opcode names
|
||||
luaP.OpCode = {} -- lookup name -> number
|
||||
luaP.ROpCode = {} -- lookup number -> name
|
||||
|
||||
local i = 0
|
||||
for v in string.gfind([[
|
||||
MOVE -- 0
|
||||
LOADK
|
||||
LOADBOOL
|
||||
LOADNIL
|
||||
GETUPVAL
|
||||
GETGLOBAL -- 5
|
||||
GETTABLE
|
||||
SETGLOBAL
|
||||
SETUPVAL
|
||||
SETTABLE
|
||||
NEWTABLE -- 10
|
||||
SELF
|
||||
ADD
|
||||
SUB
|
||||
MUL
|
||||
DIV -- 15
|
||||
MOD
|
||||
POW
|
||||
UNM
|
||||
NOT
|
||||
LEN -- 20
|
||||
CONCAT
|
||||
JMP
|
||||
EQ
|
||||
LT
|
||||
LE -- 25
|
||||
TEST
|
||||
TESTSET
|
||||
CALL
|
||||
TAILCALL
|
||||
RETURN -- 30
|
||||
FORLOOP
|
||||
FORPREP
|
||||
TFORLOOP
|
||||
SETLIST
|
||||
CLOSE -- 35
|
||||
CLOSURE
|
||||
VARARG
|
||||
]], "[%a]+") do
|
||||
local n = "OP_"..v
|
||||
luaP.opnames[i] = v
|
||||
luaP.OpCode[n] = i
|
||||
luaP.ROpCode[i] = n
|
||||
i = i + 1
|
||||
end
|
||||
luaP.NUM_OPCODES = i
|
||||
|
||||
--[[
|
||||
===========================================================================
|
||||
Notes:
|
||||
(1) In OP_CALL, if (B == 0) then B = top. C is the number of returns - 1,
|
||||
and can be 0: OP_CALL then sets 'top' to last_result+1, so
|
||||
next open instruction (OP_CALL, OP_RETURN, OP_SETLIST) may use 'top'.
|
||||
|
||||
(2) In OP_RETURN, if (B == 0) then return up to 'top'
|
||||
|
||||
(3) For comparisons, B specifies what conditions the test should accept.
|
||||
|
||||
(4) All 'skips' (pc++) assume that next instruction is a jump
|
||||
|
||||
(5) OP_SETLISTO is used when the last item in a table constructor is a
|
||||
function, so the number of elements set is up to top of stack
|
||||
===========================================================================
|
||||
--]]
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- masks for instruction properties
|
||||
------------------------------------------------------------------------
|
||||
-- was enum OpModeMask:
|
||||
luaP.OpModeBreg = 2 -- B is a register
|
||||
luaP.OpModeBrk = 3 -- B is a register/constant
|
||||
luaP.OpModeCrk = 4 -- C is a register/constant
|
||||
luaP.OpModesetA = 5 -- instruction set register A
|
||||
luaP.OpModeK = 6 -- Bx is a constant
|
||||
luaP.OpModeT = 1 -- operator is a test
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- get opcode mode, e.g. "iABC"
|
||||
------------------------------------------------------------------------
|
||||
function luaP:getOpMode(m)
|
||||
--printv(m)
|
||||
--printv(self.OpCode[m])
|
||||
--printv(self.opmodes [self.OpCode[m]+1])
|
||||
return self.OpMode[tonumber(string.sub(self.opmodes[self.OpCode[m] + 1], 7, 7))]
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- test an instruction property flag
|
||||
-- * b is a string, e.g. "OpModeBreg"
|
||||
------------------------------------------------------------------------
|
||||
function luaP:testOpMode(m, b)
|
||||
return (string.sub(self.opmodes[self.OpCode[m] + 1], self[b], self[b]) == "1")
|
||||
end
|
||||
|
||||
-- number of list items to accumulate before a SETLIST instruction
|
||||
-- (must be a power of 2)
|
||||
-- * used in lparser, lvm, ldebug, ltests
|
||||
luaP.LFIELDS_PER_FLUSH = 50 --FF updated to match 5.1
|
||||
|
||||
-- luaP_opnames[] is set above, as the luaP.opnames table
|
||||
-- opmode(t,b,bk,ck,sa,k,m) deleted
|
||||
|
||||
--[[--------------------------------------------------------------------
|
||||
Legend for luaP:opmodes:
|
||||
1 T -> T (is a test?)
|
||||
2 B -> B is a register
|
||||
3 b -> B is an RK register/constant combination
|
||||
4 C -> C is an RK register/constant combination
|
||||
5 A -> register A is set by the opcode
|
||||
6 K -> Bx is a constant
|
||||
7 m -> 1 if iABC layout,
|
||||
2 if iABx layout,
|
||||
3 if iAsBx layout
|
||||
----------------------------------------------------------------------]]
|
||||
|
||||
luaP.opmodes = {
|
||||
-- TBbCAKm opcode
|
||||
"0100101", -- OP_MOVE 0
|
||||
"0000112", -- OP_LOADK
|
||||
"0000101", -- OP_LOADBOOL
|
||||
"0100101", -- OP_LOADNIL
|
||||
"0000101", -- OP_GETUPVAL
|
||||
"0000112", -- OP_GETGLOBAL 5
|
||||
"0101101", -- OP_GETTABLE
|
||||
"0000012", -- OP_SETGLOBAL
|
||||
"0000001", -- OP_SETUPVAL
|
||||
"0011001", -- OP_SETTABLE
|
||||
"0000101", -- OP_NEWTABLE 10
|
||||
"0101101", -- OP_SELF
|
||||
"0011101", -- OP_ADD
|
||||
"0011101", -- OP_SUB
|
||||
"0011101", -- OP_MUL
|
||||
"0011101", -- OP_DIV 15
|
||||
"0011101", -- OP_MOD
|
||||
"0011101", -- OP_POW
|
||||
"0100101", -- OP_UNM
|
||||
"0100101", -- OP_NOT
|
||||
"0100101", -- OP_LEN 20
|
||||
"0101101", -- OP_CONCAT
|
||||
"0000003", -- OP_JMP
|
||||
"1011001", -- OP_EQ
|
||||
"1011001", -- OP_LT
|
||||
"1011001", -- OP_LE 25
|
||||
"1000101", -- OP_TEST
|
||||
"1100101", -- OP_TESTSET
|
||||
"0000001", -- OP_CALL
|
||||
"0000001", -- OP_TAILCALL
|
||||
"0000001", -- OP_RETURN 30
|
||||
"0000003", -- OP_FORLOOP
|
||||
"0000103", -- OP_FORPREP
|
||||
"1000101", -- OP_TFORLOOP
|
||||
"0000001", -- OP_SETLIST
|
||||
"0000001", -- OP_CLOSE 35
|
||||
"0000102", -- OP_CLOSURE
|
||||
"0000101" -- OP_VARARG
|
||||
}
|
||||
|
||||
return luaP
|
||||
@@ -0,0 +1,86 @@
|
||||
--------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2006-2013 Fabien Fleutot and others.
|
||||
--
|
||||
-- All rights reserved.
|
||||
--
|
||||
-- This program and the accompanying materials are made available
|
||||
-- under the terms of the Eclipse Public License v1.0 which
|
||||
-- accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- This program and the accompanying materials are also made available
|
||||
-- under the terms of the MIT public license which accompanies this
|
||||
-- distribution, and is available at http://www.lua.org/license.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Fabien Fleutot - API and implementation
|
||||
--
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
--*-lua-*-----------------------------------------------------------------------
|
||||
-- Override Lua's default compilation functions, so that they support Metalua
|
||||
-- rather than only plain Lua
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local mlc = require 'metalua.compiler'
|
||||
|
||||
local M = { }
|
||||
|
||||
-- Original versions
|
||||
local original_lua_versions = {
|
||||
load = load,
|
||||
loadfile = loadfile,
|
||||
loadstring = loadstring,
|
||||
dofile = dofile }
|
||||
|
||||
local lua_loadstring = loadstring
|
||||
local lua_loadfile = loadfile
|
||||
|
||||
function M.loadstring(str, name)
|
||||
if type(str) ~= 'string' then error 'string expected' end
|
||||
if str:match '^\027LuaQ' then return lua_loadstring(str) end
|
||||
local n = str:match '^#![^\n]*\n()'
|
||||
if n then str=str:sub(n, -1) end
|
||||
-- FIXME: handle erroneous returns (return nil + error msg)
|
||||
return mlc.new():src_to_function(str, name)
|
||||
end
|
||||
|
||||
function M.loadfile(filename)
|
||||
local f, err_msg = io.open(filename, 'rb')
|
||||
if not f then return nil, err_msg end
|
||||
local success, src = pcall( f.read, f, '*a')
|
||||
pcall(f.close, f)
|
||||
if success then return M.loadstring (src, '@'..filename)
|
||||
else return nil, src end
|
||||
end
|
||||
|
||||
function M.load(f, name)
|
||||
local acc = { }
|
||||
while true do
|
||||
local x = f()
|
||||
if not x then break end
|
||||
assert(type(x)=='string', "function passed to load() must return strings")
|
||||
table.insert(acc, x)
|
||||
end
|
||||
return M.loadstring(table.concat(acc))
|
||||
end
|
||||
|
||||
function M.dostring(src)
|
||||
local f, msg = M.loadstring(src)
|
||||
if not f then error(msg) end
|
||||
return f()
|
||||
end
|
||||
|
||||
function M.dofile(name)
|
||||
local f, msg = M.loadfile(name)
|
||||
if not f then error(msg) end
|
||||
return f()
|
||||
end
|
||||
|
||||
-- Export replacement functions as globals
|
||||
for name, f in pairs(M) do _G[name] = f end
|
||||
|
||||
-- To be done *after* exportation
|
||||
M.lua = original_lua_versions
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,42 @@
|
||||
--------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2006-2013 Fabien Fleutot and others.
|
||||
--
|
||||
-- All rights reserved.
|
||||
--
|
||||
-- This program and the accompanying materials are made available
|
||||
-- under the terms of the Eclipse Public License v1.0 which
|
||||
-- accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- This program and the accompanying materials are also made available
|
||||
-- under the terms of the MIT public license which accompanies this
|
||||
-- distribution, and is available at http://www.lua.org/license.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Fabien Fleutot - API and implementation
|
||||
--
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
-- Export all public APIs from sub-modules, squashed into a flat spacename
|
||||
|
||||
local MT = { __type='metalua.compiler.parser' }
|
||||
|
||||
local MODULE_REL_NAMES = { "annot.grammar", "expr", "meta", "misc",
|
||||
"stat", "table", "ext" }
|
||||
|
||||
local function new()
|
||||
local M = {
|
||||
lexer = require "metalua.compiler.parser.lexer" ();
|
||||
extensions = { } }
|
||||
for _, rel_name in ipairs(MODULE_REL_NAMES) do
|
||||
local abs_name = "metalua.compiler.parser."..rel_name
|
||||
local extender = require (abs_name)
|
||||
if not M.extensions[abs_name] then
|
||||
if type (extender) == 'function' then extender(M) end
|
||||
M.extensions[abs_name] = extender
|
||||
end
|
||||
end
|
||||
return setmetatable(M, MT)
|
||||
end
|
||||
|
||||
return { new = new }
|
||||
@@ -0,0 +1,48 @@
|
||||
--------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2006-2013 Fabien Fleutot and others.
|
||||
--
|
||||
-- All rights reserved.
|
||||
--
|
||||
-- This program and the accompanying materials are made available
|
||||
-- under the terms of the Eclipse Public License v1.0 which
|
||||
-- accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- This program and the accompanying materials are also made available
|
||||
-- under the terms of the MIT public license which accompanies this
|
||||
-- distribution, and is available at http://www.lua.org/license.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Fabien Fleutot - API and implementation
|
||||
--
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local checks = require 'checks'
|
||||
local gg = require 'metalua.grammar.generator'
|
||||
local M = { }
|
||||
|
||||
function M.opt(mlc, primary, a_type)
|
||||
checks('table', 'table|function', 'string')
|
||||
return gg.sequence{
|
||||
primary,
|
||||
gg.onkeyword{ "#", function() return assert(mlc.annot[a_type]) end },
|
||||
builder = function(x)
|
||||
local t, annot = unpack(x)
|
||||
return annot and { tag='Annot', t, annot } or t
|
||||
end }
|
||||
end
|
||||
|
||||
-- split a list of "foo" and "`Annot{foo, annot}" into a list of "foo"
|
||||
-- and a list of "annot".
|
||||
-- No annot list is returned if none of the elements were annotated.
|
||||
function M.split(lst)
|
||||
local x, a, some = { }, { }, false
|
||||
for i, p in ipairs(lst) do
|
||||
if p.tag=='Annot' then
|
||||
some, x[i], a[i] = true, unpack(p)
|
||||
else x[i] = p end
|
||||
end
|
||||
if some then return x, a else return lst end
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,112 @@
|
||||
--------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2006-2013 Fabien Fleutot and others.
|
||||
--
|
||||
-- All rights reserved.
|
||||
--
|
||||
-- This program and the accompanying materials are made available
|
||||
-- under the terms of the Eclipse Public License v1.0 which
|
||||
-- accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- This program and the accompanying materials are also made available
|
||||
-- under the terms of the MIT public license which accompanies this
|
||||
-- distribution, and is available at http://www.lua.org/license.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Fabien Fleutot - API and implementation
|
||||
--
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local gg = require 'metalua.grammar.generator'
|
||||
|
||||
return function(M)
|
||||
local _M = gg.future(M)
|
||||
M.lexer :add '->'
|
||||
local A = { }
|
||||
local _A = gg.future(A)
|
||||
M.annot = A
|
||||
|
||||
-- Type identifier: Lua keywords such as `"nil"` allowed.
|
||||
function M.annot.tid(lx)
|
||||
local w = lx :next()
|
||||
local t = w.tag
|
||||
if t=='Keyword' and w[1] :match '^[%a_][%w_]*$' or w.tag=='Id'
|
||||
then return {tag='TId'; lineinfo=w.lineinfo; w[1]}
|
||||
else return gg.parse_error (lx, 'tid expected') end
|
||||
end
|
||||
|
||||
local field_types = { var='TVar'; const='TConst';
|
||||
currently='TCurrently'; field='TField' }
|
||||
|
||||
-- TODO check lineinfo
|
||||
function M.annot.tf(lx)
|
||||
local tk = lx:next()
|
||||
local w = tk[1]
|
||||
local tag = field_types[w]
|
||||
if not tag then error ('Invalid field type '..w)
|
||||
elseif tag=='TField' then return {tag='TField'} else
|
||||
local te = M.te(lx)
|
||||
return {tag=tag; te}
|
||||
end
|
||||
end
|
||||
|
||||
M.annot.tebar_content = gg.list{
|
||||
name = 'tebar content',
|
||||
primary = _A.te,
|
||||
separators = { ",", ";" },
|
||||
terminators = ")" }
|
||||
|
||||
M.annot.tebar = gg.multisequence{
|
||||
name = 'annot.tebar',
|
||||
--{ '*', builder = 'TDynbar' }, -- maybe not user-available
|
||||
{ '(', _A.tebar_content, ')',
|
||||
builder = function(x) return x[1] end },
|
||||
{ _A.te }
|
||||
}
|
||||
|
||||
M.annot.te = gg.multisequence{
|
||||
name = 'annot.te',
|
||||
{ _A.tid, builder=function(x) return x[1] end },
|
||||
{ '*', builder = 'TDyn' },
|
||||
{ "[",
|
||||
gg.list{
|
||||
primary = gg.sequence{
|
||||
_M.expr, "=", _A.tf,
|
||||
builder = 'TPair'
|
||||
},
|
||||
separators = { ",", ";" },
|
||||
terminators = { "]", "|" } },
|
||||
gg.onkeyword{ "|", _A.tf },
|
||||
"]",
|
||||
builder = function(x)
|
||||
local fields, other = unpack(x)
|
||||
return { tag='TTable', other or {tag='TField'}, fields }
|
||||
end }, -- "[ ... ]"
|
||||
{ '(', _A.tebar_content, ')', '->', '(', _A.tebar_content, ')',
|
||||
builder = function(x)
|
||||
local p, r = unpack(x)
|
||||
return {tag='TFunction', p, r }
|
||||
end } }
|
||||
|
||||
M.annot.ts = gg.multisequence{
|
||||
name = 'annot.ts',
|
||||
{ 'return', _A.tebar_content, builder='TReturn' },
|
||||
{ _A.tid, builder = function(x)
|
||||
if x[1][1]=='pass' then return {tag='TPass'}
|
||||
else error "Bad statement type" end
|
||||
end } }
|
||||
|
||||
-- TODO: add parsers for statements:
|
||||
-- #return tebar
|
||||
-- #alias = te
|
||||
-- #ell = tf
|
||||
--[[
|
||||
M.annot.stat_annot = gg.sequence{
|
||||
gg.list{ primary=_A.tid, separators='.' },
|
||||
'=',
|
||||
XXX??,
|
||||
builder = 'Annot' }
|
||||
--]]
|
||||
|
||||
return M.annot
|
||||
end
|
||||
@@ -0,0 +1,206 @@
|
||||
-------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2006-2013 Fabien Fleutot and others.
|
||||
--
|
||||
-- All rights reserved.
|
||||
--
|
||||
-- This program and the accompanying materials are made available
|
||||
-- under the terms of the Eclipse Public License v1.0 which
|
||||
-- accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- This program and the accompanying materials are also made available
|
||||
-- under the terms of the MIT public license which accompanies this
|
||||
-- distribution, and is available at http://www.lua.org/license.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Fabien Fleutot - API and implementation
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
--
|
||||
-- Exported API:
|
||||
-- * [mlp.expr()]
|
||||
-- * [mlp.expr_list()]
|
||||
-- * [mlp.func_val()]
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
local pp = require 'metalua.pprint'
|
||||
local gg = require 'metalua.grammar.generator'
|
||||
local annot = require 'metalua.compiler.parser.annot.generator'
|
||||
|
||||
return function(M)
|
||||
local _M = gg.future(M)
|
||||
local _table = gg.future(M, 'table')
|
||||
local _meta = gg.future(M, 'meta') -- TODO move to ext?
|
||||
local _annot = gg.future(M, 'annot') -- TODO move to annot
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Non-empty expression list. Actually, this isn't used here, but that's
|
||||
-- handy to give to users.
|
||||
--------------------------------------------------------------------------------
|
||||
M.expr_list = gg.list{ primary=_M.expr, separators="," }
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Helpers for function applications / method applications
|
||||
--------------------------------------------------------------------------------
|
||||
M.func_args_content = gg.list{
|
||||
name = "function arguments",
|
||||
primary = _M.expr,
|
||||
separators = ",",
|
||||
terminators = ")" }
|
||||
|
||||
-- Used to parse methods
|
||||
M.method_args = gg.multisequence{
|
||||
name = "function argument(s)",
|
||||
{ "{", _table.content, "}" },
|
||||
{ "(", _M.func_args_content, ")", builder = unpack },
|
||||
{ "+{", _meta.quote_content, "}" },
|
||||
-- TODO lineinfo?
|
||||
function(lx) local r = M.opt_string(lx); return r and {r} or { } end }
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- [func_val] parses a function, from opening parameters parenthese to
|
||||
-- "end" keyword included. Used for anonymous functions as well as
|
||||
-- function declaration statements (both local and global).
|
||||
--
|
||||
-- It's wrapped in a [_func_val] eta expansion, so that when expr
|
||||
-- parser uses the latter, they will notice updates of [func_val]
|
||||
-- definitions.
|
||||
--------------------------------------------------------------------------------
|
||||
M.func_params_content = gg.list{
|
||||
name="function parameters",
|
||||
gg.multisequence{ { "...", builder = "Dots" }, annot.opt(M, _M.id, 'te') },
|
||||
separators = ",", terminators = {")", "|"} }
|
||||
|
||||
-- TODO move to annot
|
||||
M.func_val = gg.sequence{
|
||||
name = "function body",
|
||||
"(", _M.func_params_content, ")", _M.block, "end",
|
||||
builder = function(x)
|
||||
local params, body = unpack(x)
|
||||
local annots, some = { }, false
|
||||
for i, p in ipairs(params) do
|
||||
if p.tag=='Annot' then
|
||||
params[i], annots[i], some = p[1], p[2], true
|
||||
else annots[i] = false end
|
||||
end
|
||||
if some then return { tag='Function', params, body, annots }
|
||||
else return { tag='Function', params, body } end
|
||||
end }
|
||||
|
||||
local func_val = function(lx) return M.func_val(lx) end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Default parser for primary expressions
|
||||
--------------------------------------------------------------------------------
|
||||
function M.id_or_literal (lx)
|
||||
local a = lx:next()
|
||||
if a.tag~="Id" and a.tag~="String" and a.tag~="Number" then
|
||||
local msg
|
||||
if a.tag=='Eof' then
|
||||
msg = "End of file reached when an expression was expected"
|
||||
elseif a.tag=='Keyword' then
|
||||
msg = "An expression was expected, and `"..a[1]..
|
||||
"' can't start an expression"
|
||||
else
|
||||
msg = "Unexpected expr token " .. pp.tostring (a)
|
||||
end
|
||||
gg.parse_error (lx, msg)
|
||||
end
|
||||
return a
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Builder generator for operators. Wouldn't be worth it if "|x|" notation
|
||||
-- were allowed, but then lua 5.1 wouldn't compile it
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
-- opf1 = |op| |_,a| `Op{ op, a }
|
||||
local function opf1 (op) return
|
||||
function (_,a) return { tag="Op", op, a } end end
|
||||
|
||||
-- opf2 = |op| |a,_,b| `Op{ op, a, b }
|
||||
local function opf2 (op) return
|
||||
function (a,_,b) return { tag="Op", op, a, b } end end
|
||||
|
||||
-- opf2r = |op| |a,_,b| `Op{ op, b, a } -- (args reversed)
|
||||
local function opf2r (op) return
|
||||
function (a,_,b) return { tag="Op", op, b, a } end end
|
||||
|
||||
local function op_ne(a, _, b)
|
||||
-- This version allows to remove the "ne" operator from the AST definition.
|
||||
-- However, it doesn't always produce the exact same bytecode as Lua 5.1.
|
||||
return { tag="Op", "not",
|
||||
{ tag="Op", "eq", a, b, lineinfo= {
|
||||
first = a.lineinfo.first, last = b.lineinfo.last } } }
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
--
|
||||
-- complete expression
|
||||
--
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
-- FIXME: set line number. In [expr] transformers probably
|
||||
M.expr = gg.expr {
|
||||
name = "expression",
|
||||
primary = gg.multisequence{
|
||||
name = "expr primary",
|
||||
{ "(", _M.expr, ")", builder = "Paren" },
|
||||
{ "function", _M.func_val, builder = unpack },
|
||||
{ "-{", _meta.splice_content, "}", builder = unpack },
|
||||
{ "+{", _meta.quote_content, "}", builder = unpack },
|
||||
{ "nil", builder = "Nil" },
|
||||
{ "true", builder = "True" },
|
||||
{ "false", builder = "False" },
|
||||
{ "...", builder = "Dots" },
|
||||
{ "{", _table.content, "}", builder = unpack },
|
||||
_M.id_or_literal },
|
||||
|
||||
infix = {
|
||||
name = "expr infix op",
|
||||
{ "+", prec = 60, builder = opf2 "add" },
|
||||
{ "-", prec = 60, builder = opf2 "sub" },
|
||||
{ "*", prec = 70, builder = opf2 "mul" },
|
||||
{ "/", prec = 70, builder = opf2 "div" },
|
||||
{ "%", prec = 70, builder = opf2 "mod" },
|
||||
{ "^", prec = 90, builder = opf2 "pow", assoc = "right" },
|
||||
{ "..", prec = 40, builder = opf2 "concat", assoc = "right" },
|
||||
{ "==", prec = 30, builder = opf2 "eq" },
|
||||
{ "~=", prec = 30, builder = op_ne },
|
||||
{ "<", prec = 30, builder = opf2 "lt" },
|
||||
{ "<=", prec = 30, builder = opf2 "le" },
|
||||
{ ">", prec = 30, builder = opf2r "lt" },
|
||||
{ ">=", prec = 30, builder = opf2r "le" },
|
||||
{ "and",prec = 20, builder = opf2 "and" },
|
||||
{ "or", prec = 10, builder = opf2 "or" } },
|
||||
|
||||
prefix = {
|
||||
name = "expr prefix op",
|
||||
{ "not", prec = 80, builder = opf1 "not" },
|
||||
{ "#", prec = 80, builder = opf1 "len" },
|
||||
{ "-", prec = 80, builder = opf1 "unm" } },
|
||||
|
||||
suffix = {
|
||||
name = "expr suffix op",
|
||||
{ "[", _M.expr, "]", builder = function (tab, idx)
|
||||
return {tag="Index", tab, idx[1]} end},
|
||||
{ ".", _M.id, builder = function (tab, field)
|
||||
return {tag="Index", tab, _M.id2string(field[1])} end },
|
||||
{ "(", _M.func_args_content, ")", builder = function(f, args)
|
||||
return {tag="Call", f, unpack(args[1])} end },
|
||||
{ "{", _table.content, "}", builder = function (f, arg)
|
||||
return {tag="Call", f, arg[1]} end},
|
||||
{ ":", _M.id, _M.method_args, builder = function (obj, post)
|
||||
local m_name, args = unpack(post)
|
||||
return {tag="Invoke", obj, _M.id2string(m_name), unpack(args)} end},
|
||||
{ "+{", _meta.quote_content, "}", builder = function (f, arg)
|
||||
return {tag="Call", f, arg[1] } end },
|
||||
default = { name="opt_string_arg", parse = _M.opt_string, builder = function(f, arg)
|
||||
return {tag="Call", f, arg } end } } }
|
||||
return M
|
||||
end
|
||||
@@ -0,0 +1,96 @@
|
||||
-------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2006-2013 Fabien Fleutot and others.
|
||||
--
|
||||
-- All rights reserved.
|
||||
--
|
||||
-- This program and the accompanying materials are made available
|
||||
-- under the terms of the Eclipse Public License v1.0 which
|
||||
-- accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- This program and the accompanying materials are also made available
|
||||
-- under the terms of the MIT public license which accompanies this
|
||||
-- distribution, and is available at http://www.lua.org/license.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Fabien Fleutot - API and implementation
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
--
|
||||
-- Non-Lua syntax extensions
|
||||
--
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local gg = require 'metalua.grammar.generator'
|
||||
|
||||
return function(M)
|
||||
|
||||
local _M = gg.future(M)
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- Algebraic Datatypes
|
||||
----------------------------------------------------------------------------
|
||||
local function adt (lx)
|
||||
local node = _M.id (lx)
|
||||
local tagval = node[1]
|
||||
-- tagkey = `Pair{ `String "key", `String{ -{tagval} } }
|
||||
local tagkey = { tag="Pair", {tag="String", "tag"}, {tag="String", tagval} }
|
||||
if lx:peek().tag == "String" or lx:peek().tag == "Number" then
|
||||
-- TODO support boolean litterals
|
||||
return { tag="Table", tagkey, lx:next() }
|
||||
elseif lx:is_keyword (lx:peek(), "{") then
|
||||
local x = M.table.table (lx)
|
||||
table.insert (x, 1, tagkey)
|
||||
return x
|
||||
else return { tag="Table", tagkey } end
|
||||
end
|
||||
|
||||
M.adt = gg.sequence{ "`", adt, builder = unpack }
|
||||
|
||||
M.expr.primary :add(M.adt)
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
-- Anonymous lambda
|
||||
----------------------------------------------------------------------------
|
||||
M.lambda_expr = gg.sequence{
|
||||
"|", _M.func_params_content, "|", _M.expr,
|
||||
builder = function (x)
|
||||
local li = x[2].lineinfo
|
||||
return { tag="Function", x[1],
|
||||
{ {tag="Return", x[2], lineinfo=li }, lineinfo=li } }
|
||||
end }
|
||||
|
||||
M.expr.primary :add (M.lambda_expr)
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Allows to write "a `f` b" instead of "f(a, b)". Taken from Haskell.
|
||||
--------------------------------------------------------------------------------
|
||||
function M.expr_in_backquotes (lx) return M.expr(lx, 35) end -- 35=limited precedence
|
||||
M.expr.infix :add{ name = "infix function",
|
||||
"`", _M.expr_in_backquotes, "`", prec = 35, assoc="left",
|
||||
builder = function(a, op, b) return {tag="Call", op[1], a, b} end }
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- C-style op+assignments
|
||||
-- TODO: no protection against side-effects in LHS vars.
|
||||
--------------------------------------------------------------------------------
|
||||
local function op_assign(kw, op)
|
||||
local function rhs(a, b) return { tag="Op", op, a, b } end
|
||||
local function f(a,b)
|
||||
if #a ~= #b then gg.parse_error "assymetric operator+assignment" end
|
||||
local right = { }
|
||||
local r = { tag="Set", a, right }
|
||||
for i=1, #a do right[i] = { tag="Op", op, a[i], b[i] } end
|
||||
return r
|
||||
end
|
||||
M.lexer :add (kw)
|
||||
M.assignments[kw] = f
|
||||
end
|
||||
|
||||
local ops = { add='+='; sub='-='; mul='*='; div='/=' }
|
||||
for ast_op_name, keyword in pairs(ops) do op_assign(keyword, ast_op_name) end
|
||||
|
||||
return M
|
||||
end
|
||||
@@ -0,0 +1,43 @@
|
||||
--------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2006-2014 Fabien Fleutot and others.
|
||||
--
|
||||
-- All rights reserved.
|
||||
--
|
||||
-- This program and the accompanying materials are made available
|
||||
-- under the terms of the Eclipse Public License v1.0 which
|
||||
-- accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- This program and the accompanying materials are also made available
|
||||
-- under the terms of the MIT public license which accompanies this
|
||||
-- distribution, and is available at http://www.lua.org/license.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Fabien Fleutot - API and implementation
|
||||
--
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Generate a new lua-specific lexer, derived from the generic lexer.
|
||||
----------------------------------------------------------------------
|
||||
|
||||
local generic_lexer = require 'metalua.grammar.lexer'
|
||||
|
||||
return function()
|
||||
local lexer = generic_lexer.lexer :clone()
|
||||
|
||||
local keywords = {
|
||||
"and", "break", "do", "else", "elseif",
|
||||
"end", "false", "for", "function",
|
||||
"goto", -- Lua5.2
|
||||
"if",
|
||||
"in", "local", "nil", "not", "or", "repeat",
|
||||
"return", "then", "true", "until", "while",
|
||||
"...", "..", "==", ">=", "<=", "~=",
|
||||
"::", -- Lua5,2
|
||||
"+{", "-{" } -- Metalua
|
||||
|
||||
for _, w in ipairs(keywords) do lexer :add (w) end
|
||||
|
||||
return lexer
|
||||
end
|
||||
@@ -0,0 +1,138 @@
|
||||
-------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2006-2014 Fabien Fleutot and others.
|
||||
--
|
||||
-- All rights reserved.
|
||||
--
|
||||
-- This program and the accompanying materials are made available
|
||||
-- under the terms of the Eclipse Public License v1.0 which
|
||||
-- accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- This program and the accompanying materials are also made available
|
||||
-- under the terms of the MIT public license which accompanies this
|
||||
-- distribution, and is available at http://www.lua.org/license.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Fabien Fleutot - API and implementation
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
-- Compile-time metaprogramming features: splicing ASTs generated during compilation,
|
||||
-- AST quasi-quoting helpers.
|
||||
|
||||
local gg = require 'metalua.grammar.generator'
|
||||
|
||||
return function(M)
|
||||
local _M = gg.future(M)
|
||||
M.meta={ }
|
||||
local _MM = gg.future(M.meta)
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- External splicing: compile an AST into a chunk, load and evaluate
|
||||
-- that chunk, and replace the chunk by its result (which must also be
|
||||
-- an AST).
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
-- TODO: that's not part of the parser
|
||||
function M.meta.eval (ast)
|
||||
-- TODO: should there be one mlc per splice, or per parser instance?
|
||||
local mlc = require 'metalua.compiler'.new()
|
||||
local f = mlc :ast_to_function (ast, '=splice')
|
||||
local result=f(M) -- splices act on the current parser
|
||||
return result
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
-- Going from an AST to an AST representing that AST
|
||||
-- the only hash-part key being lifted is `"tag"`.
|
||||
-- Doesn't lift subtrees protected inside a `Splice{ ... }.
|
||||
-- e.g. change `Foo{ 123 } into
|
||||
-- `Table{ `Pair{ `String "tag", `String "foo" }, `Number 123 }
|
||||
----------------------------------------------------------------------------
|
||||
local function lift (t)
|
||||
--print("QUOTING:", table.tostring(t, 60,'nohash'))
|
||||
local cases = { }
|
||||
function cases.table (t)
|
||||
local mt = { tag = "Table" }
|
||||
--table.insert (mt, { tag = "Pair", quote "quote", { tag = "True" } })
|
||||
if t.tag == "Splice" then
|
||||
assert (#t==1, "Invalid splice")
|
||||
local sp = t[1]
|
||||
return sp
|
||||
elseif t.tag then
|
||||
table.insert (mt, { tag="Pair", lift "tag", lift(t.tag) })
|
||||
end
|
||||
for _, v in ipairs (t) do
|
||||
table.insert (mt, lift(v))
|
||||
end
|
||||
return mt
|
||||
end
|
||||
function cases.number (t) return { tag = "Number", t, quote = true } end
|
||||
function cases.string (t) return { tag = "String", t, quote = true } end
|
||||
function cases.boolean (t) return { tag = t and "True" or "False", t, quote = true } end
|
||||
local f = cases [type(t)]
|
||||
if f then return f(t) else error ("Cannot quote an AST containing "..tostring(t)) end
|
||||
end
|
||||
M.meta.lift = lift
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- when this variable is false, code inside [-{...}] is compiled and
|
||||
-- avaluated immediately. When it's true (supposedly when we're
|
||||
-- parsing data inside a quasiquote), [-{foo}] is replaced by
|
||||
-- [`Splice{foo}], which will be unpacked by [quote()].
|
||||
--------------------------------------------------------------------------------
|
||||
local in_a_quote = false
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Parse the inside of a "-{ ... }"
|
||||
--------------------------------------------------------------------------------
|
||||
function M.meta.splice_content (lx)
|
||||
local parser_name = "expr"
|
||||
if lx:is_keyword (lx:peek(2), ":") then
|
||||
local a = lx:next()
|
||||
lx:next() -- skip ":"
|
||||
assert (a.tag=="Id", "Invalid splice parser name")
|
||||
parser_name = a[1]
|
||||
end
|
||||
-- TODO FIXME running a new parser with the old lexer?!
|
||||
local parser = require 'metalua.compiler.parser'.new()
|
||||
local ast = parser [parser_name](lx)
|
||||
if in_a_quote then -- only prevent quotation in this subtree
|
||||
--printf("SPLICE_IN_QUOTE:\n%s", _G.table.tostring(ast, "nohash", 60))
|
||||
return { tag="Splice", ast }
|
||||
else -- convert in a block, eval, replace with result
|
||||
if parser_name == "expr" then ast = { { tag="Return", ast } }
|
||||
elseif parser_name == "stat" then ast = { ast }
|
||||
elseif parser_name ~= "block" then
|
||||
error ("splice content must be an expr, stat or block") end
|
||||
--printf("EXEC THIS SPLICE:\n%s", _G.table.tostring(ast, "nohash", 60))
|
||||
return M.meta.eval (ast)
|
||||
end
|
||||
end
|
||||
|
||||
M.meta.splice = gg.sequence{ "-{", _MM.splice_content, "}", builder=unpack }
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Parse the inside of a "+{ ... }"
|
||||
--------------------------------------------------------------------------------
|
||||
function M.meta.quote_content (lx)
|
||||
local parser
|
||||
if lx:is_keyword (lx:peek(2), ":") then -- +{parser: content }
|
||||
local parser_name = M.id(lx)[1]
|
||||
parser = M[parser_name]
|
||||
lx:next() -- skip ":"
|
||||
else -- +{ content }
|
||||
parser = M.expr
|
||||
end
|
||||
|
||||
local prev_iq = in_a_quote
|
||||
in_a_quote = true
|
||||
--print("IN_A_QUOTE")
|
||||
local content = parser (lx)
|
||||
local q_content = M.meta.lift (content)
|
||||
in_a_quote = prev_iq
|
||||
return q_content
|
||||
end
|
||||
|
||||
return M
|
||||
end
|
||||
@@ -0,0 +1,176 @@
|
||||
-------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2006-2013 Fabien Fleutot and others.
|
||||
--
|
||||
-- All rights reserved.
|
||||
--
|
||||
-- This program and the accompanying materials are made available
|
||||
-- under the terms of the Eclipse Public License v1.0 which
|
||||
-- accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- This program and the accompanying materials are also made available
|
||||
-- under the terms of the MIT public license which accompanies this
|
||||
-- distribution, and is available at http://www.lua.org/license.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Fabien Fleutot - API and implementation
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
--
|
||||
-- Summary: metalua parser, miscellaneous utility functions.
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
--
|
||||
-- Exported API:
|
||||
-- * [mlp.fget()]
|
||||
-- * [mlp.id()]
|
||||
-- * [mlp.opt_id()]
|
||||
-- * [mlp.id_list()]
|
||||
-- * [mlp.string()]
|
||||
-- * [mlp.opt_string()]
|
||||
-- * [mlp.id2string()]
|
||||
--
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local pp = require 'metalua.pprint'
|
||||
local gg = require 'metalua.grammar.generator'
|
||||
|
||||
-- TODO: replace splice-aware versions with naive ones, move etensions in ./meta
|
||||
|
||||
return function(M)
|
||||
local _M = gg.future(M)
|
||||
|
||||
--[[ metaprog-free versions:
|
||||
function M.id(lx)
|
||||
if lx:peek().tag~='Id' then gg.parse_error(lx, "Identifier expected")
|
||||
else return lx:next() end
|
||||
end
|
||||
|
||||
function M.opt_id(lx)
|
||||
if lx:peek().tag~='Id' then return lx:next() else return false end
|
||||
end
|
||||
|
||||
function M.string(lx)
|
||||
if lx:peek().tag~='String' then gg.parse_error(lx, "String expected")
|
||||
else return lx:next() end
|
||||
end
|
||||
|
||||
function M.opt_string(lx)
|
||||
if lx:peek().tag~='String' then return lx:next() else return false end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Converts an identifier into a string. Hopefully one day it'll handle
|
||||
-- splices gracefully, but that proves quite tricky.
|
||||
--------------------------------------------------------------------------------
|
||||
function M.id2string (id)
|
||||
if id.tag == "Id" then id.tag = "String"; return id
|
||||
else error ("Identifier expected: "..table.tostring(id, 'nohash')) end
|
||||
end
|
||||
--]]
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Try to read an identifier (possibly as a splice), or return [false] if no
|
||||
-- id is found.
|
||||
--------------------------------------------------------------------------------
|
||||
function M.opt_id (lx)
|
||||
local a = lx:peek();
|
||||
if lx:is_keyword (a, "-{") then
|
||||
local v = M.meta.splice(lx)
|
||||
if v.tag ~= "Id" and v.tag ~= "Splice" then
|
||||
gg.parse_error(lx, "Bad id splice")
|
||||
end
|
||||
return v
|
||||
elseif a.tag == "Id" then return lx:next()
|
||||
else return false end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Mandatory reading of an id: causes an error if it can't read one.
|
||||
--------------------------------------------------------------------------------
|
||||
function M.id (lx)
|
||||
return M.opt_id (lx) or gg.parse_error(lx,"Identifier expected")
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Common helper function
|
||||
--------------------------------------------------------------------------------
|
||||
M.id_list = gg.list { primary = _M.id, separators = "," }
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Converts an identifier into a string. Hopefully one day it'll handle
|
||||
-- splices gracefully, but that proves quite tricky.
|
||||
--------------------------------------------------------------------------------
|
||||
function M.id2string (id)
|
||||
--print("id2string:", disp.ast(id))
|
||||
if id.tag == "Id" then id.tag = "String"; return id
|
||||
elseif id.tag == "Splice" then
|
||||
error ("id2string on splice not implemented")
|
||||
-- Evaluating id[1] will produce `Id{ xxx },
|
||||
-- and we want it to produce `String{ xxx }.
|
||||
-- The following is the plain notation of:
|
||||
-- +{ `String{ `Index{ `Splice{ -{id[1]} }, `Number 1 } } }
|
||||
return { tag="String", { tag="Index", { tag="Splice", id[1] },
|
||||
{ tag="Number", 1 } } }
|
||||
else error ("Identifier expected: "..pp.tostring (id, {metalua_tag=1, hide_hash=1})) end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Read a string, possibly spliced, or return an error if it can't
|
||||
--------------------------------------------------------------------------------
|
||||
function M.string (lx)
|
||||
local a = lx:peek()
|
||||
if lx:is_keyword (a, "-{") then
|
||||
local v = M.meta.splice(lx)
|
||||
if v.tag ~= "String" and v.tag ~= "Splice" then
|
||||
gg.parse_error(lx,"Bad string splice")
|
||||
end
|
||||
return v
|
||||
elseif a.tag == "String" then return lx:next()
|
||||
else error "String expected" end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Try to read a string, or return false if it can't. No splice allowed.
|
||||
--------------------------------------------------------------------------------
|
||||
function M.opt_string (lx)
|
||||
return lx:peek().tag == "String" and lx:next()
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Chunk reader: block + Eof
|
||||
--------------------------------------------------------------------------------
|
||||
function M.skip_initial_sharp_comment (lx)
|
||||
-- Dirty hack: I'm happily fondling lexer's private parts
|
||||
-- FIXME: redundant with lexer:newstream()
|
||||
lx :sync()
|
||||
local i = lx.src:match ("^#.-\n()", lx.i)
|
||||
if i then
|
||||
lx.i = i
|
||||
lx.column_offset = i
|
||||
lx.line = lx.line and lx.line + 1 or 1
|
||||
end
|
||||
end
|
||||
|
||||
local function chunk (lx)
|
||||
if lx:peek().tag == 'Eof' then
|
||||
return { } -- handle empty files
|
||||
else
|
||||
M.skip_initial_sharp_comment (lx)
|
||||
local chunk = M.block (lx)
|
||||
if lx:peek().tag ~= "Eof" then
|
||||
gg.parse_error(lx, "End-of-file expected")
|
||||
end
|
||||
return chunk
|
||||
end
|
||||
end
|
||||
|
||||
-- chunk is wrapped in a sequence so that it has a "transformer" field.
|
||||
M.chunk = gg.sequence { chunk, builder = unpack }
|
||||
|
||||
return M
|
||||
end
|
||||
@@ -0,0 +1,279 @@
|
||||
------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2006-2013 Fabien Fleutot and others.
|
||||
--
|
||||
-- All rights reserved.
|
||||
--
|
||||
-- This program and the accompanying materials are made available
|
||||
-- under the terms of the Eclipse Public License v1.0 which
|
||||
-- accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- This program and the accompanying materials are also made available
|
||||
-- under the terms of the MIT public license which accompanies this
|
||||
-- distribution, and is available at http://www.lua.org/license.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Fabien Fleutot - API and implementation
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
--
|
||||
-- Summary: metalua parser, statement/block parser. This is part of the
|
||||
-- definition of module [mlp].
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
--
|
||||
-- Exports API:
|
||||
-- * [mlp.stat()]
|
||||
-- * [mlp.block()]
|
||||
-- * [mlp.for_header()]
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
local lexer = require 'metalua.grammar.lexer'
|
||||
local gg = require 'metalua.grammar.generator'
|
||||
|
||||
local annot = require 'metalua.compiler.parser.annot.generator'
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- List of all keywords that indicate the end of a statement block. Users are
|
||||
-- likely to extend this list when designing extensions.
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
|
||||
return function(M)
|
||||
local _M = gg.future(M)
|
||||
|
||||
M.block_terminators = { "else", "elseif", "end", "until", ")", "}", "]" }
|
||||
|
||||
-- FIXME: this must be handled from within GG!!!
|
||||
-- FIXME: there's no :add method in the list anyway. Added by gg.list?!
|
||||
function M.block_terminators :add(x)
|
||||
if type (x) == "table" then for _, y in ipairs(x) do self :add (y) end
|
||||
else table.insert (self, x) end
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
-- list of statements, possibly followed by semicolons
|
||||
----------------------------------------------------------------------------
|
||||
M.block = gg.list {
|
||||
name = "statements block",
|
||||
terminators = M.block_terminators,
|
||||
primary = function (lx)
|
||||
-- FIXME use gg.optkeyword()
|
||||
local x = M.stat (lx)
|
||||
if lx:is_keyword (lx:peek(), ";") then lx:next() end
|
||||
return x
|
||||
end }
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
-- Helper function for "return <expr_list>" parsing.
|
||||
-- Called when parsing return statements.
|
||||
-- The specific test for initial ";" is because it's not a block terminator,
|
||||
-- so without it gg.list would choke on "return ;" statements.
|
||||
-- We don't make a modified copy of block_terminators because this list
|
||||
-- is sometimes modified at runtime, and the return parser would get out of
|
||||
-- sync if it was relying on a copy.
|
||||
----------------------------------------------------------------------------
|
||||
local return_expr_list_parser = gg.multisequence{
|
||||
{ ";" , builder = function() return { } end },
|
||||
default = gg.list {
|
||||
_M.expr, separators = ",", terminators = M.block_terminators } }
|
||||
|
||||
|
||||
local for_vars_list = gg.list{
|
||||
name = "for variables list",
|
||||
primary = _M.id,
|
||||
separators = ",",
|
||||
terminators = "in" }
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
-- for header, between [for] and [do] (exclusive).
|
||||
-- Return the `Forxxx{...} AST, without the body element (the last one).
|
||||
----------------------------------------------------------------------------
|
||||
function M.for_header (lx)
|
||||
local vars = M.id_list(lx)
|
||||
if lx :is_keyword (lx:peek(), "=") then
|
||||
if #vars ~= 1 then
|
||||
gg.parse_error (lx, "numeric for only accepts one variable")
|
||||
end
|
||||
lx:next() -- skip "="
|
||||
local exprs = M.expr_list (lx)
|
||||
if #exprs < 2 or #exprs > 3 then
|
||||
gg.parse_error (lx, "numeric for requires 2 or 3 boundaries")
|
||||
end
|
||||
return { tag="Fornum", vars[1], unpack (exprs) }
|
||||
else
|
||||
if not lx :is_keyword (lx :next(), "in") then
|
||||
gg.parse_error (lx, '"=" or "in" expected in for loop')
|
||||
end
|
||||
local exprs = M.expr_list (lx)
|
||||
return { tag="Forin", vars, exprs }
|
||||
end
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
-- Function def parser helper: id ( . id ) *
|
||||
----------------------------------------------------------------------------
|
||||
local function fn_builder (list)
|
||||
local acc = list[1]
|
||||
local first = acc.lineinfo.first
|
||||
for i = 2, #list do
|
||||
local index = M.id2string(list[i])
|
||||
local li = lexer.new_lineinfo(first, index.lineinfo.last)
|
||||
acc = { tag="Index", acc, index, lineinfo=li }
|
||||
end
|
||||
return acc
|
||||
end
|
||||
local func_name = gg.list{ _M.id, separators = ".", builder = fn_builder }
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
-- Function def parser helper: ( : id )?
|
||||
----------------------------------------------------------------------------
|
||||
local method_name = gg.onkeyword{ name = "method invocation", ":", _M.id,
|
||||
transformers = { function(x) return x and x.tag=='Id' and M.id2string(x) end } }
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
-- Function def builder
|
||||
----------------------------------------------------------------------------
|
||||
local function funcdef_builder(x)
|
||||
local name, method, func = unpack(x)
|
||||
if method then
|
||||
name = { tag="Index", name, method,
|
||||
lineinfo = {
|
||||
first = name.lineinfo.first,
|
||||
last = method.lineinfo.last } }
|
||||
table.insert (func[1], 1, {tag="Id", "self"})
|
||||
end
|
||||
local r = { tag="Set", {name}, {func} }
|
||||
r[1].lineinfo = name.lineinfo
|
||||
r[2].lineinfo = func.lineinfo
|
||||
return r
|
||||
end
|
||||
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
-- if statement builder
|
||||
----------------------------------------------------------------------------
|
||||
local function if_builder (x)
|
||||
local cond_block_pairs, else_block, r = x[1], x[2], {tag="If"}
|
||||
local n_pairs = #cond_block_pairs
|
||||
for i = 1, n_pairs do
|
||||
local cond, block = unpack(cond_block_pairs[i])
|
||||
r[2*i-1], r[2*i] = cond, block
|
||||
end
|
||||
if else_block then table.insert(r, #r+1, else_block) end
|
||||
return r
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- produce a list of (expr,block) pairs
|
||||
--------------------------------------------------------------------------------
|
||||
local elseifs_parser = gg.list {
|
||||
gg.sequence { _M.expr, "then", _M.block , name='elseif parser' },
|
||||
separators = "elseif",
|
||||
terminators = { "else", "end" }
|
||||
}
|
||||
|
||||
local annot_expr = gg.sequence {
|
||||
_M.expr,
|
||||
gg.onkeyword{ "#", gg.future(M, 'annot').tf },
|
||||
builder = function(x)
|
||||
local e, a = unpack(x)
|
||||
if a then return { tag='Annot', e, a }
|
||||
else return e end
|
||||
end }
|
||||
|
||||
local annot_expr_list = gg.list {
|
||||
primary = annot.opt(M, _M.expr, 'tf'), separators = ',' }
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- assignments and calls: statements that don't start with a keyword
|
||||
------------------------------------------------------------------------
|
||||
local function assign_or_call_stat_parser (lx)
|
||||
local e = annot_expr_list (lx)
|
||||
local a = lx:is_keyword(lx:peek())
|
||||
local op = a and M.assignments[a]
|
||||
-- TODO: refactor annotations
|
||||
if op then
|
||||
--FIXME: check that [e] is a LHS
|
||||
lx :next()
|
||||
local annots
|
||||
e, annots = annot.split(e)
|
||||
local v = M.expr_list (lx)
|
||||
if type(op)=="string" then return { tag=op, e, v, annots }
|
||||
else return op (e, v) end
|
||||
else
|
||||
assert (#e > 0)
|
||||
if #e > 1 then
|
||||
gg.parse_error (lx,
|
||||
"comma is not a valid statement separator; statement can be "..
|
||||
"separated by semicolons, or not separated at all")
|
||||
elseif e[1].tag ~= "Call" and e[1].tag ~= "Invoke" then
|
||||
local typename
|
||||
if e[1].tag == 'Id' then
|
||||
typename = '("'..e[1][1]..'") is an identifier'
|
||||
elseif e[1].tag == 'Op' then
|
||||
typename = "is an arithmetic operation"
|
||||
else typename = "is of type '"..(e[1].tag or "<list>").."'" end
|
||||
gg.parse_error (lx,
|
||||
"This expression %s; "..
|
||||
"a statement was expected, and only function and method call "..
|
||||
"expressions can be used as statements", typename);
|
||||
end
|
||||
return e[1]
|
||||
end
|
||||
end
|
||||
|
||||
M.local_stat_parser = gg.multisequence{
|
||||
-- local function <name> <func_val>
|
||||
{ "function", _M.id, _M.func_val, builder =
|
||||
function(x)
|
||||
local vars = { x[1], lineinfo = x[1].lineinfo }
|
||||
local vals = { x[2], lineinfo = x[2].lineinfo }
|
||||
return { tag="Localrec", vars, vals }
|
||||
end },
|
||||
-- local <id_list> ( = <expr_list> )?
|
||||
default = gg.sequence{
|
||||
gg.list{
|
||||
primary = annot.opt(M, _M.id, 'tf'),
|
||||
separators = ',' },
|
||||
gg.onkeyword{ "=", _M.expr_list },
|
||||
builder = function(x)
|
||||
local annotated_left, right = unpack(x)
|
||||
local left, annotations = annot.split(annotated_left)
|
||||
return {tag="Local", left, right or { }, annotations }
|
||||
end } }
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- statement
|
||||
------------------------------------------------------------------------
|
||||
M.stat = gg.multisequence {
|
||||
name = "statement",
|
||||
{ "do", _M.block, "end", builder =
|
||||
function (x) return { tag="Do", unpack (x[1]) } end },
|
||||
{ "for", _M.for_header, "do", _M.block, "end", builder =
|
||||
function (x) x[1][#x[1]+1] = x[2]; return x[1] end },
|
||||
{ "function", func_name, method_name, _M.func_val, builder=funcdef_builder },
|
||||
{ "while", _M.expr, "do", _M.block, "end", builder = "While" },
|
||||
{ "repeat", _M.block, "until", _M.expr, builder = "Repeat" },
|
||||
{ "local", _M.local_stat_parser, builder = unpack },
|
||||
{ "return", return_expr_list_parser, builder =
|
||||
function(x) x[1].tag='Return'; return x[1] end },
|
||||
{ "break", builder = function() return { tag="Break" } end },
|
||||
{ "-{", gg.future(M, 'meta').splice_content, "}", builder = unpack },
|
||||
{ "if", gg.nonempty(elseifs_parser), gg.onkeyword{ "else", M.block }, "end",
|
||||
builder = if_builder },
|
||||
default = assign_or_call_stat_parser }
|
||||
|
||||
M.assignments = {
|
||||
["="] = "Set"
|
||||
}
|
||||
|
||||
function M.assignments:add(k, v) self[k] = v end
|
||||
|
||||
return M
|
||||
end
|
||||
@@ -0,0 +1,77 @@
|
||||
--------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2006-2013 Fabien Fleutot and others.
|
||||
--
|
||||
-- All rights reserved.
|
||||
--
|
||||
-- This program and the accompanying materials are made available
|
||||
-- under the terms of the Eclipse Public License v1.0 which
|
||||
-- accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- This program and the accompanying materials are also made available
|
||||
-- under the terms of the MIT public license which accompanies this
|
||||
-- distribution, and is available at http://www.lua.org/license.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Fabien Fleutot - API and implementation
|
||||
--
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
--
|
||||
-- Exported API:
|
||||
-- * [M.table_bracket_field()]
|
||||
-- * [M.table_field()]
|
||||
-- * [M.table_content()]
|
||||
-- * [M.table()]
|
||||
--
|
||||
-- KNOWN BUG: doesn't handle final ";" or "," before final "}"
|
||||
--
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local gg = require 'metalua.grammar.generator'
|
||||
|
||||
return function(M)
|
||||
|
||||
M.table = { }
|
||||
local _table = gg.future(M.table)
|
||||
local _expr = gg.future(M).expr
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- `[key] = value` table field definition
|
||||
--------------------------------------------------------------------------------
|
||||
M.table.bracket_pair = gg.sequence{ "[", _expr, "]", "=", _expr, builder = "Pair" }
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- table element parser: list value, `id = value` pair or `[value] = value` pair.
|
||||
--------------------------------------------------------------------------------
|
||||
function M.table.element (lx)
|
||||
if lx :is_keyword (lx :peek(), "[") then return M.table.bracket_pair(lx) end
|
||||
local e = M.expr (lx)
|
||||
if not lx :is_keyword (lx :peek(), "=") then return e end
|
||||
lx :next(); -- skip the "="
|
||||
local key = M.id2string(e) -- will fail on non-identifiers
|
||||
local val = M.expr(lx)
|
||||
local r = { tag="Pair", key, val }
|
||||
r.lineinfo = { first = key.lineinfo.first, last = val.lineinfo.last }
|
||||
return r
|
||||
end
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- table constructor, without enclosing braces; returns a full table object
|
||||
-----------------------------------------------------------------------------
|
||||
M.table.content = gg.list {
|
||||
-- eta expansion to allow patching the element definition
|
||||
primary = _table.element,
|
||||
separators = { ",", ";" },
|
||||
terminators = "}",
|
||||
builder = "Table" }
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- complete table constructor including [{...}]
|
||||
--------------------------------------------------------------------------------
|
||||
-- TODO beware, stat and expr use only table.content, this can't be patched.
|
||||
M.table.table = gg.sequence{ "{", _table.content, "}", builder = unpack }
|
||||
|
||||
return M
|
||||
end
|
||||
@@ -0,0 +1,282 @@
|
||||
-------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2006-2013 Fabien Fleutot and others.
|
||||
--
|
||||
-- All rights reserved.
|
||||
--
|
||||
-- This program and the accompanying materials are made available
|
||||
-- under the terms of the Eclipse Public License v1.0 which
|
||||
-- accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- This program and the accompanying materials are also made available
|
||||
-- under the terms of the MIT public license which accompanies this
|
||||
-- distribution, and is available at http://www.lua.org/license.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Fabien Fleutot - API and implementation
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
--
|
||||
-- This extension implements list comprehensions, similar to Haskell and
|
||||
-- Python syntax, to easily describe lists.
|
||||
--
|
||||
-- * x[a ... b] is the list { x[a], x[a+1], ..., x[b] }
|
||||
-- * { f()..., b } contains all the elements returned by f(), then b
|
||||
-- (allows to expand list fields other than the last one)
|
||||
-- * list comprehensions a la python, with "for" and "if" suffixes:
|
||||
-- {i+10*j for i=1,3 for j=1,3 if i~=j} is { 21, 31, 12, 32, 13, 23 }
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
-{ extension ("match", ...) }
|
||||
|
||||
local SUPPORT_IMPROVED_LOOPS = true
|
||||
local SUPPORT_IMPROVED_INDEXES = false -- depends on deprecated table.isub
|
||||
local SUPPORT_CONTINUE = true
|
||||
local SUPPORT_COMP_LISTS = true
|
||||
|
||||
assert (SUPPORT_IMPROVED_LOOPS or not SUPPORT_CONTINUE,
|
||||
"Can't support 'continue' without improved loop headers")
|
||||
|
||||
local gg = require 'metalua.grammar.generator'
|
||||
local Q = require 'metalua.treequery'
|
||||
|
||||
local function dots_list_suffix_builder (x) return `DotsSuffix{ x } end
|
||||
|
||||
local function for_list_suffix_builder (list_element, suffix)
|
||||
local new_header = suffix[1]
|
||||
match list_element with
|
||||
| `Comp{ _, acc } -> table.insert (acc, new_header); return list_element
|
||||
| _ -> return `Comp{ list_element, { new_header } }
|
||||
end
|
||||
end
|
||||
|
||||
local function if_list_suffix_builder (list_element, suffix)
|
||||
local new_header = `If{ suffix[1] }
|
||||
match list_element with
|
||||
| `Comp{ _, acc } -> table.insert (acc, new_header); return list_element
|
||||
| _ -> return `Comp{ list_element, { new_header } }
|
||||
end
|
||||
end
|
||||
|
||||
-- Builds a statement from a table element, which adds this element to
|
||||
-- a table `t`, potentially thanks to an alias `tinsert` to
|
||||
-- `table.insert`.
|
||||
-- @param core the part around which the loops are built.
|
||||
-- either `DotsSuffix{expr}, `Pair{ expr } or a plain expression
|
||||
-- @param list comprehension suffixes, in the order in which they appear
|
||||
-- either `Forin{ ... } or `Fornum{ ...} or `If{ ... }. In each case,
|
||||
-- it misses a last child node as its body.
|
||||
-- @param t a variable containing the table to fill
|
||||
-- @param tinsert a variable containing `table.insert`.
|
||||
--
|
||||
-- @return fill a statement which fills empty table `t` with the denoted element
|
||||
local function comp_list_builder(core, list, t, tinsert)
|
||||
local filler
|
||||
-- 1 - Build the loop's core: if it has suffix "...", every elements of the
|
||||
-- multi-return must be inserted, hence the extra [for] loop.
|
||||
match core with
|
||||
| `DotsSuffix{ element } ->
|
||||
local x = gg.gensym()
|
||||
filler = +{stat: for _, -{x} in pairs{ -{element} } do (-{tinsert})(-{t}, -{x}) end }
|
||||
| `Pair{ key, value } ->
|
||||
--filler = +{ -{t}[-{key}] = -{value} }
|
||||
filler = `Set{ { `Index{ t, key } }, { value } }
|
||||
| _ -> filler = +{ (-{tinsert})(-{t}, -{core}) }
|
||||
end
|
||||
|
||||
-- 2 - Stack the `if` and `for` control structures, from outside to inside.
|
||||
-- This is done in a destructive way for the elements of [list].
|
||||
for i = #list, 1, -1 do
|
||||
table.insert (list[i], {filler})
|
||||
filler = list[i]
|
||||
end
|
||||
|
||||
return filler
|
||||
end
|
||||
|
||||
local function table_content_builder (list)
|
||||
local special = false -- Does the table need a special builder?
|
||||
for _, element in ipairs(list) do
|
||||
local etag = element.tag
|
||||
if etag=='Comp' or etag=='DotsSuffix' then special=true; break end
|
||||
end
|
||||
if not special then list.tag='Table'; return list end
|
||||
|
||||
local t, tinsert = gg.gensym 'table', gg.gensym 'table_insert'
|
||||
local filler_block = { +{stat: local -{t}, -{tinsert} = { }, table.insert } }
|
||||
for _, element in ipairs(list) do
|
||||
local filler
|
||||
match element with
|
||||
| `Comp{ core, comp } -> filler = comp_list_builder(core, comp, t, tinsert)
|
||||
| _ -> filler = comp_list_builder(element, { }, t, tinsert)
|
||||
end
|
||||
table.insert(filler_block, filler)
|
||||
end
|
||||
return `Stat{ filler_block, t }
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Back-end for improved index operator.
|
||||
local function index_builder(a, suffix)
|
||||
match suffix[1] with
|
||||
-- Single index, no range: keep the native semantics
|
||||
| { { e, false } } -> return `Index{ a, e }
|
||||
-- Either a range, or multiple indexes, or both
|
||||
| ranges ->
|
||||
local r = `Call{ +{table.isub}, a }
|
||||
local function acc (x,y) table.insert (r,x); table.insert (r,y) end
|
||||
for _, seq in ipairs (ranges) do
|
||||
match seq with
|
||||
| { e, false } -> acc(e,e)
|
||||
| { e, f } -> acc(e,f)
|
||||
end
|
||||
end
|
||||
return r
|
||||
end
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------
|
||||
-- Find continue statements in a loop body, change them into goto
|
||||
-- end-of-body.
|
||||
local function transform_continue_statements(body)
|
||||
local continue_statements = Q(body)
|
||||
:if_unknown() -- tolerate unknown 'Continue' statements
|
||||
:not_under ('Forin', 'Fornum', 'While', 'Repeat')
|
||||
:filter ('Continue')
|
||||
:list()
|
||||
if next(continue_statements) then
|
||||
local continue_label = gg.gensym 'continue' [1]
|
||||
table.insert(body, `Label{ continue_label })
|
||||
for _, statement in ipairs(continue_statements) do
|
||||
statement.tag = 'Goto'
|
||||
statement[1] = continue_label
|
||||
end
|
||||
return true
|
||||
else return false end
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Back-end for loops with a multi-element header
|
||||
local function loop_builder(x)
|
||||
local first, elements, body = unpack(x)
|
||||
|
||||
-- Change continue statements into gotos.
|
||||
if SUPPORT_CONTINUE then transform_continue_statements(body) end
|
||||
|
||||
-------------------------------------------------------------------
|
||||
-- If it's a regular loop, don't bloat the code
|
||||
if not next(elements) then
|
||||
table.insert(first, body)
|
||||
return first
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------
|
||||
-- There's no reason to treat the first element in a special way
|
||||
table.insert(elements, 1, first)
|
||||
|
||||
-------------------------------------------------------------------
|
||||
-- Change breaks into gotos that escape all loops at once.
|
||||
local exit_label = nil
|
||||
local function break_to_goto(break_node)
|
||||
if not exit_label then exit_label = gg.gensym 'break' [1] end
|
||||
break_node = break_node or { }
|
||||
break_node.tag = 'Goto'
|
||||
break_node[1] = exit_label
|
||||
return break_node
|
||||
end
|
||||
Q(body)
|
||||
:not_under('Function', 'Forin', 'Fornum', 'While', 'Repeat')
|
||||
:filter('Break')
|
||||
:foreach (break_to_goto)
|
||||
|
||||
-------------------------------------------------------------------
|
||||
-- Compile all headers elements, from last to first.
|
||||
-- invariant: `body` is a block (not a statement)
|
||||
local result = body
|
||||
for i = #elements, 1, -1 do
|
||||
local e = elements[i]
|
||||
match e with
|
||||
| `If{ cond } ->
|
||||
result = { `If{ cond, result } }
|
||||
| `Until{ cond } ->
|
||||
result = +{block: if -{cond} then -{break_to_goto()} else -{result} end }
|
||||
| `While{ cond } ->
|
||||
if i==1 then result = { `While{ cond, result } } -- top-level while
|
||||
else result = +{block: if -{cond} then -{result} else -{break_to_goto()} end } end
|
||||
| `Forin{ ... } | `Fornum{ ... } ->
|
||||
table.insert (e, result); result={e}
|
||||
| _-> require'metalua.pprint'.printf("Bad loop header element %s", e)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-------------------------------------------------------------------
|
||||
-- If some breaks had to be changed into gotos, insert the label
|
||||
if exit_label then result = { result, `Label{ exit_label } } end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Improved "[...]" index operator:
|
||||
-- * support for multi-indexes ("foo[bar, gnat]")
|
||||
-- * support for ranges ("foo[bar ... gnat]")
|
||||
--------------------------------------------------------------------------------
|
||||
local function extend(M)
|
||||
|
||||
local _M = gg.future(M)
|
||||
|
||||
if SUPPORT_COMP_LISTS then
|
||||
-- support for "for" / "if" comprehension suffixes in literal tables
|
||||
local original_table_element = M.table.element
|
||||
M.table.element = gg.expr{ name="table cell",
|
||||
primary = original_table_element,
|
||||
suffix = { name="table cell suffix",
|
||||
{ "...", builder = dots_list_suffix_builder },
|
||||
{ "for", _M.for_header, builder = for_list_suffix_builder },
|
||||
{ "if", _M.expr, builder = if_list_suffix_builder } } }
|
||||
M.table.content.builder = table_content_builder
|
||||
end
|
||||
|
||||
if SUPPORT_IMPROVED_INDEXES then
|
||||
-- Support for ranges and multiple indices in bracket suffixes
|
||||
M.expr.suffix:del '['
|
||||
M.expr.suffix:add{ name="table index/range",
|
||||
"[", gg.list{
|
||||
gg.sequence { _M.expr, gg.onkeyword{ "...", _M.expr } } ,
|
||||
separators = { ",", ";" } },
|
||||
"]", builder = index_builder }
|
||||
end
|
||||
|
||||
if SUPPORT_IMPROVED_LOOPS then
|
||||
local original_for_header = M.for_header
|
||||
M.stat :del 'for'
|
||||
M.stat :del 'while'
|
||||
|
||||
M.loop_suffix = gg.multisequence{
|
||||
{ 'while', _M.expr, builder = |x| `Until{ `Op{ 'not', x[1] } } },
|
||||
{ 'until', _M.expr, builder = |x| `Until{ x[1] } },
|
||||
{ 'if', _M.expr, builder = |x| `If{ x[1] } },
|
||||
{ 'for', original_for_header, builder = |x| x[1] } }
|
||||
|
||||
M.loop_suffix_list = gg.list{ _M.loop_suffix, terminators='do' }
|
||||
|
||||
M.stat :add{
|
||||
'for', original_for_header, _M.loop_suffix_list, 'do', _M.block, 'end',
|
||||
builder = loop_builder }
|
||||
|
||||
M.stat :add{
|
||||
'while', _M.expr, _M.loop_suffix_list, 'do', _M.block, 'end',
|
||||
builder = |x| loop_builder{ `While{x[1]}, x[2], x[3] } }
|
||||
end
|
||||
|
||||
if SUPPORT_CONTINUE then
|
||||
M.lexer :add 'continue'
|
||||
M.stat :add{ 'continue', builder='Continue' }
|
||||
end
|
||||
end
|
||||
|
||||
return extend
|
||||
@@ -0,0 +1,400 @@
|
||||
-------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2006-2013 Fabien Fleutot and others.
|
||||
--
|
||||
-- All rights reserved.
|
||||
--
|
||||
-- This program and the accompanying materials are made available
|
||||
-- under the terms of the Eclipse Public License v1.0 which
|
||||
-- accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- This program and the accompanying materials are also made available
|
||||
-- under the terms of the MIT public license which accompanies this
|
||||
-- distribution, and is available at http://www.lua.org/license.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Fabien Fleutot - API and implementation
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
--
|
||||
-- Glossary:
|
||||
--
|
||||
-- * term_seq: the tested stuff, a sequence of terms
|
||||
-- * pattern_element: might match one term of a term seq. Represented
|
||||
-- as expression ASTs.
|
||||
-- * pattern_seq: might match a term_seq
|
||||
-- * pattern_group: several pattern seqs, one of them might match
|
||||
-- the term seq.
|
||||
-- * case: pattern_group * guard option * block
|
||||
-- * match_statement: tested term_seq * case list
|
||||
--
|
||||
-- Hence a complete match statement is a:
|
||||
--
|
||||
-- { list(expr), list{ list(list(expr)), expr or false, block } }
|
||||
--
|
||||
-- Implementation hints
|
||||
-- ====================
|
||||
--
|
||||
-- The implementation is made as modular as possible, so that parts
|
||||
-- can be reused in other extensions. The priviledged way to share
|
||||
-- contextual information across functions is through the 'cfg' table
|
||||
-- argument. Its fields include:
|
||||
--
|
||||
-- * code: code generated from pattern. A pattern_(element|seq|group)
|
||||
-- is compiled as a sequence of instructions which will jump to
|
||||
-- label [cfg.on_failure] if the tested term doesn't match.
|
||||
--
|
||||
-- * on_failure: name of the label where the code will jump if the
|
||||
-- pattern doesn't match
|
||||
--
|
||||
-- * locals: names of local variables used by the pattern. This
|
||||
-- includes bound variables, and temporary variables used to
|
||||
-- destructure tables. Names are stored as keys of the table,
|
||||
-- values are meaningless.
|
||||
--
|
||||
-- * after_success: label where the code must jump after a pattern
|
||||
-- succeeded to capture a term, and the guard suceeded if there is
|
||||
-- any, and the conditional block has run.
|
||||
--
|
||||
-- * ntmp: number of temporary variables used to destructurate table
|
||||
-- in the current case.
|
||||
--
|
||||
-- Code generation is performed by acc_xxx() functions, which accumulate
|
||||
-- code in cfg.code:
|
||||
--
|
||||
-- * acc_test(test, cfg) will generate a jump to cfg.on_failure
|
||||
-- *when the test returns TRUE*
|
||||
--
|
||||
-- * acc_stat accumulates a statement
|
||||
--
|
||||
-- * acc_assign accumulate an assignment statement, and makes sure that
|
||||
-- the LHS variable the registered as local in cfg.locals.
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
-- TODO: hygiene wrt type()
|
||||
-- TODO: cfg.ntmp isn't reset as often as it could. I'm not even sure
|
||||
-- the corresponding locals are declared.
|
||||
|
||||
local checks = require 'checks'
|
||||
local gg = require 'metalua.grammar.generator'
|
||||
local pp = require 'metalua.pprint'
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- This would have been best done through library 'metalua.walk',
|
||||
-- but walk depends on match, so we have to break the dependency.
|
||||
-- It replaces all instances of `...' in `ast' with `term', unless
|
||||
-- it appears in a function.
|
||||
----------------------------------------------------------------------
|
||||
local function replace_dots (ast, term)
|
||||
local function rec (node)
|
||||
for i, child in ipairs(node) do
|
||||
if type(child)~="table" then -- pass
|
||||
elseif child.tag=='Dots' then
|
||||
if term=='ambiguous' then
|
||||
error ("You can't use `...' on the right of a match case when it appears "..
|
||||
"more than once on the left")
|
||||
else node[i] = term end
|
||||
elseif child.tag=='Function' then return nil
|
||||
else rec(child) end
|
||||
end
|
||||
end
|
||||
return rec(ast)
|
||||
end
|
||||
|
||||
local tmpvar_base = gg.gensym 'submatch.' [1]
|
||||
|
||||
local function next_tmpvar(cfg)
|
||||
assert (cfg.ntmp, "No cfg.ntmp imbrication level in the match compiler")
|
||||
cfg.ntmp = cfg.ntmp+1
|
||||
return `Id{ tmpvar_base .. cfg.ntmp }
|
||||
end
|
||||
|
||||
-- Code accumulators
|
||||
local acc_stat = |x,cfg| table.insert (cfg.code, x)
|
||||
local acc_test = |x,cfg| acc_stat(+{stat: if -{x} then -{`Goto{cfg.on_failure}} end}, cfg)
|
||||
-- lhs :: `Id{ string }
|
||||
-- rhs :: expr
|
||||
local function acc_assign (lhs, rhs, cfg)
|
||||
assert(lhs.tag=='Id')
|
||||
cfg.locals[lhs[1]] = true
|
||||
acc_stat (`Set{ {lhs}, {rhs} }, cfg)
|
||||
end
|
||||
|
||||
local literal_tags = { String=1, Number=1, True=1, False=1, Nil=1 }
|
||||
|
||||
-- pattern :: `Id{ string }
|
||||
-- term :: expr
|
||||
local function id_pattern_element_builder (pattern, term, cfg)
|
||||
assert (pattern.tag == "Id")
|
||||
if pattern[1] == "_" then
|
||||
-- "_" is used as a dummy var ==> no assignment, no == checking
|
||||
cfg.locals._ = true
|
||||
elseif cfg.locals[pattern[1]] then
|
||||
-- This var is already bound ==> test for equality
|
||||
acc_test (+{ -{term} ~= -{pattern} }, cfg)
|
||||
else
|
||||
-- Free var ==> bind it, and remember it for latter linearity checking
|
||||
acc_assign (pattern, term, cfg)
|
||||
cfg.locals[pattern[1]] = true
|
||||
end
|
||||
end
|
||||
|
||||
-- mutually recursive with table_pattern_element_builder
|
||||
local pattern_element_builder
|
||||
|
||||
-- pattern :: pattern and `Table{ }
|
||||
-- term :: expr
|
||||
local function table_pattern_element_builder (pattern, term, cfg)
|
||||
local seen_dots, len = false, 0
|
||||
acc_test (+{ type( -{term} ) ~= "table" }, cfg)
|
||||
for i = 1, #pattern do
|
||||
local key, sub_pattern
|
||||
if pattern[i].tag=="Pair" then -- Explicit key/value pair
|
||||
key, sub_pattern = unpack (pattern[i])
|
||||
assert (literal_tags[key.tag], "Invalid key")
|
||||
else -- Implicit key
|
||||
len, key, sub_pattern = len+1, `Number{ len+1 }, pattern[i]
|
||||
end
|
||||
|
||||
-- '...' can only appear in final position
|
||||
-- Could be fixed actually...
|
||||
assert (not seen_dots, "Wrongly placed `...' ")
|
||||
|
||||
if sub_pattern.tag == "Id" then
|
||||
-- Optimization: save a useless [ v(n+1)=v(n).key ]
|
||||
id_pattern_element_builder (sub_pattern, `Index{ term, key }, cfg)
|
||||
if sub_pattern[1] ~= "_" then
|
||||
acc_test (+{ -{sub_pattern} == nil }, cfg)
|
||||
end
|
||||
elseif sub_pattern.tag == "Dots" then
|
||||
-- Remember where the capture is, and thatt arity checking shouldn't occur
|
||||
seen_dots = true
|
||||
else
|
||||
-- Business as usual:
|
||||
local v2 = next_tmpvar(cfg)
|
||||
acc_assign (v2, `Index{ term, key }, cfg)
|
||||
pattern_element_builder (sub_pattern, v2, cfg)
|
||||
-- TODO: restore ntmp?
|
||||
end
|
||||
end
|
||||
if seen_dots then -- remember how to retrieve `...'
|
||||
-- FIXME: check, but there might be cases where the variable -{term}
|
||||
-- will be overridden in contrieved tables.
|
||||
-- ==> save it now, and clean the setting statement if unused
|
||||
if cfg.dots_replacement then cfg.dots_replacement = 'ambiguous'
|
||||
else cfg.dots_replacement = +{ select (-{`Number{len}}, unpack(-{term})) } end
|
||||
else -- Check arity
|
||||
acc_test (+{ #-{term} ~= -{`Number{len}} }, cfg)
|
||||
end
|
||||
end
|
||||
|
||||
-- mutually recursive with pattern_element_builder
|
||||
local eq_pattern_element_builder, regexp_pattern_element_builder
|
||||
|
||||
-- Concatenate code in [cfg.code], that will jump to label
|
||||
-- [cfg.on_failure] if [pattern] doesn't match [term]. [pattern]
|
||||
-- should be an identifier, or at least cheap to compute and
|
||||
-- side-effects free.
|
||||
--
|
||||
-- pattern :: pattern_element
|
||||
-- term :: expr
|
||||
function pattern_element_builder (pattern, term, cfg)
|
||||
if literal_tags[pattern.tag] then
|
||||
acc_test (+{ -{term} ~= -{pattern} }, cfg)
|
||||
elseif "Id" == pattern.tag then
|
||||
id_pattern_element_builder (pattern, term, cfg)
|
||||
elseif "Op" == pattern.tag and "div" == pattern[1] then
|
||||
regexp_pattern_element_builder (pattern, term, cfg)
|
||||
elseif "Op" == pattern.tag and "eq" == pattern[1] then
|
||||
eq_pattern_element_builder (pattern, term, cfg)
|
||||
elseif "Table" == pattern.tag then
|
||||
table_pattern_element_builder (pattern, term, cfg)
|
||||
else
|
||||
error ("Invalid pattern at "..
|
||||
tostring(pattern.lineinfo)..
|
||||
": "..pp.tostring(pattern, {hide_hash=true}))
|
||||
end
|
||||
end
|
||||
|
||||
function eq_pattern_element_builder (pattern, term, cfg)
|
||||
local _, pat1, pat2 = unpack (pattern)
|
||||
local ntmp_save = cfg.ntmp
|
||||
pattern_element_builder (pat1, term, cfg)
|
||||
cfg.ntmp = ntmp_save
|
||||
pattern_element_builder (pat2, term, cfg)
|
||||
end
|
||||
|
||||
-- pattern :: `Op{ 'div', string, list{`Id string} or `Id{ string }}
|
||||
-- term :: expr
|
||||
local function regexp_pattern_element_builder (pattern, term, cfg)
|
||||
local op, regexp, sub_pattern = unpack(pattern)
|
||||
|
||||
-- Sanity checks --
|
||||
assert (op=='div', "Don't know what to do with that op in a pattern")
|
||||
assert (regexp.tag=="String",
|
||||
"Left hand side operand for '/' in a pattern must be "..
|
||||
"a literal string representing a regular expression")
|
||||
if sub_pattern.tag=="Table" then
|
||||
for _, x in ipairs(sub_pattern) do
|
||||
assert (x.tag=="Id" or x.tag=='Dots',
|
||||
"Right hand side operand for '/' in a pattern must be "..
|
||||
"a list of identifiers")
|
||||
end
|
||||
else
|
||||
assert (sub_pattern.tag=="Id",
|
||||
"Right hand side operand for '/' in a pattern must be "..
|
||||
"an identifier or a list of identifiers")
|
||||
end
|
||||
|
||||
-- Regexp patterns can only match strings
|
||||
acc_test (+{ type(-{term}) ~= 'string' }, cfg)
|
||||
-- put all captures in a list
|
||||
local capt_list = +{ { string.strmatch(-{term}, -{regexp}) } }
|
||||
-- save them in a var_n for recursive decomposition
|
||||
local v2 = next_tmpvar(cfg)
|
||||
acc_stat (+{stat: local -{v2} = -{capt_list} }, cfg)
|
||||
-- was capture successful?
|
||||
acc_test (+{ not next (-{v2}) }, cfg)
|
||||
pattern_element_builder (sub_pattern, v2, cfg)
|
||||
end
|
||||
|
||||
|
||||
-- Jumps to [cfg.on_faliure] if pattern_seq doesn't match
|
||||
-- term_seq.
|
||||
local function pattern_seq_builder (pattern_seq, term_seq, cfg)
|
||||
if #pattern_seq ~= #term_seq then error ("Bad seq arity") end
|
||||
cfg.locals = { } -- reset bound variables between alternatives
|
||||
for i=1, #pattern_seq do
|
||||
cfg.ntmp = 1 -- reset the tmp var generator
|
||||
pattern_element_builder(pattern_seq[i], term_seq[i], cfg)
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------
|
||||
-- for each case i:
|
||||
-- pattern_seq_builder_i:
|
||||
-- * on failure, go to on_failure_i
|
||||
-- * on success, go to on_success
|
||||
-- label on_success:
|
||||
-- block
|
||||
-- goto after_success
|
||||
-- label on_failure_i
|
||||
--------------------------------------------------
|
||||
local function case_builder (case, term_seq, cfg)
|
||||
local patterns_group, guard, block = unpack(case)
|
||||
local on_success = gg.gensym 'on_success' [1]
|
||||
for i = 1, #patterns_group do
|
||||
local pattern_seq = patterns_group[i]
|
||||
cfg.on_failure = gg.gensym 'match_fail' [1]
|
||||
cfg.dots_replacement = false
|
||||
pattern_seq_builder (pattern_seq, term_seq, cfg)
|
||||
if i<#patterns_group then
|
||||
acc_stat (`Goto{on_success}, cfg)
|
||||
acc_stat (`Label{cfg.on_failure}, cfg)
|
||||
end
|
||||
end
|
||||
acc_stat (`Label{on_success}, cfg)
|
||||
if guard then acc_test (+{not -{guard}}, cfg) end
|
||||
if cfg.dots_replacement then
|
||||
replace_dots (block, cfg.dots_replacement)
|
||||
end
|
||||
block.tag = 'Do'
|
||||
acc_stat (block, cfg)
|
||||
acc_stat (`Goto{cfg.after_success}, cfg)
|
||||
acc_stat (`Label{cfg.on_failure}, cfg)
|
||||
end
|
||||
|
||||
local function match_builder (x)
|
||||
local term_seq, cases = unpack(x)
|
||||
local cfg = {
|
||||
code = `Do{ },
|
||||
after_success = gg.gensym "_after_success" }
|
||||
|
||||
|
||||
-- Some sharing issues occur when modifying term_seq,
|
||||
-- so it's replaced by a copy new_term_seq.
|
||||
-- TODO: clean that up, and re-suppress the useless copies
|
||||
-- (cf. remarks about capture bug below).
|
||||
local new_term_seq = { }
|
||||
|
||||
local match_locals
|
||||
|
||||
-- Make sure that all tested terms are variables or literals
|
||||
for i=1, #term_seq do
|
||||
local t = term_seq[i]
|
||||
-- Capture problem: the following would compile wrongly:
|
||||
-- `match x with x -> end'
|
||||
-- Temporary workaround: suppress the condition, so that
|
||||
-- all external variables are copied into unique names.
|
||||
--if t.tag ~= 'Id' and not literal_tags[t.tag] then
|
||||
local v = gg.gensym 'v'
|
||||
if not match_locals then match_locals = `Local{ {v}, {t} } else
|
||||
table.insert(match_locals[1], v)
|
||||
table.insert(match_locals[2], t)
|
||||
end
|
||||
new_term_seq[i] = v
|
||||
--end
|
||||
end
|
||||
term_seq = new_term_seq
|
||||
|
||||
if match_locals then acc_stat(match_locals, cfg) end
|
||||
|
||||
for i=1, #cases do
|
||||
local case_cfg = {
|
||||
after_success = cfg.after_success,
|
||||
code = `Do{ }
|
||||
-- locals = { } -- unnecessary, done by pattern_seq_builder
|
||||
}
|
||||
case_builder (cases[i], term_seq, case_cfg)
|
||||
if next (case_cfg.locals) then
|
||||
local case_locals = { }
|
||||
table.insert (case_cfg.code, 1, `Local{ case_locals, { } })
|
||||
for v, _ in pairs (case_cfg.locals) do
|
||||
table.insert (case_locals, `Id{ v })
|
||||
end
|
||||
end
|
||||
acc_stat(case_cfg.code, cfg)
|
||||
end
|
||||
local li = `String{tostring(cases.lineinfo)}
|
||||
acc_stat(+{error('mismatch at '..-{li})}, cfg)
|
||||
acc_stat(`Label{cfg.after_success}, cfg)
|
||||
return cfg.code
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Syntactical front-end
|
||||
----------------------------------------------------------------------
|
||||
|
||||
local function extend(M)
|
||||
|
||||
local _M = gg.future(M)
|
||||
|
||||
checks('metalua.compiler.parser')
|
||||
M.lexer:add{ "match", "with", "->" }
|
||||
M.block.terminators:add "|"
|
||||
|
||||
local match_cases_list_parser = gg.list{ name = "match cases list",
|
||||
gg.sequence{ name = "match case",
|
||||
gg.list{ name = "match case patterns list",
|
||||
primary = _M.expr_list,
|
||||
separators = "|",
|
||||
terminators = { "->", "if" } },
|
||||
gg.onkeyword{ "if", _M.expr, consume = true },
|
||||
"->",
|
||||
_M.block },
|
||||
separators = "|",
|
||||
terminators = "end" }
|
||||
|
||||
M.stat:add{ name = "match statement",
|
||||
"match",
|
||||
_M.expr_list,
|
||||
"with", gg.optkeyword "|",
|
||||
match_cases_list_parser,
|
||||
"end",
|
||||
builder = |x| match_builder{ x[1], x[3] } }
|
||||
end
|
||||
|
||||
return extend
|
||||
@@ -0,0 +1,834 @@
|
||||
--------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2006-2013 Fabien Fleutot and others.
|
||||
--
|
||||
-- All rights reserved.
|
||||
--
|
||||
-- This program and the accompanying materials are made available
|
||||
-- under the terms of the Eclipse Public License v1.0 which
|
||||
-- accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- This program and the accompanying materials are also made available
|
||||
-- under the terms of the MIT public license which accompanies this
|
||||
-- distribution, and is available at http://www.lua.org/license.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Fabien Fleutot - API and implementation
|
||||
--
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
--
|
||||
-- Summary: parser generator. Collection of higher order functors,
|
||||
-- which allow to build and combine parsers. Relies on a lexer
|
||||
-- that supports the same API as the one exposed in mll.lua.
|
||||
--
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
--
|
||||
-- Exported API:
|
||||
--
|
||||
-- Parser generators:
|
||||
-- * [gg.sequence()]
|
||||
-- * [gg.multisequence()]
|
||||
-- * [gg.expr()]
|
||||
-- * [gg.list()]
|
||||
-- * [gg.onkeyword()]
|
||||
-- * [gg.optkeyword()]
|
||||
--
|
||||
-- Other functions:
|
||||
-- * [gg.parse_error()]
|
||||
-- * [gg.make_parser()]
|
||||
-- * [gg.is_parser()]
|
||||
--
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local M = { }
|
||||
|
||||
local checks = require 'checks'
|
||||
local lexer = require 'metalua.grammar.lexer'
|
||||
local pp = require 'metalua.pprint'
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Symbol generator: [gensym()] returns a guaranteed-to-be-unique identifier.
|
||||
-- The main purpose is to avoid variable capture in macros.
|
||||
--
|
||||
-- If a string is passed as an argument, theis string will be part of the
|
||||
-- id name (helpful for macro debugging)
|
||||
--------------------------------------------------------------------------------
|
||||
local gensymidx = 0
|
||||
|
||||
function M.gensym (arg)
|
||||
gensymidx = gensymidx + 1
|
||||
return { tag="Id", string.format(".%i.%s", gensymidx, arg or "")}
|
||||
end
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- parser metatable, which maps __call to method parse, and adds some
|
||||
-- error tracing boilerplate.
|
||||
-------------------------------------------------------------------------------
|
||||
local parser_metatable = { }
|
||||
|
||||
function parser_metatable :__call (lx, ...)
|
||||
return self :parse (lx, ...)
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Turn a table into a parser, mainly by setting the metatable.
|
||||
-------------------------------------------------------------------------------
|
||||
function M.make_parser(kind, p)
|
||||
p.kind = kind
|
||||
if not p.transformers then p.transformers = { } end
|
||||
function p.transformers:add (x)
|
||||
table.insert (self, x)
|
||||
end
|
||||
setmetatable (p, parser_metatable)
|
||||
return p
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Return true iff [x] is a parser.
|
||||
-- If it's a gg-generated parser, return the name of its kind.
|
||||
-------------------------------------------------------------------------------
|
||||
function M.is_parser (x)
|
||||
return type(x)=="function" or getmetatable(x)==parser_metatable and x.kind
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Parse a sequence, without applying builder nor transformers.
|
||||
-------------------------------------------------------------------------------
|
||||
local function raw_parse_sequence (lx, p)
|
||||
local r = { }
|
||||
for i=1, #p do
|
||||
local e=p[i]
|
||||
if type(e) == "string" then
|
||||
local kw = lx :next()
|
||||
if not lx :is_keyword (kw, e) then
|
||||
M.parse_error(
|
||||
lx, "A keyword was expected, probably `%s'.", e)
|
||||
end
|
||||
elseif M.is_parser (e) then
|
||||
table.insert (r, e(lx))
|
||||
else -- Invalid parser definition, this is *not* a parsing error
|
||||
error(string.format(
|
||||
"Sequence `%s': element #%i is neither a string nor a parser: %s",
|
||||
p.name, i, pp.tostring(e)))
|
||||
end
|
||||
end
|
||||
return r
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Parse a multisequence, without applying multisequence transformers.
|
||||
-- The sequences are completely parsed.
|
||||
-------------------------------------------------------------------------------
|
||||
local function raw_parse_multisequence (lx, sequence_table, default)
|
||||
local seq_parser = sequence_table[lx:is_keyword(lx:peek())]
|
||||
if seq_parser then return seq_parser (lx)
|
||||
elseif default then return default (lx)
|
||||
else return false end
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Applies all transformers listed in parser on ast.
|
||||
-------------------------------------------------------------------------------
|
||||
local function transform (ast, parser, fli, lli)
|
||||
if parser.transformers then
|
||||
for _, t in ipairs (parser.transformers) do ast = t(ast) or ast end
|
||||
end
|
||||
if type(ast) == 'table' then
|
||||
local ali = ast.lineinfo
|
||||
if not ali or ali.first~=fli or ali.last~=lli then
|
||||
ast.lineinfo = lexer.new_lineinfo(fli, lli)
|
||||
end
|
||||
end
|
||||
return ast
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Generate a tracable parsing error (not implemented yet)
|
||||
-------------------------------------------------------------------------------
|
||||
function M.parse_error(lx, fmt, ...)
|
||||
local li = lx:lineinfo_left()
|
||||
local file, line, column, offset, positions
|
||||
if li then
|
||||
file, line, column, offset = li.source, li.line, li.column, li.offset
|
||||
positions = { first = li, last = li }
|
||||
else
|
||||
line, column, offset = -1, -1, -1
|
||||
end
|
||||
|
||||
local msg = string.format("line %i, char %i: "..fmt, line, column, ...)
|
||||
if file and file~='?' then msg = "file "..file..", "..msg end
|
||||
|
||||
local src = lx.src
|
||||
if offset>0 and src then
|
||||
local i, j = offset, offset
|
||||
while src:sub(i,i) ~= '\n' and i>=0 do i=i-1 end
|
||||
while src:sub(j,j) ~= '\n' and j<=#src do j=j+1 end
|
||||
local srcline = src:sub (i+1, j-1)
|
||||
local idx = string.rep (" ", column).."^"
|
||||
msg = string.format("%s\n>>> %s\n>>> %s", msg, srcline, idx)
|
||||
end
|
||||
--lx :kill()
|
||||
error(msg)
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
--
|
||||
-- Sequence parser generator
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
-- Input fields:
|
||||
--
|
||||
-- * [builder]: how to build an AST out of sequence parts. let [x] be the list
|
||||
-- of subparser results (keywords are simply omitted). [builder] can be:
|
||||
-- - [nil], in which case the result of parsing is simply [x]
|
||||
-- - a string, which is then put as a tag on [x]
|
||||
-- - a function, which takes [x] as a parameter and returns an AST.
|
||||
--
|
||||
-- * [name]: the name of the parser. Used for debug messages
|
||||
--
|
||||
-- * [transformers]: a list of AST->AST functions, applied in order on ASTs
|
||||
-- returned by the parser.
|
||||
--
|
||||
-- * Table-part entries corresponds to keywords (strings) and subparsers
|
||||
-- (function and callable objects).
|
||||
--
|
||||
-- After creation, the following fields are added:
|
||||
-- * [parse] the parsing function lexer->AST
|
||||
-- * [kind] == "sequence"
|
||||
-- * [name] is set, if it wasn't in the input.
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
function M.sequence (p)
|
||||
M.make_parser ("sequence", p)
|
||||
|
||||
-------------------------------------------------------------------
|
||||
-- Parsing method
|
||||
-------------------------------------------------------------------
|
||||
function p:parse (lx)
|
||||
|
||||
-- Raw parsing:
|
||||
local fli = lx:lineinfo_right()
|
||||
local seq = raw_parse_sequence (lx, self)
|
||||
local lli = lx:lineinfo_left()
|
||||
|
||||
-- Builder application:
|
||||
local builder, tb = self.builder, type (self.builder)
|
||||
if tb == "string" then seq.tag = builder
|
||||
elseif tb == "function" or builder and builder.__call then seq = builder(seq)
|
||||
elseif builder == nil then -- nothing
|
||||
else error ("Invalid builder of type "..tb.." in sequence") end
|
||||
seq = transform (seq, self, fli, lli)
|
||||
assert (not seq or seq.lineinfo)
|
||||
return seq
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------
|
||||
-- Construction
|
||||
-------------------------------------------------------------------
|
||||
-- Try to build a proper name
|
||||
if p.name then
|
||||
-- don't touch existing name
|
||||
elseif type(p[1])=="string" then -- find name based on 1st keyword
|
||||
if #p==1 then p.name=p[1]
|
||||
elseif type(p[#p])=="string" then
|
||||
p.name = p[1] .. " ... " .. p[#p]
|
||||
else p.name = p[1] .. " ..." end
|
||||
else -- can't find a decent name
|
||||
p.name = "unnamed_sequence"
|
||||
end
|
||||
|
||||
return p
|
||||
end --</sequence>
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
--
|
||||
-- Multiple, keyword-driven, sequence parser generator
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
-- in [p], useful fields are:
|
||||
--
|
||||
-- * [transformers]: as usual
|
||||
--
|
||||
-- * [name]: as usual
|
||||
--
|
||||
-- * Table-part entries must be sequence parsers, or tables which can
|
||||
-- be turned into a sequence parser by [gg.sequence]. These
|
||||
-- sequences must start with a keyword, and this initial keyword
|
||||
-- must be different for each sequence. The table-part entries will
|
||||
-- be removed after [gg.multisequence] returns.
|
||||
--
|
||||
-- * [default]: the parser to run if the next keyword in the lexer is
|
||||
-- none of the registered initial keywords. If there's no default
|
||||
-- parser and no suitable initial keyword, the multisequence parser
|
||||
-- simply returns [false].
|
||||
--
|
||||
-- After creation, the following fields are added:
|
||||
--
|
||||
-- * [parse] the parsing function lexer->AST
|
||||
--
|
||||
-- * [sequences] the table of sequences, indexed by initial keywords.
|
||||
--
|
||||
-- * [add] method takes a sequence parser or a config table for
|
||||
-- [gg.sequence], and adds/replaces the corresponding sequence
|
||||
-- parser. If the keyword was already used, the former sequence is
|
||||
-- removed and a warning is issued.
|
||||
--
|
||||
-- * [get] method returns a sequence by its initial keyword
|
||||
--
|
||||
-- * [kind] == "multisequence"
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
function M.multisequence (p)
|
||||
M.make_parser ("multisequence", p)
|
||||
|
||||
-------------------------------------------------------------------
|
||||
-- Add a sequence (might be just a config table for [gg.sequence])
|
||||
-------------------------------------------------------------------
|
||||
function p :add (s)
|
||||
-- compile if necessary:
|
||||
local keyword = type(s)=='table' and s[1]
|
||||
if type(s)=='table' and not M.is_parser(s) then M.sequence(s) end
|
||||
if M.is_parser(s)~='sequence' or type(keyword)~='string' then
|
||||
if self.default then -- two defaults
|
||||
error ("In a multisequence parser, all but one sequences "..
|
||||
"must start with a keyword")
|
||||
else self.default = s end -- first default
|
||||
else
|
||||
if self.sequences[keyword] then -- duplicate keyword
|
||||
-- TODO: warn that initial keyword `keyword` is overloaded in multiseq
|
||||
end
|
||||
self.sequences[keyword] = s
|
||||
end
|
||||
end -- </multisequence.add>
|
||||
|
||||
-------------------------------------------------------------------
|
||||
-- Get the sequence starting with this keyword. [kw :: string]
|
||||
-------------------------------------------------------------------
|
||||
function p :get (kw) return self.sequences [kw] end
|
||||
|
||||
-------------------------------------------------------------------
|
||||
-- Remove the sequence starting with keyword [kw :: string]
|
||||
-------------------------------------------------------------------
|
||||
function p :del (kw)
|
||||
if not self.sequences[kw] then
|
||||
-- TODO: warn that we try to delete a non-existent entry
|
||||
end
|
||||
local removed = self.sequences[kw]
|
||||
self.sequences[kw] = nil
|
||||
return removed
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------
|
||||
-- Parsing method
|
||||
-------------------------------------------------------------------
|
||||
function p :parse (lx)
|
||||
local fli = lx:lineinfo_right()
|
||||
local x = raw_parse_multisequence (lx, self.sequences, self.default)
|
||||
local lli = lx:lineinfo_left()
|
||||
return transform (x, self, fli, lli)
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------
|
||||
-- Construction
|
||||
-------------------------------------------------------------------
|
||||
-- Register the sequences passed to the constructor. They're going
|
||||
-- from the array part of the parser to the hash part of field
|
||||
-- [sequences]
|
||||
p.sequences = { }
|
||||
for i=1, #p do p :add (p[i]); p[i] = nil end
|
||||
|
||||
-- FIXME: why is this commented out?
|
||||
--if p.default and not is_parser(p.default) then sequence(p.default) end
|
||||
return p
|
||||
end --</multisequence>
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
--
|
||||
-- Expression parser generator
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
--
|
||||
-- Expression configuration relies on three tables: [prefix], [infix]
|
||||
-- and [suffix]. Moreover, the primary parser can be replaced by a
|
||||
-- table: in this case the [primary] table will be passed to
|
||||
-- [gg.multisequence] to create a parser.
|
||||
--
|
||||
-- Each of these tables is a modified multisequence parser: the
|
||||
-- differences with respect to regular multisequence config tables are:
|
||||
--
|
||||
-- * the builder takes specific parameters:
|
||||
-- - for [prefix], it takes the result of the prefix sequence parser,
|
||||
-- and the prefixed expression
|
||||
-- - for [infix], it takes the left-hand-side expression, the results
|
||||
-- of the infix sequence parser, and the right-hand-side expression.
|
||||
-- - for [suffix], it takes the suffixed expression, and the result
|
||||
-- of the suffix sequence parser.
|
||||
--
|
||||
-- * the default field is a list, with parameters:
|
||||
-- - [parser] the raw parsing function
|
||||
-- - [transformers], as usual
|
||||
-- - [prec], the operator's precedence
|
||||
-- - [assoc] for [infix] table, the operator's associativity, which
|
||||
-- can be "left", "right" or "flat" (default to left)
|
||||
--
|
||||
-- In [p], useful fields are:
|
||||
-- * [transformers]: as usual
|
||||
-- * [name]: as usual
|
||||
-- * [primary]: the atomic expression parser, or a multisequence config
|
||||
-- table (mandatory)
|
||||
-- * [prefix]: prefix operators config table, see above.
|
||||
-- * [infix]: infix operators config table, see above.
|
||||
-- * [suffix]: suffix operators config table, see above.
|
||||
--
|
||||
-- After creation, these fields are added:
|
||||
-- * [kind] == "expr"
|
||||
-- * [parse] as usual
|
||||
-- * each table is turned into a multisequence, and therefore has an
|
||||
-- [add] method
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
function M.expr (p)
|
||||
M.make_parser ("expr", p)
|
||||
|
||||
-------------------------------------------------------------------
|
||||
-- parser method.
|
||||
-- In addition to the lexer, it takes an optional precedence:
|
||||
-- it won't read expressions whose precedence is lower or equal
|
||||
-- to [prec].
|
||||
-------------------------------------------------------------------
|
||||
function p :parse (lx, prec)
|
||||
prec = prec or 0
|
||||
|
||||
------------------------------------------------------
|
||||
-- Extract the right parser and the corresponding
|
||||
-- options table, for (pre|in|suff)fix operators.
|
||||
-- Options include prec, assoc, transformers.
|
||||
------------------------------------------------------
|
||||
local function get_parser_info (tab)
|
||||
local p2 = tab :get (lx :is_keyword (lx :peek()))
|
||||
if p2 then -- keyword-based sequence found
|
||||
local function parser(lx) return raw_parse_sequence(lx, p2) end
|
||||
return parser, p2
|
||||
else -- Got to use the default parser
|
||||
local d = tab.default
|
||||
if d then return d.parse or d.parser, d
|
||||
else return false, false end
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------
|
||||
-- Look for a prefix sequence. Multiple prefixes are
|
||||
-- handled through the recursive [p.parse] call.
|
||||
-- Notice the double-transform: one for the primary
|
||||
-- expr, and one for the one with the prefix op.
|
||||
------------------------------------------------------
|
||||
local function handle_prefix ()
|
||||
local fli = lx :lineinfo_right()
|
||||
local p2_func, p2 = get_parser_info (self.prefix)
|
||||
local op = p2_func and p2_func (lx)
|
||||
if op then -- Keyword-based sequence found
|
||||
local ili = lx :lineinfo_right() -- Intermediate LineInfo
|
||||
local e = p2.builder (op, self :parse (lx, p2.prec))
|
||||
local lli = lx :lineinfo_left()
|
||||
return transform (transform (e, p2, ili, lli), self, fli, lli)
|
||||
else -- No prefix found, get a primary expression
|
||||
local e = self.primary(lx)
|
||||
local lli = lx :lineinfo_left()
|
||||
return transform (e, self, fli, lli)
|
||||
end
|
||||
end --</expr.parse.handle_prefix>
|
||||
|
||||
------------------------------------------------------
|
||||
-- Look for an infix sequence+right-hand-side operand.
|
||||
-- Return the whole binary expression result,
|
||||
-- or false if no operator was found.
|
||||
------------------------------------------------------
|
||||
local function handle_infix (e)
|
||||
local p2_func, p2 = get_parser_info (self.infix)
|
||||
if not p2 then return false end
|
||||
|
||||
-----------------------------------------
|
||||
-- Handle flattening operators: gather all operands
|
||||
-- of the series in [list]; when a different operator
|
||||
-- is found, stop, build from [list], [transform] and
|
||||
-- return.
|
||||
-----------------------------------------
|
||||
if (not p2.prec or p2.prec>prec) and p2.assoc=="flat" then
|
||||
local fli = lx:lineinfo_right()
|
||||
local pflat, list = p2, { e }
|
||||
repeat
|
||||
local op = p2_func(lx)
|
||||
if not op then break end
|
||||
table.insert (list, self:parse (lx, p2.prec))
|
||||
local _ -- We only care about checking that p2==pflat
|
||||
_, p2 = get_parser_info (self.infix)
|
||||
until p2 ~= pflat
|
||||
local e2 = pflat.builder (list)
|
||||
local lli = lx:lineinfo_left()
|
||||
return transform (transform (e2, pflat, fli, lli), self, fli, lli)
|
||||
|
||||
-----------------------------------------
|
||||
-- Handle regular infix operators: [e] the LHS is known,
|
||||
-- just gather the operator and [e2] the RHS.
|
||||
-- Result goes in [e3].
|
||||
-----------------------------------------
|
||||
elseif p2.prec and p2.prec>prec or
|
||||
p2.prec==prec and p2.assoc=="right" then
|
||||
local fli = e.lineinfo.first -- lx:lineinfo_right()
|
||||
local op = p2_func(lx)
|
||||
if not op then return false end
|
||||
local e2 = self:parse (lx, p2.prec)
|
||||
local e3 = p2.builder (e, op, e2)
|
||||
local lli = lx:lineinfo_left()
|
||||
return transform (transform (e3, p2, fli, lli), self, fli, lli)
|
||||
|
||||
-----------------------------------------
|
||||
-- Check for non-associative operators, and complain if applicable.
|
||||
-----------------------------------------
|
||||
elseif p2.assoc=="none" and p2.prec==prec then
|
||||
M.parse_error (lx, "non-associative operator!")
|
||||
|
||||
-----------------------------------------
|
||||
-- No infix operator suitable at that precedence
|
||||
-----------------------------------------
|
||||
else return false end
|
||||
|
||||
end --</expr.parse.handle_infix>
|
||||
|
||||
------------------------------------------------------
|
||||
-- Look for a suffix sequence.
|
||||
-- Return the result of suffix operator on [e],
|
||||
-- or false if no operator was found.
|
||||
------------------------------------------------------
|
||||
local function handle_suffix (e)
|
||||
-- FIXME bad fli, must take e.lineinfo.first
|
||||
local p2_func, p2 = get_parser_info (self.suffix)
|
||||
if not p2 then return false end
|
||||
if not p2.prec or p2.prec>=prec then
|
||||
--local fli = lx:lineinfo_right()
|
||||
local fli = e.lineinfo.first
|
||||
local op = p2_func(lx)
|
||||
if not op then return false end
|
||||
local lli = lx:lineinfo_left()
|
||||
e = p2.builder (e, op)
|
||||
e = transform (transform (e, p2, fli, lli), self, fli, lli)
|
||||
return e
|
||||
end
|
||||
return false
|
||||
end --</expr.parse.handle_suffix>
|
||||
|
||||
------------------------------------------------------
|
||||
-- Parser body: read suffix and (infix+operand)
|
||||
-- extensions as long as we're able to fetch more at
|
||||
-- this precedence level.
|
||||
------------------------------------------------------
|
||||
local e = handle_prefix()
|
||||
repeat
|
||||
local x = handle_suffix (e); e = x or e
|
||||
local y = handle_infix (e); e = y or e
|
||||
until not (x or y)
|
||||
|
||||
-- No transform: it already happened in operators handling
|
||||
return e
|
||||
end --</expr.parse>
|
||||
|
||||
-------------------------------------------------------------------
|
||||
-- Construction
|
||||
-------------------------------------------------------------------
|
||||
if not p.primary then p.primary=p[1]; p[1]=nil end
|
||||
for _, t in ipairs{ "primary", "prefix", "infix", "suffix" } do
|
||||
if not p[t] then p[t] = { } end
|
||||
if not M.is_parser(p[t]) then M.multisequence(p[t]) end
|
||||
end
|
||||
function p:add(...) return self.primary:add(...) end
|
||||
return p
|
||||
end --</expr>
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
--
|
||||
-- List parser generator
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
-- In [p], the following fields can be provided in input:
|
||||
--
|
||||
-- * [builder]: takes list of subparser results, returns AST
|
||||
-- * [transformers]: as usual
|
||||
-- * [name]: as usual
|
||||
--
|
||||
-- * [terminators]: list of strings representing the keywords which
|
||||
-- might mark the end of the list. When non-empty, the list is
|
||||
-- allowed to be empty. A string is treated as a single-element
|
||||
-- table, whose element is that string, e.g. ["do"] is the same as
|
||||
-- [{"do"}].
|
||||
--
|
||||
-- * [separators]: list of strings representing the keywords which can
|
||||
-- separate elements of the list. When non-empty, one of these
|
||||
-- keyword has to be found between each element. Lack of a separator
|
||||
-- indicates the end of the list. A string is treated as a
|
||||
-- single-element table, whose element is that string, e.g. ["do"]
|
||||
-- is the same as [{"do"}]. If [terminators] is empty/nil, then
|
||||
-- [separators] has to be non-empty.
|
||||
--
|
||||
-- After creation, the following fields are added:
|
||||
-- * [parse] the parsing function lexer->AST
|
||||
-- * [kind] == "list"
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
function M.list (p)
|
||||
M.make_parser ("list", p)
|
||||
|
||||
-------------------------------------------------------------------
|
||||
-- Parsing method
|
||||
-------------------------------------------------------------------
|
||||
function p :parse (lx)
|
||||
|
||||
------------------------------------------------------
|
||||
-- Used to quickly check whether there's a terminator
|
||||
-- or a separator immediately ahead
|
||||
------------------------------------------------------
|
||||
local function peek_is_in (keywords)
|
||||
return keywords and lx:is_keyword(lx:peek(), unpack(keywords)) end
|
||||
|
||||
local x = { }
|
||||
local fli = lx :lineinfo_right()
|
||||
|
||||
-- if there's a terminator to start with, don't bother trying
|
||||
local is_empty_list = self.terminators and (peek_is_in (self.terminators) or lx:peek().tag=="Eof")
|
||||
if not is_empty_list then
|
||||
repeat
|
||||
local item = self.primary(lx)
|
||||
table.insert (x, item) -- read one element
|
||||
until
|
||||
-- There's a separator list specified, and next token isn't in it.
|
||||
-- Otherwise, consume it with [lx:next()]
|
||||
self.separators and not(peek_is_in (self.separators) and lx:next()) or
|
||||
-- Terminator token ahead
|
||||
peek_is_in (self.terminators) or
|
||||
-- Last reason: end of file reached
|
||||
lx:peek().tag=="Eof"
|
||||
end
|
||||
|
||||
local lli = lx:lineinfo_left()
|
||||
|
||||
-- Apply the builder. It can be a string, or a callable value,
|
||||
-- or simply nothing.
|
||||
local b = self.builder
|
||||
if b then
|
||||
if type(b)=="string" then x.tag = b -- b is a string, use it as a tag
|
||||
elseif type(b)=="function" then x=b(x)
|
||||
else
|
||||
local bmt = getmetatable(b)
|
||||
if bmt and bmt.__call then x=b(x) end
|
||||
end
|
||||
end
|
||||
return transform (x, self, fli, lli)
|
||||
end --</list.parse>
|
||||
|
||||
-------------------------------------------------------------------
|
||||
-- Construction
|
||||
-------------------------------------------------------------------
|
||||
if not p.primary then p.primary = p[1]; p[1] = nil end
|
||||
if type(p.terminators) == "string" then p.terminators = { p.terminators }
|
||||
elseif p.terminators and #p.terminators == 0 then p.terminators = nil end
|
||||
if type(p.separators) == "string" then p.separators = { p.separators }
|
||||
elseif p.separators and #p.separators == 0 then p.separators = nil end
|
||||
|
||||
return p
|
||||
end --</list>
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
--
|
||||
-- Keyword-conditioned parser generator
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
--
|
||||
-- Only apply a parser if a given keyword is found. The result of
|
||||
-- [gg.onkeyword] parser is the result of the subparser (modulo
|
||||
-- [transformers] applications).
|
||||
--
|
||||
-- lineinfo: the keyword is *not* included in the boundaries of the
|
||||
-- resulting lineinfo. A review of all usages of gg.onkeyword() in the
|
||||
-- implementation of metalua has shown that it was the appropriate choice
|
||||
-- in every case.
|
||||
--
|
||||
-- Input fields:
|
||||
--
|
||||
-- * [name]: as usual
|
||||
--
|
||||
-- * [transformers]: as usual
|
||||
--
|
||||
-- * [peek]: if non-nil, the conditioning keyword is left in the lexeme
|
||||
-- stream instead of being consumed.
|
||||
--
|
||||
-- * [primary]: the subparser.
|
||||
--
|
||||
-- * [keywords]: list of strings representing triggering keywords.
|
||||
--
|
||||
-- * Table-part entries can contain strings, and/or exactly one parser.
|
||||
-- Strings are put in [keywords], and the parser is put in [primary].
|
||||
--
|
||||
-- After the call, the following fields will be set:
|
||||
--
|
||||
-- * [parse] the parsing method
|
||||
-- * [kind] == "onkeyword"
|
||||
-- * [primary]
|
||||
-- * [keywords]
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
function M.onkeyword (p)
|
||||
M.make_parser ("onkeyword", p)
|
||||
|
||||
-------------------------------------------------------------------
|
||||
-- Parsing method
|
||||
-------------------------------------------------------------------
|
||||
function p :parse (lx)
|
||||
if lx :is_keyword (lx:peek(), unpack(self.keywords)) then
|
||||
local fli = lx:lineinfo_right()
|
||||
if not self.peek then lx:next() end
|
||||
local content = self.primary (lx)
|
||||
local lli = lx:lineinfo_left()
|
||||
local li = content.lineinfo or { }
|
||||
fli, lli = li.first or fli, li.last or lli
|
||||
return transform (content, p, fli, lli)
|
||||
else return false end
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------
|
||||
-- Construction
|
||||
-------------------------------------------------------------------
|
||||
if not p.keywords then p.keywords = { } end
|
||||
for _, x in ipairs(p) do
|
||||
if type(x)=="string" then table.insert (p.keywords, x)
|
||||
else assert (not p.primary and M.is_parser (x)); p.primary = x end
|
||||
end
|
||||
assert (next (p.keywords), "Missing trigger keyword in gg.onkeyword")
|
||||
assert (p.primary, 'no primary parser in gg.onkeyword')
|
||||
return p
|
||||
end --</onkeyword>
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
--
|
||||
-- Optional keyword consummer pseudo-parser generator
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
--
|
||||
-- This doesn't return a real parser, just a function. That function parses
|
||||
-- one of the keywords passed as parameters, and returns it. It returns
|
||||
-- [false] if no matching keyword is found.
|
||||
--
|
||||
-- Notice that tokens returned by lexer already carry lineinfo, therefore
|
||||
-- there's no need to add them, as done usually through transform() calls.
|
||||
-------------------------------------------------------------------------------
|
||||
function M.optkeyword (...)
|
||||
local args = {...}
|
||||
if type (args[1]) == "table" then
|
||||
assert (#args == 1)
|
||||
args = args[1]
|
||||
end
|
||||
for _, v in ipairs(args) do assert (type(v)=="string") end
|
||||
return function (lx)
|
||||
local x = lx:is_keyword (lx:peek(), unpack (args))
|
||||
if x then lx:next(); return x
|
||||
else return false end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
--
|
||||
-- Run a parser with a special lexer
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
--
|
||||
-- This doesn't return a real parser, just a function.
|
||||
-- First argument is the lexer class to be used with the parser,
|
||||
-- 2nd is the parser itself.
|
||||
-- The resulting parser returns whatever the argument parser does.
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
function M.with_lexer(new_lexer, parser)
|
||||
|
||||
-------------------------------------------------------------------
|
||||
-- Most gg functions take their parameters in a table, so it's
|
||||
-- better to silently accept when with_lexer{ } is called with
|
||||
-- its arguments in a list:
|
||||
-------------------------------------------------------------------
|
||||
if not parser and #new_lexer==2 and type(new_lexer[1])=='table' then
|
||||
return M.with_lexer(unpack(new_lexer))
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------
|
||||
-- Save the current lexer, switch it for the new one, run the parser,
|
||||
-- restore the previous lexer, even if the parser caused an error.
|
||||
-------------------------------------------------------------------
|
||||
return function (lx)
|
||||
local old_lexer = getmetatable(lx)
|
||||
lx:sync()
|
||||
setmetatable(lx, new_lexer)
|
||||
local status, result = pcall(parser, lx)
|
||||
lx:sync()
|
||||
setmetatable(lx, old_lexer)
|
||||
if status then return result else error(result) end
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
--
|
||||
-- Make sure a parser is used and returns successfully.
|
||||
--
|
||||
--------------------------------------------------------------------------------
|
||||
function M.nonempty(primary)
|
||||
local p = M.make_parser('non-empty list', { primary = primary, name=primary.name })
|
||||
function p :parse (lx)
|
||||
local fli = lx:lineinfo_right()
|
||||
local content = self.primary (lx)
|
||||
local lli = lx:lineinfo_left()
|
||||
local li = content.lineinfo or { }
|
||||
fli, lli = li.first or fli, li.last or lli
|
||||
if #content == 0 then
|
||||
M.parse_error (lx, "`%s' must not be empty.", self.name or "list")
|
||||
else
|
||||
return transform (content, self, fli, lli)
|
||||
end
|
||||
end
|
||||
return p
|
||||
end
|
||||
|
||||
local FUTURE_MT = { }
|
||||
function FUTURE_MT:__tostring() return "<Proxy parser module>" end
|
||||
function FUTURE_MT:__newindex(key, value) error "don't write in futures" end
|
||||
function FUTURE_MT :__index (parser_name)
|
||||
return function(...)
|
||||
local p, m = rawget(self, '__path'), self.__module
|
||||
if p then for _, name in ipairs(p) do
|
||||
m=rawget(m, name)
|
||||
if not m then error ("Submodule '"..name.."' undefined") end
|
||||
end end
|
||||
local f = rawget(m, parser_name)
|
||||
if not f then error ("Parser '"..parser_name.."' undefined") end
|
||||
return f(...)
|
||||
end
|
||||
end
|
||||
|
||||
function M.future(module, ...)
|
||||
checks('table')
|
||||
local path = ... and {...}
|
||||
if path then for _, x in ipairs(path) do
|
||||
assert(type(x)=='string', "Bad future arg")
|
||||
end end
|
||||
local self = { __module = module,
|
||||
__path = path }
|
||||
return setmetatable(self, FUTURE_MT)
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,672 @@
|
||||
-------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2006-2013 Fabien Fleutot and others.
|
||||
--
|
||||
-- All rights reserved.
|
||||
--
|
||||
-- This program and the accompanying materials are made available
|
||||
-- under the terms of the Eclipse Public License v1.0 which
|
||||
-- accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- This program and the accompanying materials are also made available
|
||||
-- under the terms of the MIT public license which accompanies this
|
||||
-- distribution, and is available at http://www.lua.org/license.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Fabien Fleutot - API and implementation
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
local checks = require 'checks'
|
||||
|
||||
local M = { }
|
||||
|
||||
local lexer = { alpha={ }, sym={ } }
|
||||
lexer.__index=lexer
|
||||
lexer.__type='lexer.stream'
|
||||
|
||||
M.lexer = lexer
|
||||
|
||||
|
||||
local debugf = function() end
|
||||
-- local debugf=printf
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Some locale settings produce bad results, e.g. French locale
|
||||
-- expect float numbers to use commas instead of periods.
|
||||
-- TODO: change number parser into something loclae-independent,
|
||||
-- locales are nasty.
|
||||
----------------------------------------------------------------------
|
||||
os.setlocale('C')
|
||||
|
||||
local MT = { }
|
||||
|
||||
M.metatables=MT
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Create a new metatable, for a new class of objects.
|
||||
----------------------------------------------------------------------
|
||||
local function new_metatable(name)
|
||||
local mt = { __type = 'lexer.'..name };
|
||||
mt.__index = mt
|
||||
MT[name] = mt
|
||||
end
|
||||
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Position: represent a point in a source file.
|
||||
----------------------------------------------------------------------
|
||||
new_metatable 'position'
|
||||
|
||||
local position_idx=1
|
||||
|
||||
function M.new_position(line, column, offset, source)
|
||||
checks('number', 'number', 'number', 'string')
|
||||
local id = position_idx; position_idx = position_idx+1
|
||||
return setmetatable({line=line, column=column, offset=offset,
|
||||
source=source, id=id}, MT.position)
|
||||
end
|
||||
|
||||
function MT.position :__tostring()
|
||||
return string.format("<%s%s|L%d|C%d|K%d>",
|
||||
self.comments and "C|" or "",
|
||||
self.source, self.line, self.column, self.offset)
|
||||
end
|
||||
|
||||
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Position factory: convert offsets into line/column/offset positions.
|
||||
----------------------------------------------------------------------
|
||||
new_metatable 'position_factory'
|
||||
|
||||
function M.new_position_factory(src, src_name)
|
||||
-- assert(type(src)=='string')
|
||||
-- assert(type(src_name)=='string')
|
||||
local lines = { 1 }
|
||||
for offset in src :gmatch '\n()' do table.insert(lines, offset) end
|
||||
local max = #src+1
|
||||
table.insert(lines, max+1) -- +1 includes Eof
|
||||
return setmetatable({ src_name=src_name, line2offset=lines, max=max },
|
||||
MT.position_factory)
|
||||
end
|
||||
|
||||
function MT.position_factory :get_position (offset)
|
||||
-- assert(type(offset)=='number')
|
||||
assert(offset<=self.max)
|
||||
local line2offset = self.line2offset
|
||||
local left = self.last_left or 1
|
||||
if offset<line2offset[left] then left=1 end
|
||||
local right = left+1
|
||||
if line2offset[right]<=offset then right = right+1 end
|
||||
if line2offset[right]<=offset then right = #line2offset end
|
||||
while true do
|
||||
-- print (" trying lines "..left.."/"..right..", offsets "..line2offset[left]..
|
||||
-- "/"..line2offset[right].." for offset "..offset)
|
||||
-- assert(line2offset[left]<=offset)
|
||||
-- assert(offset<line2offset[right])
|
||||
-- assert(left<right)
|
||||
if left+1==right then break end
|
||||
local middle = math.floor((left+right)/2)
|
||||
if line2offset[middle]<=offset then left=middle else right=middle end
|
||||
end
|
||||
-- assert(left+1==right)
|
||||
-- printf("found that offset %d is between %d and %d, hence on line %d",
|
||||
-- offset, line2offset[left], line2offset[right], left)
|
||||
local line = left
|
||||
local column = offset - line2offset[line] + 1
|
||||
self.last_left = left
|
||||
return M.new_position(line, column, offset, self.src_name)
|
||||
end
|
||||
|
||||
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Lineinfo: represent a node's range in a source file;
|
||||
-- embed information about prefix and suffix comments.
|
||||
----------------------------------------------------------------------
|
||||
new_metatable 'lineinfo'
|
||||
|
||||
function M.new_lineinfo(first, last)
|
||||
checks('lexer.position', 'lexer.position')
|
||||
return setmetatable({first=first, last=last}, MT.lineinfo)
|
||||
end
|
||||
|
||||
function MT.lineinfo :__tostring()
|
||||
local fli, lli = self.first, self.last
|
||||
local line = fli.line; if line~=lli.line then line =line ..'-'..lli.line end
|
||||
local column = fli.column; if column~=lli.column then column=column..'-'..lli.column end
|
||||
local offset = fli.offset; if offset~=lli.offset then offset=offset..'-'..lli.offset end
|
||||
return string.format("<%s%s|L%s|C%s|K%s%s>",
|
||||
fli.comments and "C|" or "",
|
||||
fli.source, line, column, offset,
|
||||
lli.comments and "|C" or "")
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Token: atomic Lua language element, with a category, a content,
|
||||
-- and some lineinfo relating it to its original source.
|
||||
----------------------------------------------------------------------
|
||||
new_metatable 'token'
|
||||
|
||||
function M.new_token(tag, content, lineinfo)
|
||||
--printf("TOKEN `%s{ %q, lineinfo = %s} boundaries %d, %d",
|
||||
-- tag, content, tostring(lineinfo), lineinfo.first.id, lineinfo.last.id)
|
||||
return setmetatable({tag=tag, lineinfo=lineinfo, content}, MT.token)
|
||||
end
|
||||
|
||||
function MT.token :__tostring()
|
||||
--return string.format("`%s{ %q, %s }", self.tag, self[1], tostring(self.lineinfo))
|
||||
return string.format("`%s %q", self.tag, self[1])
|
||||
end
|
||||
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Comment: series of comment blocks with associated lineinfo.
|
||||
-- To be attached to the tokens just before and just after them.
|
||||
----------------------------------------------------------------------
|
||||
new_metatable 'comment'
|
||||
|
||||
function M.new_comment(lines)
|
||||
local first = lines[1].lineinfo.first
|
||||
local last = lines[#lines].lineinfo.last
|
||||
local lineinfo = M.new_lineinfo(first, last)
|
||||
return setmetatable({lineinfo=lineinfo, unpack(lines)}, MT.comment)
|
||||
end
|
||||
|
||||
function MT.comment :text()
|
||||
local last_line = self[1].lineinfo.last.line
|
||||
local acc = { }
|
||||
for i, line in ipairs(self) do
|
||||
local nreturns = line.lineinfo.first.line - last_line
|
||||
table.insert(acc, ("\n"):rep(nreturns))
|
||||
table.insert(acc, line[1])
|
||||
end
|
||||
return table.concat(acc)
|
||||
end
|
||||
|
||||
function M.new_comment_line(text, lineinfo, nequals)
|
||||
checks('string', 'lexer.lineinfo', '?number')
|
||||
return { lineinfo = lineinfo, text, nequals }
|
||||
end
|
||||
|
||||
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Patterns used by [lexer :extract] to decompose the raw string into
|
||||
-- correctly tagged tokens.
|
||||
----------------------------------------------------------------------
|
||||
lexer.patterns = {
|
||||
spaces = "^[ \r\n\t]*()",
|
||||
short_comment = "^%-%-([^\n]*)\n?()",
|
||||
--final_short_comment = "^%-%-([^\n]*)()$",
|
||||
long_comment = "^%-%-%[(=*)%[\n?(.-)%]%1%]()",
|
||||
long_string = "^%[(=*)%[\n?(.-)%]%1%]()",
|
||||
number_mantissa = { "^%d+%.?%d*()", "^%d*%.%d+()" },
|
||||
number_mantissa_hex = { "^%x+%.?%x*()", "^%x*%.%x+()" }, --Lua5.1 and Lua5.2
|
||||
number_exponant = "^[eE][%+%-]?%d+()",
|
||||
number_exponant_hex = "^[pP][%+%-]?%d+()", --Lua5.2
|
||||
number_hex = "^0[xX]()",
|
||||
word = "^([%a_][%w_]*)()"
|
||||
}
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- unescape a whole string, applying [unesc_digits] and
|
||||
-- [unesc_letter] as many times as required.
|
||||
----------------------------------------------------------------------
|
||||
local function unescape_string (s)
|
||||
|
||||
-- Turn the digits of an escape sequence into the corresponding
|
||||
-- character, e.g. [unesc_digits("123") == string.char(123)].
|
||||
local function unesc_digits (backslashes, digits)
|
||||
if #backslashes%2==0 then
|
||||
-- Even number of backslashes, they escape each other, not the digits.
|
||||
-- Return them so that unesc_letter() can treat them
|
||||
return backslashes..digits
|
||||
else
|
||||
-- Remove the odd backslash, which escapes the number sequence.
|
||||
-- The rest will be returned and parsed by unesc_letter()
|
||||
backslashes = backslashes :sub (1,-2)
|
||||
end
|
||||
local k, j, i = digits :reverse() :byte(1, 3)
|
||||
local z = string.byte "0"
|
||||
local code = (k or z) + 10*(j or z) + 100*(i or z) - 111*z
|
||||
if code > 255 then
|
||||
error ("Illegal escape sequence '\\"..digits..
|
||||
"' in string: ASCII codes must be in [0..255]")
|
||||
end
|
||||
local c = string.char (code)
|
||||
if c == '\\' then c = '\\\\' end -- parsed by unesc_letter (test: "\092b" --> "\\b")
|
||||
return backslashes..c
|
||||
end
|
||||
|
||||
-- Turn hex digits of escape sequence into char.
|
||||
local function unesc_hex(backslashes, digits)
|
||||
if #backslashes%2==0 then
|
||||
return backslashes..'x'..digits
|
||||
else
|
||||
backslashes = backslashes :sub (1,-2)
|
||||
end
|
||||
local c = string.char(tonumber(digits,16))
|
||||
if c == '\\' then c = '\\\\' end -- parsed by unesc_letter (test: "\x5cb" --> "\\b")
|
||||
return backslashes..c
|
||||
end
|
||||
|
||||
-- Handle Lua 5.2 \z sequences
|
||||
local function unesc_z(backslashes, more)
|
||||
if #backslashes%2==0 then
|
||||
return backslashes..more
|
||||
else
|
||||
return backslashes :sub (1,-2)
|
||||
end
|
||||
end
|
||||
|
||||
-- Take a letter [x], and returns the character represented by the
|
||||
-- sequence ['\\'..x], e.g. [unesc_letter "n" == "\n"].
|
||||
local function unesc_letter(x)
|
||||
local t = {
|
||||
a = "\a", b = "\b", f = "\f",
|
||||
n = "\n", r = "\r", t = "\t", v = "\v",
|
||||
["\\"] = "\\", ["'"] = "'", ['"'] = '"', ["\n"] = "\n" }
|
||||
return t[x] or x
|
||||
end
|
||||
|
||||
s = s: gsub ("(\\+)(z%s*)", unesc_z) -- Lua 5.2
|
||||
s = s: gsub ("(\\+)([0-9][0-9]?[0-9]?)", unesc_digits)
|
||||
s = s: gsub ("(\\+)x([0-9a-fA-F][0-9a-fA-F])", unesc_hex) -- Lua 5.2
|
||||
s = s: gsub ("\\(%D)",unesc_letter)
|
||||
return s
|
||||
end
|
||||
|
||||
lexer.extractors = {
|
||||
"extract_long_comment", "extract_short_comment",
|
||||
"extract_short_string", "extract_word", "extract_number",
|
||||
"extract_long_string", "extract_symbol" }
|
||||
|
||||
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Really extract next token from the raw string
|
||||
-- (and update the index).
|
||||
-- loc: offset of the position just after spaces and comments
|
||||
-- previous_i: offset in src before extraction began
|
||||
----------------------------------------------------------------------
|
||||
function lexer :extract ()
|
||||
local attached_comments = { }
|
||||
local function gen_token(...)
|
||||
local token = M.new_token(...)
|
||||
if #attached_comments>0 then -- attach previous comments to token
|
||||
local comments = M.new_comment(attached_comments)
|
||||
token.lineinfo.first.comments = comments
|
||||
if self.lineinfo_last_extracted then
|
||||
self.lineinfo_last_extracted.comments = comments
|
||||
end
|
||||
attached_comments = { }
|
||||
end
|
||||
token.lineinfo.first.facing = self.lineinfo_last_extracted
|
||||
self.lineinfo_last_extracted.facing = assert(token.lineinfo.first)
|
||||
self.lineinfo_last_extracted = assert(token.lineinfo.last)
|
||||
return token
|
||||
end
|
||||
while true do -- loop until a non-comment token is found
|
||||
|
||||
-- skip whitespaces
|
||||
self.i = self.src:match (self.patterns.spaces, self.i)
|
||||
if self.i>#self.src then
|
||||
local fli = self.posfact :get_position (#self.src+1)
|
||||
local lli = self.posfact :get_position (#self.src+1) -- ok?
|
||||
local tok = gen_token("Eof", "eof", M.new_lineinfo(fli, lli))
|
||||
tok.lineinfo.last.facing = lli
|
||||
return tok
|
||||
end
|
||||
local i_first = self.i -- loc = position after whitespaces
|
||||
|
||||
-- try every extractor until a token is found
|
||||
for _, extractor in ipairs(self.extractors) do
|
||||
local tag, content, xtra = self [extractor] (self)
|
||||
if tag then
|
||||
local fli = self.posfact :get_position (i_first)
|
||||
local lli = self.posfact :get_position (self.i-1)
|
||||
local lineinfo = M.new_lineinfo(fli, lli)
|
||||
if tag=='Comment' then
|
||||
local prev_comment = attached_comments[#attached_comments]
|
||||
if not xtra -- new comment is short
|
||||
and prev_comment and not prev_comment[2] -- prev comment is short
|
||||
and prev_comment.lineinfo.last.line+1==fli.line then -- adjascent lines
|
||||
-- concat with previous comment
|
||||
prev_comment[1] = prev_comment[1].."\n"..content -- TODO quadratic, BAD!
|
||||
prev_comment.lineinfo.last = lli
|
||||
else -- accumulate comment
|
||||
local comment = M.new_comment_line(content, lineinfo, xtra)
|
||||
table.insert(attached_comments, comment)
|
||||
end
|
||||
break -- back to skipping spaces
|
||||
else -- not a comment: real token, then
|
||||
return gen_token(tag, content, lineinfo)
|
||||
end -- if token is a comment
|
||||
end -- if token found
|
||||
end -- for each extractor
|
||||
end -- while token is a comment
|
||||
end -- :extract()
|
||||
|
||||
|
||||
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Extract a short comment.
|
||||
----------------------------------------------------------------------
|
||||
function lexer :extract_short_comment()
|
||||
-- TODO: handle final_short_comment
|
||||
local content, j = self.src :match (self.patterns.short_comment, self.i)
|
||||
if content then self.i=j; return 'Comment', content, nil end
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Extract a long comment.
|
||||
----------------------------------------------------------------------
|
||||
function lexer :extract_long_comment()
|
||||
local equals, content, j = self.src:match (self.patterns.long_comment, self.i)
|
||||
if j then self.i = j; return "Comment", content, #equals end
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Extract a '...' or "..." short string.
|
||||
----------------------------------------------------------------------
|
||||
function lexer :extract_short_string()
|
||||
local k = self.src :sub (self.i,self.i) -- first char
|
||||
if k~=[[']] and k~=[["]] then return end -- no match'
|
||||
local i = self.i + 1
|
||||
local j = i
|
||||
while true do
|
||||
local x,y; x, j, y = self.src :match ("([\\\r\n"..k.."])()(.?)", j) -- next interesting char
|
||||
if x == '\\' then
|
||||
if y == 'z' then -- Lua 5.2 \z
|
||||
j = self.src :match ("^%s*()", j+1)
|
||||
else
|
||||
j=j+1 -- escaped char
|
||||
end
|
||||
elseif x == k then break -- end of string
|
||||
else
|
||||
assert (not x or x=='\r' or x=='\n')
|
||||
return nil, 'Unterminated string'
|
||||
end
|
||||
end
|
||||
self.i = j
|
||||
|
||||
return 'String', unescape_string (self.src :sub (i,j-2))
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Extract Id or Keyword.
|
||||
----------------------------------------------------------------------
|
||||
function lexer :extract_word()
|
||||
local word, j = self.src:match (self.patterns.word, self.i)
|
||||
if word then
|
||||
self.i = j
|
||||
return (self.alpha [word] and 'Keyword' or 'Id'), word
|
||||
end
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Extract Number.
|
||||
----------------------------------------------------------------------
|
||||
function lexer :extract_number()
|
||||
local j = self.src:match(self.patterns.number_hex, self.i)
|
||||
if j then
|
||||
j = self.src:match (self.patterns.number_mantissa_hex[1], j) or
|
||||
self.src:match (self.patterns.number_mantissa_hex[2], j)
|
||||
if j then
|
||||
j = self.src:match (self.patterns.number_exponant_hex, j) or j
|
||||
end
|
||||
else
|
||||
j = self.src:match (self.patterns.number_mantissa[1], self.i) or
|
||||
self.src:match (self.patterns.number_mantissa[2], self.i)
|
||||
if j then
|
||||
j = self.src:match (self.patterns.number_exponant, j) or j
|
||||
end
|
||||
end
|
||||
if not j then return end
|
||||
-- Number found, interpret with tonumber() and return it
|
||||
local str = self.src:sub (self.i, j-1)
|
||||
-- :TODO: tonumber on Lua5.2 floating hex may or may not work on Lua5.1
|
||||
local n = tonumber (str)
|
||||
if not n then error(str.." is not a valid number according to tonumber()") end
|
||||
self.i = j
|
||||
return 'Number', n
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Extract long string.
|
||||
----------------------------------------------------------------------
|
||||
function lexer :extract_long_string()
|
||||
local _, content, j = self.src :match (self.patterns.long_string, self.i)
|
||||
if j then self.i = j; return 'String', content end
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Extract symbol.
|
||||
----------------------------------------------------------------------
|
||||
function lexer :extract_symbol()
|
||||
local k = self.src:sub (self.i,self.i)
|
||||
local symk = self.sym [k] -- symbols starting with `k`
|
||||
if not symk then
|
||||
self.i = self.i + 1
|
||||
return 'Keyword', k
|
||||
end
|
||||
for _, sym in pairs (symk) do
|
||||
if sym == self.src:sub (self.i, self.i + #sym - 1) then
|
||||
self.i = self.i + #sym
|
||||
return 'Keyword', sym
|
||||
end
|
||||
end
|
||||
self.i = self.i+1
|
||||
return 'Keyword', k
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Add a keyword to the list of keywords recognized by the lexer.
|
||||
----------------------------------------------------------------------
|
||||
function lexer :add (w, ...)
|
||||
assert(not ..., "lexer :add() takes only one arg, although possibly a table")
|
||||
if type (w) == "table" then
|
||||
for _, x in ipairs (w) do self :add (x) end
|
||||
else
|
||||
if w:match (self.patterns.word .. "$") then self.alpha [w] = true
|
||||
elseif w:match "^%p%p+$" then
|
||||
local k = w:sub(1,1)
|
||||
local list = self.sym [k]
|
||||
if not list then list = { }; self.sym [k] = list end
|
||||
table.insert (list, w)
|
||||
elseif w:match "^%p$" then return
|
||||
else error "Invalid keyword" end
|
||||
end
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Return the [n]th next token, without consuming it.
|
||||
-- [n] defaults to 1. If it goes pass the end of the stream, an EOF
|
||||
-- token is returned.
|
||||
----------------------------------------------------------------------
|
||||
function lexer :peek (n)
|
||||
if not n then n=1 end
|
||||
if n > #self.peeked then
|
||||
for i = #self.peeked+1, n do
|
||||
self.peeked [i] = self :extract()
|
||||
end
|
||||
end
|
||||
return self.peeked [n]
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Return the [n]th next token, removing it as well as the 0..n-1
|
||||
-- previous tokens. [n] defaults to 1. If it goes pass the end of the
|
||||
-- stream, an EOF token is returned.
|
||||
----------------------------------------------------------------------
|
||||
function lexer :next (n)
|
||||
n = n or 1
|
||||
self :peek (n)
|
||||
local a
|
||||
for i=1,n do
|
||||
a = table.remove (self.peeked, 1)
|
||||
-- TODO: is this used anywhere? I think not. a.lineinfo.last may be nil.
|
||||
--self.lastline = a.lineinfo.last.line
|
||||
end
|
||||
self.lineinfo_last_consumed = a.lineinfo.last
|
||||
return a
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Returns an object which saves the stream's current state.
|
||||
----------------------------------------------------------------------
|
||||
-- FIXME there are more fields than that to save
|
||||
function lexer :save () return { self.i; {unpack(self.peeked) } } end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Restore the stream's state, as saved by method [save].
|
||||
----------------------------------------------------------------------
|
||||
-- FIXME there are more fields than that to restore
|
||||
function lexer :restore (s) self.i=s[1]; self.peeked=s[2] end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Resynchronize: cancel any token in self.peeked, by emptying the
|
||||
-- list and resetting the indexes
|
||||
----------------------------------------------------------------------
|
||||
function lexer :sync()
|
||||
local p1 = self.peeked[1]
|
||||
if p1 then
|
||||
local li_first = p1.lineinfo.first
|
||||
if li_first.comments then li_first=li_first.comments.lineinfo.first end
|
||||
self.i = li_first.offset
|
||||
self.column_offset = self.i - li_first.column
|
||||
self.peeked = { }
|
||||
self.attached_comments = p1.lineinfo.first.comments or { }
|
||||
end
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Take the source and offset of an old lexer.
|
||||
----------------------------------------------------------------------
|
||||
function lexer :takeover(old)
|
||||
self :sync(); old :sync()
|
||||
for _, field in ipairs{ 'i', 'src', 'attached_comments', 'posfact' } do
|
||||
self[field] = old[field]
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Return the current position in the sources. This position is between
|
||||
-- two tokens, and can be within a space / comment area, and therefore
|
||||
-- have a non-null width. :lineinfo_left() returns the beginning of the
|
||||
-- separation area, :lineinfo_right() returns the end of that area.
|
||||
--
|
||||
-- ____ last consummed token ____ first unconsummed token
|
||||
-- / /
|
||||
-- XXXXX <spaces and comments> YYYYY
|
||||
-- \____ \____
|
||||
-- :lineinfo_left() :lineinfo_right()
|
||||
----------------------------------------------------------------------
|
||||
function lexer :lineinfo_right()
|
||||
return self :peek(1).lineinfo.first
|
||||
end
|
||||
|
||||
function lexer :lineinfo_left()
|
||||
return self.lineinfo_last_consumed
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Create a new lexstream.
|
||||
----------------------------------------------------------------------
|
||||
function lexer :newstream (src_or_stream, name)
|
||||
name = name or "?"
|
||||
if type(src_or_stream)=='table' then -- it's a stream
|
||||
return setmetatable ({ }, self) :takeover (src_or_stream)
|
||||
elseif type(src_or_stream)=='string' then -- it's a source string
|
||||
local src = src_or_stream
|
||||
local pos1 = M.new_position(1, 1, 1, name)
|
||||
local stream = {
|
||||
src_name = name; -- Name of the file
|
||||
src = src; -- The source, as a single string
|
||||
peeked = { }; -- Already peeked, but not discarded yet, tokens
|
||||
i = 1; -- Character offset in src
|
||||
attached_comments = { },-- comments accumulator
|
||||
lineinfo_last_extracted = pos1,
|
||||
lineinfo_last_consumed = pos1,
|
||||
posfact = M.new_position_factory (src_or_stream, name)
|
||||
}
|
||||
setmetatable (stream, self)
|
||||
|
||||
-- Skip initial sharp-bang for Unix scripts
|
||||
-- FIXME: redundant with mlp.chunk()
|
||||
if src and src :match "^#!" then
|
||||
local endofline = src :find "\n"
|
||||
stream.i = endofline and (endofline + 1) or #src
|
||||
end
|
||||
return stream
|
||||
else
|
||||
assert(false, ":newstream() takes a source string or a stream, not a "..
|
||||
type(src_or_stream))
|
||||
end
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- If there's no ... args, return the token a (whose truth value is
|
||||
-- true) if it's a `Keyword{ }, or nil. If there are ... args, they
|
||||
-- have to be strings. if the token a is a keyword, and it's content
|
||||
-- is one of the ... args, then returns it (it's truth value is
|
||||
-- true). If no a keyword or not in ..., return nil.
|
||||
----------------------------------------------------------------------
|
||||
function lexer :is_keyword (a, ...)
|
||||
if not a or a.tag ~= "Keyword" then return false end
|
||||
local words = {...}
|
||||
if #words == 0 then return a[1] end
|
||||
for _, w in ipairs (words) do
|
||||
if w == a[1] then return w end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Cause an error if the next token isn't a keyword whose content
|
||||
-- is listed among ... args (which have to be strings).
|
||||
----------------------------------------------------------------------
|
||||
function lexer :check (...)
|
||||
local words = {...}
|
||||
local a = self :next()
|
||||
local function err ()
|
||||
error ("Got " .. tostring (a) ..
|
||||
", expected one of these keywords : '" ..
|
||||
table.concat (words,"', '") .. "'") end
|
||||
if not a or a.tag ~= "Keyword" then err () end
|
||||
if #words == 0 then return a[1] end
|
||||
for _, w in ipairs (words) do
|
||||
if w == a[1] then return w end
|
||||
end
|
||||
err ()
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
--
|
||||
----------------------------------------------------------------------
|
||||
function lexer :clone()
|
||||
local alpha_clone, sym_clone = { }, { }
|
||||
for word in pairs(self.alpha) do alpha_clone[word]=true end
|
||||
for letter, list in pairs(self.sym) do sym_clone[letter] = { unpack(list) } end
|
||||
local clone = { alpha=alpha_clone, sym=sym_clone }
|
||||
setmetatable(clone, self)
|
||||
clone.__index = clone
|
||||
return clone
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Cancel everything left in a lexer, all subsequent attempts at
|
||||
-- `:peek()` or `:next()` will return `Eof`.
|
||||
----------------------------------------------------------------------
|
||||
function lexer :kill()
|
||||
self.i = #self.src+1
|
||||
self.peeked = { }
|
||||
self.attached_comments = { }
|
||||
self.lineinfo_last = self.posfact :get_position (#self.src+1)
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,133 @@
|
||||
--------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2006-2013 Fabien Fleutot and others.
|
||||
--
|
||||
-- All rights reserved.
|
||||
--
|
||||
-- This program and the accompanying materials are made available
|
||||
-- under the terms of the Eclipse Public License v1.0 which
|
||||
-- accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- This program and the accompanying materials are also made available
|
||||
-- under the terms of the MIT public license which accompanies this
|
||||
-- distribution, and is available at http://www.lua.org/license.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Fabien Fleutot - API and implementation
|
||||
--
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local M = require "package" -- extend Lua's basic "package" module
|
||||
local checks = require 'checks'
|
||||
|
||||
M.metalua_extension_prefix = 'metalua.extension.'
|
||||
|
||||
-- Initialize package.mpath from package.path
|
||||
M.mpath = M.mpath or os.getenv 'LUA_MPATH' or
|
||||
(M.path..";") :gsub("%.(lua[:;])", ".m%1") :sub(1, -2)
|
||||
|
||||
M.mcache = M.mcache or os.getenv 'LUA_MCACHE'
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- resc(k) returns "%"..k if it's a special regular expression char,
|
||||
-- or just k if it's normal.
|
||||
----------------------------------------------------------------------
|
||||
local regexp_magic = { }
|
||||
for k in ("^$()%.[]*+-?") :gmatch "." do regexp_magic[k]="%"..k end
|
||||
|
||||
local function resc(k) return regexp_magic[k] or k end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Take a Lua module name, return the open file and its name,
|
||||
-- or <false> and an error message.
|
||||
----------------------------------------------------------------------
|
||||
function M.findfile(name, path_string)
|
||||
local config_regexp = ("([^\n])\n"):rep(5):sub(1, -2)
|
||||
local dir_sep, path_sep, path_mark, execdir, igmark =
|
||||
M.config :match (config_regexp)
|
||||
name = name:gsub ('%.', dir_sep)
|
||||
local errors = { }
|
||||
local path_pattern = string.format('[^%s]+', resc(path_sep))
|
||||
for path in path_string:gmatch (path_pattern) do
|
||||
--printf('path = %s, rpath_mark=%s, name=%s', path, resc(path_mark), name)
|
||||
local filename = path:gsub (resc (path_mark), name)
|
||||
--printf('filename = %s', filename)
|
||||
local file = io.open (filename, 'rb')
|
||||
if file then return file, filename end
|
||||
table.insert(errors, string.format("\tno file %q", filename))
|
||||
end
|
||||
return false, '\n'..table.concat(errors, "\n")..'\n'
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Before compiling a metalua source module, try to find and load
|
||||
-- a more recent bytecode dump. Requires lfs
|
||||
----------------------------------------------------------------------
|
||||
local function metalua_cache_loader(name, src_filename, src)
|
||||
if not M.mcache:find('%?') then
|
||||
-- This is highly suspicious...
|
||||
print("WARNING: no '?' character in $LUA_MCACHE/package.mcache")
|
||||
end
|
||||
local mlc = require 'metalua.compiler'.new()
|
||||
local lfs = require 'lfs'
|
||||
local dir_sep = M.config:sub(1,1)
|
||||
local dst_filename = M.mcache :gsub ('%?', (name:gsub('%.', dir_sep)))
|
||||
local src_a = lfs.attributes(src_filename)
|
||||
local src_date = src_a and src_a.modification or 0
|
||||
local dst_a = lfs.attributes(dst_filename)
|
||||
local dst_date = dst_a and dst_a.modification or 0
|
||||
local delta = dst_date - src_date
|
||||
local bytecode, file, msg
|
||||
if delta <= 0 then
|
||||
--print ("(need to recompile "..src_filename.." into "..dst_filename..")")
|
||||
bytecode = mlc :src_to_bytecode (src, '@'..src_filename)
|
||||
for x in dst_filename :gmatch('()'..dir_sep) do
|
||||
lfs.mkdir(dst_filename:sub(1,x))
|
||||
end
|
||||
file, msg = io.open(dst_filename, 'wb')
|
||||
if not file then error(msg) end
|
||||
file :write (bytecode)
|
||||
file :close()
|
||||
else
|
||||
file, msg = io.open(dst_filename, 'rb')
|
||||
if not file then error(msg) end
|
||||
bytecode = file :read '*a'
|
||||
file :close()
|
||||
end
|
||||
return mlc :bytecode_to_function (bytecode, '@'..src_filename)
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Load a metalua source file.
|
||||
----------------------------------------------------------------------
|
||||
function M.metalua_loader (name)
|
||||
local file, filename_or_msg = M.findfile (name, M.mpath)
|
||||
if not file then return filename_or_msg end
|
||||
local luastring = file:read '*a'
|
||||
file:close()
|
||||
if M.mcache and pcall(require, 'lfs') then
|
||||
return metalua_cache_loader(name, filename_or_msg, luastring)
|
||||
else return require 'metalua.compiler'.new() :src_to_function (luastring, '@'..filename_or_msg) end
|
||||
end
|
||||
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Placed after lua/luac loader, so precompiled files have
|
||||
-- higher precedence.
|
||||
----------------------------------------------------------------------
|
||||
table.insert(M.loaders, M.metalua_loader)
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Load an extension.
|
||||
----------------------------------------------------------------------
|
||||
function extension (name, mlp)
|
||||
local complete_name = M.metalua_extension_prefix..name
|
||||
local extend_func = require (complete_name)
|
||||
if not mlp.extensions[complete_name] then
|
||||
local ast =extend_func(mlp)
|
||||
mlp.extensions[complete_name] =extend_func
|
||||
return ast
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,295 @@
|
||||
-------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2006-2013 Fabien Fleutot and others.
|
||||
--
|
||||
-- All rights reserved.
|
||||
--
|
||||
-- This program and the accompanying materials are made available
|
||||
-- under the terms of the Eclipse Public License v1.0 which
|
||||
-- accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- This program and the accompanying materials are also made available
|
||||
-- under the terms of the MIT public license which accompanies this
|
||||
-- distribution, and is available at http://www.lua.org/license.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Fabien Fleutot - API and implementation
|
||||
--
|
||||
----------------------------------------------------------------------
|
||||
|
||||
----------------------------------------------------------------------
|
||||
----------------------------------------------------------------------
|
||||
--
|
||||
-- Lua objects pretty-printer
|
||||
--
|
||||
----------------------------------------------------------------------
|
||||
----------------------------------------------------------------------
|
||||
|
||||
local M = { }
|
||||
|
||||
M.DEFAULT_CFG = {
|
||||
hide_hash = false; -- Print the non-array part of tables?
|
||||
metalua_tag = true; -- Use Metalua's backtick syntax sugar?
|
||||
fix_indent = nil; -- If a number, number of indentation spaces;
|
||||
-- If false, indent to the previous brace.
|
||||
line_max = nil; -- If a number, tries to avoid making lines with
|
||||
-- more than this number of chars.
|
||||
initial_indent = 0; -- If a number, starts at this level of indentation
|
||||
keywords = { }; -- Set of keywords which must not use Lua's field
|
||||
-- shortcuts {["foo"]=...} -> {foo=...}
|
||||
}
|
||||
|
||||
local function valid_id(cfg, x)
|
||||
if type(x) ~= "string" then return false end
|
||||
if not x:match "^[a-zA-Z_][a-zA-Z0-9_]*$" then return false end
|
||||
if cfg.keywords and cfg.keywords[x] then return false end
|
||||
return true
|
||||
end
|
||||
|
||||
local __tostring_cache = setmetatable({ }, {__mode='k'})
|
||||
|
||||
-- Retrieve the string produced by `__tostring` metamethod if present,
|
||||
-- return `false` otherwise. Cached in `__tostring_cache`.
|
||||
local function __tostring(x)
|
||||
local the_string = __tostring_cache[x]
|
||||
if the_string~=nil then return the_string end
|
||||
local mt = getmetatable(x)
|
||||
if mt then
|
||||
local __tostring = mt.__tostring
|
||||
if __tostring then
|
||||
the_string = __tostring(x)
|
||||
__tostring_cache[x] = the_string
|
||||
return the_string
|
||||
end
|
||||
end
|
||||
if x~=nil then __tostring_cache[x] = false end -- nil is an illegal key
|
||||
return false
|
||||
end
|
||||
|
||||
local xlen -- mutually recursive with `xlen_type`
|
||||
|
||||
local xlen_cache = setmetatable({ }, {__mode='k'})
|
||||
|
||||
-- Helpers for the `xlen` function
|
||||
local xlen_type = {
|
||||
["nil"] = function ( ) return 3 end;
|
||||
number = function (x) return #tostring(x) end;
|
||||
boolean = function (x) return x and 4 or 5 end;
|
||||
string = function (x) return #string.format("%q",x) end;
|
||||
}
|
||||
|
||||
function xlen_type.table (adt, cfg, nested)
|
||||
local custom_string = __tostring(adt)
|
||||
if custom_string then return #custom_string end
|
||||
|
||||
-- Circular referenced objects are printed with the plain
|
||||
-- `tostring` function in nested positions.
|
||||
if nested [adt] then return #tostring(adt) end
|
||||
nested [adt] = true
|
||||
|
||||
local has_tag = cfg.metalua_tag and valid_id(cfg, adt.tag)
|
||||
local alen = #adt
|
||||
local has_arr = alen>0
|
||||
local has_hash = false
|
||||
local x = 0
|
||||
|
||||
if not cfg.hide_hash then
|
||||
-- first pass: count hash-part
|
||||
for k, v in pairs(adt) do
|
||||
if k=="tag" and has_tag then
|
||||
-- this is the tag -> do nothing!
|
||||
elseif type(k)=="number" and k<=alen and math.fmod(k,1)==0 and k>0 then
|
||||
-- array-part pair -> do nothing!
|
||||
else
|
||||
has_hash = true
|
||||
if valid_id(cfg, k) then x=x+#k
|
||||
else x = x + xlen (k, cfg, nested) + 2 end -- count surrounding brackets
|
||||
x = x + xlen (v, cfg, nested) + 5 -- count " = " and ", "
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, alen do x = x + xlen (adt[i], nested) + 2 end -- count ", "
|
||||
|
||||
nested[adt] = false -- No more nested calls
|
||||
|
||||
if not (has_tag or has_arr or has_hash) then return 3 end
|
||||
if has_tag then x=x+#adt.tag+1 end
|
||||
if not (has_arr or has_hash) then return x end
|
||||
if not has_hash and alen==1 and type(adt[1])~="table" then
|
||||
return x-2 -- substract extraneous ", "
|
||||
end
|
||||
return x+2 -- count "{ " and " }", substract extraneous ", "
|
||||
end
|
||||
|
||||
|
||||
-- Compute the number of chars it would require to display the table
|
||||
-- on a single line. Helps to decide whether some carriage returns are
|
||||
-- required. Since the size of each sub-table is required many times,
|
||||
-- it's cached in [xlen_cache].
|
||||
xlen = function (x, cfg, nested)
|
||||
-- no need to compute length for 1-line prints
|
||||
if not cfg.line_max then return 0 end
|
||||
nested = nested or { }
|
||||
if x==nil then return #"nil" end
|
||||
local len = xlen_cache[x]
|
||||
if len then return len end
|
||||
local f = xlen_type[type(x)]
|
||||
if not f then return #tostring(x) end
|
||||
len = f (x, cfg, nested)
|
||||
xlen_cache[x] = len
|
||||
return len
|
||||
end
|
||||
|
||||
local function consider_newline(p, len)
|
||||
if not p.cfg.line_max then return end
|
||||
if p.current_offset + len <= p.cfg.line_max then return end
|
||||
if p.indent < p.current_offset then
|
||||
p:acc "\n"; p:acc ((" "):rep(p.indent))
|
||||
p.current_offset = p.indent
|
||||
end
|
||||
end
|
||||
|
||||
local acc_value
|
||||
|
||||
local acc_type = {
|
||||
["nil"] = function(p) p:acc("nil") end;
|
||||
number = function(p, adt) p:acc (tostring (adt)) end;
|
||||
string = function(p, adt) p:acc ((string.format ("%q", adt):gsub("\\\n", "\\n"))) end;
|
||||
boolean = function(p, adt) p:acc (adt and "true" or "false") end }
|
||||
|
||||
-- Indentation:
|
||||
-- * if `cfg.fix_indent` is set to a number:
|
||||
-- * add this number of space for each level of depth
|
||||
-- * return to the line as soon as it flushes things further left
|
||||
-- * if not, tabulate to one space after the opening brace.
|
||||
-- * as a result, it never saves right-space to return before first element
|
||||
|
||||
function acc_type.table(p, adt)
|
||||
if p.nested[adt] then p:acc(tostring(adt)); return end
|
||||
p.nested[adt] = true
|
||||
|
||||
local has_tag = p.cfg.metalua_tag and valid_id(p.cfg, adt.tag)
|
||||
local alen = #adt
|
||||
local has_arr = alen>0
|
||||
local has_hash = false
|
||||
|
||||
local previous_indent = p.indent
|
||||
|
||||
if has_tag then p:acc("`"); p:acc(adt.tag) end
|
||||
|
||||
local function indent(p)
|
||||
if not p.cfg.fix_indent then p.indent = p.current_offset
|
||||
else p.indent = p.indent + p.cfg.fix_indent end
|
||||
end
|
||||
|
||||
-- First pass: handle hash-part
|
||||
if not p.cfg.hide_hash then
|
||||
for k, v in pairs(adt) do
|
||||
|
||||
if has_tag and k=='tag' then -- pass the 'tag' field
|
||||
elseif type(k)=="number" and k<=alen and k>0 and math.fmod(k,1)==0 then
|
||||
-- pass array-part keys (consecutive ints less than `#adt`)
|
||||
else -- hash-part keys
|
||||
if has_hash then p:acc ", " else -- 1st hash-part pair ever found
|
||||
p:acc "{ "; indent(p)
|
||||
end
|
||||
|
||||
-- Determine whether a newline is required
|
||||
local is_id, expected_len=valid_id(p.cfg, k)
|
||||
if is_id then expected_len=#k+xlen(v, p.cfg, p.nested)+#" = , "
|
||||
else expected_len = xlen(k, p.cfg, p.nested)+xlen(v, p.cfg, p.nested)+#"[] = , " end
|
||||
consider_newline(p, expected_len)
|
||||
|
||||
-- Print the key
|
||||
if is_id then p:acc(k); p:acc " = " else
|
||||
p:acc "["; acc_value (p, k); p:acc "] = "
|
||||
end
|
||||
|
||||
acc_value (p, v) -- Print the value
|
||||
has_hash = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Now we know whether there's a hash-part, an array-part, and a tag.
|
||||
-- Tag and hash-part are already printed if they're present.
|
||||
if not has_tag and not has_hash and not has_arr then p:acc "{ }";
|
||||
elseif has_tag and not has_hash and not has_arr then -- nothing, tag already in acc
|
||||
else
|
||||
assert (has_hash or has_arr) -- special case { } already handled
|
||||
local no_brace = false
|
||||
if has_hash and has_arr then p:acc ", "
|
||||
elseif has_tag and not has_hash and alen==1 and type(adt[1])~="table" then
|
||||
-- No brace required; don't print "{", remember not to print "}"
|
||||
p:acc (" "); acc_value (p, adt[1]) -- indent= indent+(cfg.fix_indent or 0))
|
||||
no_brace = true
|
||||
elseif not has_hash then
|
||||
-- Braces required, but not opened by hash-part handler yet
|
||||
p:acc "{ "; indent(p)
|
||||
end
|
||||
|
||||
-- 2nd pass: array-part
|
||||
if not no_brace and has_arr then
|
||||
local expected_len = xlen(adt[1], p.cfg, p.nested)
|
||||
consider_newline(p, expected_len)
|
||||
acc_value(p, adt[1]) -- indent+(cfg.fix_indent or 0)
|
||||
for i=2, alen do
|
||||
p:acc ", ";
|
||||
consider_newline(p, xlen(adt[i], p.cfg, p.nested))
|
||||
acc_value (p, adt[i]) --indent+(cfg.fix_indent or 0)
|
||||
end
|
||||
end
|
||||
if not no_brace then p:acc " }" end
|
||||
end
|
||||
p.nested[adt] = false -- No more nested calls
|
||||
p.indent = previous_indent
|
||||
end
|
||||
|
||||
|
||||
function acc_value(p, v)
|
||||
local custom_string = __tostring(v)
|
||||
if custom_string then p:acc(custom_string) else
|
||||
local f = acc_type[type(v)]
|
||||
if f then f(p, v) else p:acc(tostring(v)) end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- FIXME: new_indent seems to be always nil?!s detection
|
||||
-- FIXME: accumulator function should be configurable,
|
||||
-- so that print() doesn't need to bufferize the whole string
|
||||
-- before starting to print.
|
||||
function M.tostring(t, cfg)
|
||||
|
||||
cfg = cfg or M.DEFAULT_CFG or { }
|
||||
|
||||
local p = {
|
||||
cfg = cfg;
|
||||
indent = 0;
|
||||
current_offset = cfg.initial_indent or 0;
|
||||
buffer = { };
|
||||
nested = { };
|
||||
acc = function(self, str)
|
||||
table.insert(self.buffer, str)
|
||||
self.current_offset = self.current_offset + #str
|
||||
end;
|
||||
}
|
||||
acc_value(p, t)
|
||||
return table.concat(p.buffer)
|
||||
end
|
||||
|
||||
function M.print(...) return print(M.tostring(...)) end
|
||||
function M.sprintf(fmt, ...)
|
||||
local args={...}
|
||||
for i, v in pairs(args) do
|
||||
local t=type(v)
|
||||
if t=='table' then args[i]=M.tostring(v)
|
||||
elseif t=='nil' then args[i]='nil' end
|
||||
end
|
||||
return string.format(fmt, unpack(args))
|
||||
end
|
||||
|
||||
function M.printf(...) print(M.sprintf(...)) end
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,108 @@
|
||||
-------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2006-2013 Fabien Fleutot and others.
|
||||
--
|
||||
-- All rights reserved.
|
||||
--
|
||||
-- This program and the accompanying materials are made available
|
||||
-- under the terms of the Eclipse Public License v1.0 which
|
||||
-- accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- This program and the accompanying materials are also made available
|
||||
-- under the terms of the MIT public license which accompanies this
|
||||
-- distribution, and is available at http://www.lua.org/license.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Fabien Fleutot - API and implementation
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
-- Keep these global:
|
||||
PRINT_AST = true
|
||||
LINE_WIDTH = 60
|
||||
PROMPT = "M> "
|
||||
PROMPT2 = ">> "
|
||||
|
||||
local pp=require 'metalua.pprint'
|
||||
local M = { }
|
||||
|
||||
mlc = require 'metalua.compiler'.new()
|
||||
|
||||
local readline
|
||||
|
||||
do -- set readline() to a line reader, either editline otr a default
|
||||
local status, editline = pcall(require, 'editline')
|
||||
if status then
|
||||
local rl_handle = editline.init 'metalua'
|
||||
readline = |p| rl_handle:read(p)
|
||||
else
|
||||
local status, rl = pcall(require, 'readline')
|
||||
if status then
|
||||
rl.set_options{histfile='~/.metalua_history', keeplines=100, completion=false }
|
||||
readline = rl.readline
|
||||
else -- neither editline nor readline available
|
||||
function readline (p)
|
||||
io.write (p)
|
||||
io.flush ()
|
||||
return io.read '*l'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function reached_eof(lx, msg)
|
||||
return lx:peek().tag=='Eof' or msg:find "token `Eof"
|
||||
end
|
||||
|
||||
|
||||
function M.run()
|
||||
pp.printf ("Metalua, interactive REPLoop.\n"..
|
||||
"(c) 2006-2013 <metalua@gmail.com>")
|
||||
local lines = { }
|
||||
while true do
|
||||
local src, lx, ast, f, results, success
|
||||
repeat
|
||||
local line = readline(next(lines) and PROMPT2 or PROMPT)
|
||||
if not line then print(); os.exit(0) end -- line==nil iff eof on stdin
|
||||
if not next(lines) then
|
||||
line = line:gsub('^%s*=', 'return ')
|
||||
end
|
||||
table.insert(lines, line)
|
||||
src = table.concat (lines, "\n")
|
||||
until #line>0
|
||||
lx = mlc :src_to_lexstream(src)
|
||||
success, ast = pcall(mlc.lexstream_to_ast, mlc, lx)
|
||||
if success then
|
||||
success, f = pcall(mlc.ast_to_function, mlc, ast, '=stdin')
|
||||
if success then
|
||||
results = { xpcall(f, debug.traceback) }
|
||||
success = table.remove (results, 1)
|
||||
if success then
|
||||
-- Success!
|
||||
for _, x in ipairs(results) do
|
||||
pp.print(x, {line_max=LINE_WIDTH, metalua_tag=true})
|
||||
end
|
||||
lines = { }
|
||||
else
|
||||
print "Evaluation error:"
|
||||
print (results[1])
|
||||
lines = { }
|
||||
end
|
||||
else
|
||||
print "Can't compile into bytecode:"
|
||||
print (f)
|
||||
lines = { }
|
||||
end
|
||||
else
|
||||
-- If lx has been read entirely, try to read
|
||||
-- another line before failing.
|
||||
if not reached_eof(lx, ast) then
|
||||
print "Can't compile source into AST:"
|
||||
print (ast)
|
||||
lines = { }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,488 @@
|
||||
-------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2006-2013 Fabien Fleutot and others.
|
||||
--
|
||||
-- All rights reserved.
|
||||
--
|
||||
-- This program and the accompanying materials are made available
|
||||
-- under the terms of the Eclipse Public License v1.0 which
|
||||
-- accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- This program and the accompanying materials are also made available
|
||||
-- under the terms of the MIT public license which accompanies this
|
||||
-- distribution, and is available at http://www.lua.org/license.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Fabien Fleutot - API and implementation
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
local walk = require 'metalua.treequery.walk'
|
||||
|
||||
local M = { }
|
||||
-- support for old-style modules
|
||||
treequery = M
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- -----------------------------------------------------------------------------
|
||||
--
|
||||
-- multimap helper mmap: associate a key to a set of values
|
||||
--
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
local function mmap_add (mmap, node, x)
|
||||
if node==nil then return false end
|
||||
local set = mmap[node]
|
||||
if set then set[x] = true
|
||||
else mmap[node] = {[x]=true} end
|
||||
end
|
||||
|
||||
-- currently unused, I throw the whole set away
|
||||
local function mmap_remove (mmap, node, x)
|
||||
local set = mmap[node]
|
||||
if not set then return false
|
||||
elseif not set[x] then return false
|
||||
elseif next(set) then set[x]=nil
|
||||
else mmap[node] = nil end
|
||||
return true
|
||||
end
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- -----------------------------------------------------------------------------
|
||||
--
|
||||
-- TreeQuery object.
|
||||
--
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
local ACTIVE_SCOPE = setmetatable({ }, {__mode="k"})
|
||||
|
||||
-- treequery metatable
|
||||
local Q = { }; Q.__index = Q
|
||||
|
||||
--- treequery constructor
|
||||
-- the resultingg object will allow to filter ans operate on the AST
|
||||
-- @param root the AST to visit
|
||||
-- @return a treequery visitor instance
|
||||
function M.treequery(root)
|
||||
return setmetatable({
|
||||
root = root,
|
||||
unsatisfied = 0,
|
||||
predicates = { },
|
||||
until_up = { },
|
||||
from_up = { },
|
||||
up_f = false,
|
||||
down_f = false,
|
||||
filters = { },
|
||||
}, Q)
|
||||
end
|
||||
|
||||
-- helper to share the implementations of positional filters
|
||||
local function add_pos_filter(self, position, inverted, inclusive, f, ...)
|
||||
if type(f)=='string' then f = M.has_tag(f, ...) end
|
||||
if not inverted then self.unsatisfied += 1 end
|
||||
local x = {
|
||||
pred = f,
|
||||
position = position,
|
||||
satisfied = false,
|
||||
inverted = inverted or false,
|
||||
inclusive = inclusive or false }
|
||||
table.insert(self.predicates, x)
|
||||
return self
|
||||
end
|
||||
|
||||
function Q :if_unknown(f)
|
||||
self.unknown_handler = f or (||nil)
|
||||
return self
|
||||
end
|
||||
|
||||
-- TODO: offer an API for inclusive pos_filters
|
||||
|
||||
--- select nodes which are after one which satisfies predicate f
|
||||
Q.after = |self, f, ...| add_pos_filter(self, 'after', false, false, f, ...)
|
||||
--- select nodes which are not after one which satisfies predicate f
|
||||
Q.not_after = |self, f, ...| add_pos_filter(self, 'after', true, false, f, ...)
|
||||
--- select nodes which are under one which satisfies predicate f
|
||||
Q.under = |self, f, ...| add_pos_filter(self, 'under', false, false, f, ...)
|
||||
--- select nodes which are not under one which satisfies predicate f
|
||||
Q.not_under = |self, f, ...| add_pos_filter(self, 'under', true, false, f, ...)
|
||||
|
||||
--- select nodes which satisfy predicate f
|
||||
function Q :filter(f, ...)
|
||||
if type(f)=='string' then f = M.has_tag(f, ...) end
|
||||
table.insert(self.filters, f);
|
||||
return self
|
||||
end
|
||||
|
||||
--- select nodes which satisfy predicate f
|
||||
function Q :filter_not(f, ...)
|
||||
if type(f)=='string' then f = M.has_tag(f, ...) end
|
||||
table.insert(self.filters, |...| not f(...))
|
||||
return self
|
||||
end
|
||||
|
||||
-- private helper: apply filters and execute up/down callbacks when applicable
|
||||
function Q :execute()
|
||||
local cfg = { }
|
||||
-- TODO: optimize away not_under & not_after by pruning the tree
|
||||
function cfg.down(...)
|
||||
--printf ("[down]\t%s\t%s", self.unsatisfied, table.tostring((...)))
|
||||
ACTIVE_SCOPE[...] = cfg.scope
|
||||
local satisfied = self.unsatisfied==0
|
||||
for _, x in ipairs(self.predicates) do
|
||||
if not x.satisfied and x.pred(...) then
|
||||
x.satisfied = true
|
||||
local node, parent = ...
|
||||
local inc = x.inverted and 1 or -1
|
||||
if x.position=='under' then
|
||||
-- satisfied from after we get down this node...
|
||||
self.unsatisfied += inc
|
||||
-- ...until before we get up this node
|
||||
mmap_add(self.until_up, node, x)
|
||||
elseif x.position=='after' then
|
||||
-- satisfied from after we get up this node...
|
||||
mmap_add(self.from_up, node, x)
|
||||
-- ...until before we get up this node's parent
|
||||
mmap_add(self.until_up, parent, x)
|
||||
elseif x.position=='under_or_after' then
|
||||
-- satisfied from after we get down this node...
|
||||
self.satisfied += inc
|
||||
-- ...until before we get up this node's parent...
|
||||
mmap_add(self.until_up, parent, x)
|
||||
else
|
||||
error "position not understood"
|
||||
end -- position
|
||||
if x.inclusive then satisfied = self.unsatisfied==0 end
|
||||
end -- predicate passed
|
||||
end -- for predicates
|
||||
|
||||
if satisfied then
|
||||
for _, f in ipairs(self.filters) do
|
||||
if not f(...) then satisfied=false; break end
|
||||
end
|
||||
if satisfied and self.down_f then self.down_f(...) end
|
||||
end
|
||||
end
|
||||
|
||||
function cfg.up(...)
|
||||
--printf ("[up]\t%s", table.tostring((...)))
|
||||
|
||||
-- Remove predicates which are due before we go up this node
|
||||
local preds = self.until_up[...]
|
||||
if preds then
|
||||
for x, _ in pairs(preds) do
|
||||
local inc = x.inverted and -1 or 1
|
||||
self.unsatisfied += inc
|
||||
x.satisfied = false
|
||||
end
|
||||
self.until_up[...] = nil
|
||||
end
|
||||
|
||||
-- Execute the up callback
|
||||
-- TODO: cache the filter passing result from the down callback
|
||||
-- TODO: skip if there's no callback
|
||||
local satisfied = self.unsatisfied==0
|
||||
if satisfied then
|
||||
for _, f in ipairs(self.filters) do
|
||||
if not f(self, ...) then satisfied=false; break end
|
||||
end
|
||||
if satisfied and self.up_f then self.up_f(...) end
|
||||
end
|
||||
|
||||
-- Set predicate which are due after we go up this node
|
||||
local preds = self.from_up[...]
|
||||
if preds then
|
||||
for p, _ in pairs(preds) do
|
||||
local inc = p.inverted and 1 or -1
|
||||
self.unsatisfied += inc
|
||||
end
|
||||
self.from_up[...] = nil
|
||||
end
|
||||
ACTIVE_SCOPE[...] = nil
|
||||
end
|
||||
|
||||
function cfg.binder(id_node, ...)
|
||||
--printf(" >>> Binder called on %s, %s", table.tostring(id_node),
|
||||
-- table.tostring{...}:sub(2,-2))
|
||||
cfg.down(id_node, ...)
|
||||
cfg.up(id_node, ...)
|
||||
--printf("down/up on binder done")
|
||||
end
|
||||
|
||||
cfg.unknown = self.unknown_handler
|
||||
|
||||
--function cfg.occurrence (binder, occ)
|
||||
-- if binder then OCC2BIND[occ] = binder[1] end
|
||||
--printf(" >>> %s is an occurrence of %s", occ[1], table.tostring(binder and binder[2]))
|
||||
--end
|
||||
|
||||
--function cfg.binder(...) cfg.down(...); cfg.up(...) end
|
||||
return walk.guess(cfg, self.root)
|
||||
end
|
||||
|
||||
--- Execute a function on each selected node
|
||||
-- @down: function executed when we go down a node, i.e. before its children
|
||||
-- have been examined.
|
||||
-- @up: function executed when we go up a node, i.e. after its children
|
||||
-- have been examined.
|
||||
function Q :foreach(down, up)
|
||||
if not up and not down then
|
||||
error "iterator missing"
|
||||
end
|
||||
self.up_f = up
|
||||
self.down_f = down
|
||||
return self :execute()
|
||||
end
|
||||
|
||||
--- Return the list of nodes selected by a given treequery.
|
||||
function Q :list()
|
||||
local acc = { }
|
||||
self :foreach(|x| table.insert(acc, x))
|
||||
return acc
|
||||
end
|
||||
|
||||
--- Return the first matching element
|
||||
-- TODO: dirty hack, to implement properly with a 'break' return.
|
||||
-- Also, it won't behave correctly if a predicate causes an error,
|
||||
-- or if coroutines are involved.
|
||||
function Q :first()
|
||||
local result = { }
|
||||
local function f(...) result = {...}; error() end
|
||||
pcall(|| self :foreach(f))
|
||||
return unpack(result)
|
||||
end
|
||||
|
||||
--- Pretty printer for queries
|
||||
function Q :__tostring() return "<treequery>" end
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- -----------------------------------------------------------------------------
|
||||
--
|
||||
-- Predicates.
|
||||
--
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
--- Return a predicate which is true if the tested node's tag is among the
|
||||
-- one listed as arguments
|
||||
-- @param ... a sequence of tag names
|
||||
function M.has_tag(...)
|
||||
local args = {...}
|
||||
if #args==1 then
|
||||
local tag = ...
|
||||
return (|node| node.tag==tag)
|
||||
--return function(self, node) printf("node %s has_tag %s?", table.tostring(node), tag); return node.tag==tag end
|
||||
else
|
||||
local tags = { }
|
||||
for _, tag in ipairs(args) do tags[tag]=true end
|
||||
return function(node)
|
||||
local node_tag = node.tag
|
||||
return node_tag and tags[node_tag]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Predicate to test whether a node represents an expression.
|
||||
M.is_expr = M.has_tag('Nil', 'Dots', 'True', 'False', 'Number','String',
|
||||
'Function', 'Table', 'Op', 'Paren', 'Call', 'Invoke',
|
||||
'Id', 'Index')
|
||||
|
||||
-- helper for is_stat
|
||||
local STAT_TAGS = { Do=1, Set=1, While=1, Repeat=1, If=1, Fornum=1,
|
||||
Forin=1, Local=1, Localrec=1, Return=1, Break=1 }
|
||||
|
||||
--- Predicate to test whether a node represents a statement.
|
||||
-- It is context-aware, i.e. it recognizes `Call and `Invoke nodes
|
||||
-- used in a statement context as such.
|
||||
function M.is_stat(node, parent)
|
||||
local tag = node.tag
|
||||
if not tag then return false
|
||||
elseif STAT_TAGS[tag] then return true
|
||||
elseif tag=='Call' or tag=='Invoke' then return parent and parent.tag==nil
|
||||
else return false end
|
||||
end
|
||||
|
||||
--- Predicate to test whether a node represents a statements block.
|
||||
function M.is_block(node) return node.tag==nil end
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- -----------------------------------------------------------------------------
|
||||
--
|
||||
-- Variables and scopes.
|
||||
--
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
local BINDER_PARENT_TAG = {
|
||||
Local=true, Localrec=true, Forin=true, Function=true }
|
||||
|
||||
--- Test whether a node is a binder. This is local predicate, although it
|
||||
-- might need to inspect the parent node.
|
||||
function M.is_binder(node, parent)
|
||||
--printf('is_binder(%s, %s)', table.tostring(node), table.tostring(parent))
|
||||
if node.tag ~= 'Id' or not parent then return false end
|
||||
if parent.tag=='Fornum' then return parent[1]==node end
|
||||
if not BINDER_PARENT_TAG[parent.tag] then return false end
|
||||
for _, binder in ipairs(parent[1]) do
|
||||
if binder==node then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--- Retrieve the binder associated to an occurrence within root.
|
||||
-- @param occurrence an Id node representing an occurrence in `root`.
|
||||
-- @param root the tree in which `node` and its binder occur.
|
||||
-- @return the binder node, and its ancestors up to root if found.
|
||||
-- @return nil if node is global (or not an occurrence) in `root`.
|
||||
function M.binder(occurrence, root)
|
||||
local cfg, id_name, result = { }, occurrence[1], { }
|
||||
function cfg.occurrence(id)
|
||||
if id == occurrence then result = cfg.scope :get(id_name) end
|
||||
-- TODO: break the walker
|
||||
end
|
||||
walk.guess(cfg, root)
|
||||
return unpack(result)
|
||||
end
|
||||
|
||||
--- Predicate to filter occurrences of a given binder.
|
||||
-- Warning: it relies on internal scope book-keeping,
|
||||
-- and for this reason, it only works as query method argument.
|
||||
-- It won't work outside of a query.
|
||||
-- @param binder the binder whose occurrences must be kept by predicate
|
||||
-- @return a predicate
|
||||
|
||||
-- function M.is_occurrence_of(binder)
|
||||
-- return function(node, ...)
|
||||
-- if node.tag ~= 'Id' then return nil end
|
||||
-- if M.is_binder(node, ...) then return nil end
|
||||
-- local scope = ACTIVE_SCOPE[node]
|
||||
-- if not scope then return nil end
|
||||
-- local result = scope :get (node[1]) or { }
|
||||
-- if result[1] ~= binder then return nil end
|
||||
-- return unpack(result)
|
||||
-- end
|
||||
-- end
|
||||
|
||||
function M.is_occurrence_of(binder)
|
||||
return function(node, ...)
|
||||
local b = M.get_binder(node)
|
||||
return b and b==binder
|
||||
end
|
||||
end
|
||||
|
||||
function M.get_binder(occurrence, ...)
|
||||
if occurrence.tag ~= 'Id' then return nil end
|
||||
if M.is_binder(occurrence, ...) then return nil end
|
||||
local scope = ACTIVE_SCOPE[occurrence]
|
||||
local binder_hierarchy = scope :get(occurrence[1])
|
||||
return unpack (binder_hierarchy or { })
|
||||
end
|
||||
|
||||
--- Transform a predicate on a node into a predicate on this node's
|
||||
-- parent. For instance if p tests whether a node has property P,
|
||||
-- then parent(p) tests whether this node's parent has property P.
|
||||
-- The ancestor level is precised with n, with 1 being the node itself,
|
||||
-- 2 its parent, 3 its grand-parent etc.
|
||||
-- @param[optional] n the parent to examine, default=2
|
||||
-- @param pred the predicate to transform
|
||||
-- @return a predicate
|
||||
function M.parent(n, pred, ...)
|
||||
if type(n)~='number' then n, pred = 2, n end
|
||||
if type(pred)=='string' then pred = M.has_tag(pred, ...) end
|
||||
return function(self, ...)
|
||||
return select(n, ...) and pred(self, select(n, ...))
|
||||
end
|
||||
end
|
||||
|
||||
--- Transform a predicate on a node into a predicate on this node's
|
||||
-- n-th child.
|
||||
-- @param n the child's index number
|
||||
-- @param pred the predicate to transform
|
||||
-- @return a predicate
|
||||
function M.child(n, pred)
|
||||
return function(node, ...)
|
||||
local child = node[n]
|
||||
return child and pred(child, node, ...)
|
||||
end
|
||||
end
|
||||
|
||||
--- Predicate to test the position of a node in its parent.
|
||||
-- The predicate succeeds if the node is the n-th child of its parent,
|
||||
-- and a <= n <= b.
|
||||
-- nth(a) is equivalent to nth(a, a).
|
||||
-- Negative indices are admitted, and count from the last child,
|
||||
-- as done for instance by string.sub().
|
||||
--
|
||||
-- TODO: This is wrong, this tests the table relationship rather than the
|
||||
-- AST node relationship.
|
||||
-- Must build a getindex helper, based on pattern matching, then build
|
||||
-- the predicate around it.
|
||||
--
|
||||
-- @param a lower bound
|
||||
-- @param a upper bound
|
||||
-- @return a predicate
|
||||
function M.is_nth(a, b)
|
||||
b = b or a
|
||||
return function(self, node, parent)
|
||||
if not parent then return false end
|
||||
local nchildren = #parent
|
||||
local a = a<=0 and nchildren+a+1 or a
|
||||
if a>nchildren then return false end
|
||||
local b = b<=0 and nchildren+b+1 or b>nchildren and nchildren or b
|
||||
for i=a,b do if parent[i]==node then return true end end
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
--- Returns a list of the direct children of AST node `ast`.
|
||||
-- Children are only expressions, statements and blocks,
|
||||
-- not intermediates such as `Pair` nodes or internal lists
|
||||
-- in `Local` or `Set` nodes.
|
||||
-- Children are returned in parsing order, which isn't necessarily
|
||||
-- the same as source code order. For instance, the right-hand-side
|
||||
-- of a `Local` node is listed before the left-hand-side, because
|
||||
-- semantically the right is evaluated before the variables on the
|
||||
-- left enter scope.
|
||||
--
|
||||
-- @param ast the node whose children are needed
|
||||
-- @return a list of the direct children of `ast`
|
||||
function M.children(ast)
|
||||
local acc = { }
|
||||
local cfg = { }
|
||||
function cfg.down(x)
|
||||
if x~=ast then table.insert(acc, x); return 'break' end
|
||||
end
|
||||
walk.guess(cfg, ast)
|
||||
return acc
|
||||
end
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- -----------------------------------------------------------------------------
|
||||
--
|
||||
-- Comments parsing.
|
||||
--
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
local comment_extractor = |which_side| function (node)
|
||||
local x = node.lineinfo
|
||||
x = x and x[which_side]
|
||||
x = x and x.comments
|
||||
if not x then return nil end
|
||||
local lines = { }
|
||||
for _, record in ipairs(x) do
|
||||
table.insert(lines, record[1])
|
||||
end
|
||||
return table.concat(lines, '\n')
|
||||
end
|
||||
|
||||
M.comment_prefix = comment_extractor 'first'
|
||||
M.comment_suffix = comment_extractor 'last'
|
||||
|
||||
|
||||
--- Shortcut for the query constructor
|
||||
function M :__call(...) return self.treequery(...) end
|
||||
setmetatable(M, M)
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,266 @@
|
||||
-------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2006-2013 Fabien Fleutot and others.
|
||||
--
|
||||
-- All rights reserved.
|
||||
--
|
||||
-- This program and the accompanying materials are made available
|
||||
-- under the terms of the Eclipse Public License v1.0 which
|
||||
-- accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- This program and the accompanying materials are also made available
|
||||
-- under the terms of the MIT public license which accompanies this
|
||||
-- distribution, and is available at http://www.lua.org/license.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Fabien Fleutot - API and implementation
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
-- Low level AST traversal library.
|
||||
--
|
||||
-- This library is a helper for the higher-level `treequery` library.
|
||||
-- It walks through every node of an AST, depth-first, and executes
|
||||
-- some callbacks contained in its `cfg` config table:
|
||||
--
|
||||
-- * `cfg.down(...)` is called when it walks down a node, and receive as
|
||||
-- parameters the node just entered, followed by its parent, grand-parent
|
||||
-- etc. until the root node.
|
||||
--
|
||||
-- * `cfg.up(...)` is called when it walks back up a node, and receive as
|
||||
-- parameters the node just entered, followed by its parent, grand-parent
|
||||
-- etc. until the root node.
|
||||
--
|
||||
-- * `cfg.occurrence(binder, id_node, ...)` is called when it visits
|
||||
-- an `` `Id{ }`` node which isn't a local variable creator. binder
|
||||
-- is a reference to its binder with its context. The binder is the
|
||||
-- `` `Id{ }`` node which created this local variable. By "binder
|
||||
-- and its context", we mean a list starting with the `` `Id{ }``,
|
||||
-- and followed by every ancestor of the binder node, up until the
|
||||
-- common root node. `binder` is nil if the variable is global.
|
||||
-- `id_node` is followed by its ancestor, up until the root node.
|
||||
--
|
||||
-- `cfg.scope` is maintained during the traversal, associating a
|
||||
-- variable name to the binder which creates it in the context of the
|
||||
-- node currently visited.
|
||||
--
|
||||
-- `walk.traverse.xxx` functions are in charge of the recursive
|
||||
-- descent into children nodes. They're private helpers. They are also
|
||||
-- in charge of calling appropriate `cfg.xxx` callbacks.
|
||||
|
||||
-{ extension ("match", ...) }
|
||||
|
||||
local pp = require 'metalua.pprint'
|
||||
|
||||
local M = { traverse = { }; tags = { }; debug = false }
|
||||
|
||||
local function table_transpose(t)
|
||||
local tt = { }; for a, b in pairs(t) do tt[b]=a end; return tt
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Standard tags: can be used to guess the type of an AST, or to check
|
||||
-- that the type of an AST is respected.
|
||||
--------------------------------------------------------------------------------
|
||||
M.tags.stat = table_transpose{
|
||||
'Do', 'Set', 'While', 'Repeat', 'Local', 'Localrec', 'Return',
|
||||
'Fornum', 'Forin', 'If', 'Break', 'Goto', 'Label',
|
||||
'Call', 'Invoke' }
|
||||
M.tags.expr = table_transpose{
|
||||
'Paren', 'Call', 'Invoke', 'Index', 'Op', 'Function', 'Stat',
|
||||
'Table', 'Nil', 'Dots', 'True', 'False', 'Number', 'String', 'Id' }
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- These [M.traverse.xxx()] functions are in charge of actually going through
|
||||
-- ASTs. At each node, they make sure to call the appropriate walker.
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
function M.traverse.stat (cfg, x, ...)
|
||||
if M.debug then pp.printf("traverse stat %s", x) end
|
||||
local ancestors = {...}
|
||||
local B = |y| M.block (cfg, y, x, unpack(ancestors)) -- Block
|
||||
local S = |y| M.stat (cfg, y, x, unpack(ancestors)) -- Statement
|
||||
local E = |y| M.expr (cfg, y, x, unpack(ancestors)) -- Expression
|
||||
local EL = |y| M.expr_list (cfg, y, x, unpack(ancestors)) -- Expression List
|
||||
local IL = |y| M.binder_list (cfg, y, x, unpack(ancestors)) -- Id binders List
|
||||
local OS = || cfg.scope :save() -- Open scope
|
||||
local CS = || cfg.scope :restore() -- Close scope
|
||||
|
||||
match x with
|
||||
| {...} if x.tag == nil -> for _, y in ipairs(x) do M.stat(cfg, y, ...) end
|
||||
-- no tag --> node not inserted in the history ancestors
|
||||
| `Do{...} -> OS(x); for _, y in ipairs(x) do S(y) end; CS(x)
|
||||
| `Set{ lhs, rhs } -> EL(lhs); EL(rhs)
|
||||
| `While{ cond, body } -> E(cond); OS(); B(body); CS()
|
||||
| `Repeat{ body, cond } -> OS(body); B(body); E(cond); CS(body)
|
||||
| `Local{ lhs } -> IL(lhs)
|
||||
| `Local{ lhs, rhs } -> EL(rhs); IL(lhs)
|
||||
| `Localrec{ lhs, rhs } -> IL(lhs); EL(rhs)
|
||||
| `Fornum{ i, a, b, body } -> E(a); E(b); OS(); IL{i}; B(body); CS()
|
||||
| `Fornum{ i, a, b, c, body } -> E(a); E(b); E(c); OS(); IL{i}; B(body); CS()
|
||||
| `Forin{ i, rhs, body } -> EL(rhs); OS(); IL(i); B(body); CS()
|
||||
| `If{...} ->
|
||||
for i=1, #x-1, 2 do
|
||||
E(x[i]); OS(); B(x[i+1]); CS()
|
||||
end
|
||||
if #x%2 == 1 then
|
||||
OS(); B(x[#x]); CS()
|
||||
end
|
||||
| `Call{...}|`Invoke{...}|`Return{...} -> EL(x)
|
||||
| `Break | `Goto{ _ } | `Label{ _ } -> -- nothing
|
||||
| { tag=tag, ...} if M.tags.stat[tag]->
|
||||
M.malformed (cfg, x, unpack (ancestors))
|
||||
| _ ->
|
||||
M.unknown (cfg, x, unpack (ancestors))
|
||||
end
|
||||
end
|
||||
|
||||
function M.traverse.expr (cfg, x, ...)
|
||||
if M.debug then pp.printf("traverse expr %s", x) end
|
||||
local ancestors = {...}
|
||||
local B = |y| M.block (cfg, y, x, unpack(ancestors)) -- Block
|
||||
local S = |y| M.stat (cfg, y, x, unpack(ancestors)) -- Statement
|
||||
local E = |y| M.expr (cfg, y, x, unpack(ancestors)) -- Expression
|
||||
local EL = |y| M.expr_list (cfg, y, x, unpack(ancestors)) -- Expression List
|
||||
local IL = |y| M.binder_list (cfg, y, x, unpack(ancestors)) -- Id binders list
|
||||
local OS = || cfg.scope :save() -- Open scope
|
||||
local CS = || cfg.scope :restore() -- Close scope
|
||||
|
||||
match x with
|
||||
| `Paren{ e } -> E(e)
|
||||
| `Call{...} | `Invoke{...} -> EL(x)
|
||||
| `Index{ a, b } -> E(a); E(b)
|
||||
| `Op{ opid, ... } -> E(x[2]); if #x==3 then E(x[3]) end
|
||||
| `Function{ params, body } -> OS(body); IL(params); B(body); CS(body)
|
||||
| `Stat{ b, e } -> OS(b); B(b); E(e); CS(b)
|
||||
| `Id{ name } -> M.occurrence(cfg, x, unpack(ancestors))
|
||||
| `Table{ ... } ->
|
||||
for i = 1, #x do match x[i] with
|
||||
| `Pair{ k, v } -> E(k); E(v)
|
||||
| v -> E(v)
|
||||
end end
|
||||
| `Nil|`Dots|`True|`False|`Number{_}|`String{_} -> -- terminal node
|
||||
| { tag=tag, ...} if M.tags.expr[tag]-> M.malformed (cfg, x, unpack (ancestors))
|
||||
| _ -> M.unknown (cfg, x, unpack (ancestors))
|
||||
end
|
||||
end
|
||||
|
||||
function M.traverse.block (cfg, x, ...)
|
||||
assert(type(x)=='table', "traverse.block() expects a table")
|
||||
if x.tag then M.malformed(cfg, x, ...)
|
||||
else for _, y in ipairs(x) do M.stat(cfg, y, x, ...) end
|
||||
end
|
||||
end
|
||||
|
||||
function M.traverse.expr_list (cfg, x, ...)
|
||||
assert(type(x)=='table', "traverse.expr_list() expects a table")
|
||||
-- x doesn't appear in the ancestors
|
||||
for _, y in ipairs(x) do M.expr(cfg, y, ...) end
|
||||
end
|
||||
|
||||
function M.malformed(cfg, x, ...)
|
||||
local f = cfg.malformed or cfg.error
|
||||
if f then f(x, ...) else
|
||||
error ("Malformed node of tag "..(x.tag or '(nil)'))
|
||||
end
|
||||
end
|
||||
|
||||
function M.unknown(cfg, x, ...)
|
||||
local f = cfg.unknown or cfg.error
|
||||
if f then f(x, ...) else
|
||||
error ("Unknown node tag "..(x.tag or '(nil)'))
|
||||
end
|
||||
end
|
||||
|
||||
function M.occurrence(cfg, x, ...)
|
||||
if cfg.occurrence then cfg.occurrence(cfg.scope :get(x[1]), x, ...) end
|
||||
end
|
||||
|
||||
-- TODO: Is it useful to call each error handling function?
|
||||
function M.binder_list (cfg, id_list, ...)
|
||||
local f = cfg.binder
|
||||
local ferror = cfg.error or cfg.malformed or cfg.unknown
|
||||
for i, id_node in ipairs(id_list) do
|
||||
local down, up = cfg.down, cfg.up
|
||||
if id_node.tag == 'Id' then
|
||||
cfg.scope :set (id_node[1], { id_node, ... })
|
||||
if down then down(id_node, ...) end
|
||||
if f then f(id_node, ...) end
|
||||
if up then up(id_node, ...) end
|
||||
elseif i==#id_list and id_node.tag=='Dots' then
|
||||
if down then down(id_node, ...) end
|
||||
if up then up(id_node, ...) end
|
||||
-- Do nothing, those are valid `Dots
|
||||
elseif ferror then
|
||||
-- Traverse error handling function
|
||||
ferror(id_node, ...)
|
||||
else
|
||||
error("Invalid binders list")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Generic walker generator.
|
||||
-- * if `cfg' has an entry matching the tree name, use this entry
|
||||
-- * if not, try to use the entry whose name matched the ast kind
|
||||
-- * if an entry is a table, look for 'up' and 'down' entries
|
||||
-- * if it is a function, consider it as a `down' traverser.
|
||||
----------------------------------------------------------------------
|
||||
local walker_builder = function(traverse)
|
||||
assert(traverse)
|
||||
return function (cfg, ...)
|
||||
if not cfg.scope then cfg.scope = M.newscope() end
|
||||
local down, up = cfg.down, cfg.up
|
||||
local broken = down and down(...)
|
||||
if broken ~= 'break' then M.traverse[traverse] (cfg, ...) end
|
||||
if up then up(...) end
|
||||
end
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Declare [M.stat], [M.expr], [M.block].
|
||||
-- `M.binder_list` is not here, because `cfg.up` and `cfg.down` must
|
||||
-- be called on individual binders, not on the list itself.
|
||||
-- It's therefore handled in `traverse.binder_list()`
|
||||
----------------------------------------------------------------------
|
||||
for _, w in ipairs{ "stat", "expr", "block" } do --, "malformed", "unknown" } do
|
||||
M[w] = walker_builder (w, M.traverse[w])
|
||||
end
|
||||
|
||||
-- Don't call up/down callbacks on expr lists
|
||||
M.expr_list = M.traverse.expr_list
|
||||
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Try to guess the type of the AST then choose the right walkker.
|
||||
----------------------------------------------------------------------
|
||||
function M.guess (cfg, x, ...)
|
||||
assert(type(x)=='table', "arg #2 in a walker must be an AST")
|
||||
if M.tags.expr[x.tag] then return M.expr(cfg, x, ...) end
|
||||
if M.tags.stat[x.tag] then return M.stat(cfg, x, ...) end
|
||||
if not x.tag then return M.block(cfg, x, ...) end
|
||||
error ("Can't guess the AST type from tag "..(x.tag or '<none>'))
|
||||
end
|
||||
|
||||
local S = { }; S.__index = S
|
||||
|
||||
function M.newscope()
|
||||
local instance = { current = { } }
|
||||
instance.stack = { instance.current }
|
||||
setmetatable (instance, S)
|
||||
return instance
|
||||
end
|
||||
|
||||
function S :save(...)
|
||||
local current_copy = { }
|
||||
for a, b in pairs(self.current) do current_copy[a]=b end
|
||||
table.insert (self.stack, current_copy)
|
||||
if ... then return self :add(...) end
|
||||
end
|
||||
|
||||
function S :restore() self.current = table.remove (self.stack) end
|
||||
function S :get (var_name) return self.current[var_name] end
|
||||
function S :set (key, val) self.current[key] = val end
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,241 @@
|
||||
--------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2011-2012 Sierra Wireless.
|
||||
-- All rights reserved. This program and the accompanying materials
|
||||
-- are made available under the terms of the Eclipse Public License v1.0
|
||||
-- which accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Simon BERNARD <sbernard@sierrawireless.com>
|
||||
-- - initial API and implementation and initial documentation
|
||||
--------------------------------------------------------------------------------
|
||||
local M = {}
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- API MODEL
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
function M._file()
|
||||
local file = {
|
||||
-- FIELDS
|
||||
tag = "file",
|
||||
name = nil, -- string
|
||||
shortdescription = "", -- string
|
||||
description = "", -- string
|
||||
types = {}, -- map from typename to type
|
||||
globalvars = {}, -- map from varname to item
|
||||
returns = {}, -- list of return
|
||||
|
||||
-- FUNCTIONS
|
||||
addtype = function (self,type)
|
||||
self.types[type.name] = type
|
||||
type.parent = self
|
||||
end,
|
||||
|
||||
mergetype = function (self,newtype,erase,erasesourcerangefield)
|
||||
local currenttype = self.types[newtype.name]
|
||||
if currenttype then
|
||||
-- merge recordtypedef
|
||||
if currenttype.tag =="recordtypedef" and newtype.tag == "recordtypedef" then
|
||||
-- merge fields
|
||||
for fieldname ,field in pairs( newtype.fields) do
|
||||
local currentfield = currenttype.fields[fieldname]
|
||||
if erase or not currentfield then
|
||||
currenttype:addfield(field)
|
||||
elseif erasesourcerangefield then
|
||||
if field.sourcerange.min and field.sourcerange.max then
|
||||
currentfield.sourcerange.min = field.sourcerange.min
|
||||
currentfield.sourcerange.max = field.sourcerange.max
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- merge descriptions and source ranges
|
||||
if erase then
|
||||
if newtype.description or newtype.description == "" then currenttype.description = newtype.description end
|
||||
if newtype.shortdescription or newtype.shortdescription == "" then currenttype.shortdescription = newtype.shortdescription end
|
||||
if newtype.sourcerange.min and newtype.sourcerange.max then
|
||||
currenttype.sourcerange.min = newtype.sourcerange.min
|
||||
currenttype.sourcerange.max = newtype.sourcerange.max
|
||||
end
|
||||
end
|
||||
-- merge functiontypedef
|
||||
elseif currenttype.tag == "functiontypedef" and newtype.tag == "functiontypedef" then
|
||||
-- merge params
|
||||
for i, param1 in ipairs(newtype.params) do
|
||||
local missing = true
|
||||
for j, param2 in ipairs(currenttype.params) do
|
||||
if param1.name == param2.name then
|
||||
missing = false
|
||||
break
|
||||
end
|
||||
end
|
||||
if missing then
|
||||
table.insert(currenttype.params,param1)
|
||||
end
|
||||
end
|
||||
|
||||
-- merge descriptions and source ranges
|
||||
if erase then
|
||||
if newtype.description or newtype.description == "" then currenttype.description = newtype.description end
|
||||
if newtype.shortdescription or newtype.shortdescription == "" then currenttype.shortdescription = newtype.shortdescription end
|
||||
if newtype.sourcerange.min and newtype.sourcerange.max then
|
||||
currenttype.sourcerange.min = newtype.sourcerange.min
|
||||
currenttype.sourcerange.max = newtype.sourcerange.max
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
self:addtype(newtype)
|
||||
end
|
||||
end,
|
||||
|
||||
addglobalvar = function (self,item)
|
||||
self.globalvars[item.name] = item
|
||||
item.parent = self
|
||||
end,
|
||||
|
||||
moduletyperef = function (self)
|
||||
if self and self.returns[1] and self.returns[1].types[1] then
|
||||
local typeref = self.returns[1].types[1]
|
||||
return typeref
|
||||
end
|
||||
end
|
||||
}
|
||||
return file
|
||||
end
|
||||
|
||||
function M._recordtypedef(name)
|
||||
local recordtype = {
|
||||
-- FIELDS
|
||||
tag = "recordtypedef",
|
||||
name = name, -- string (mandatory)
|
||||
shortdescription = "", -- string
|
||||
description = "", -- string
|
||||
fields = {}, -- map from fieldname to field
|
||||
sourcerange = {min=0,max=0},
|
||||
|
||||
-- FUNCTIONS
|
||||
addfield = function (self,field)
|
||||
self.fields[field.name] = field
|
||||
field.parent = self
|
||||
end
|
||||
}
|
||||
return recordtype
|
||||
end
|
||||
|
||||
function M._functiontypedef(name)
|
||||
return {
|
||||
tag = "functiontypedef",
|
||||
name = name, -- string (mandatory)
|
||||
shortdescription = "", -- string
|
||||
description = "", -- string
|
||||
params = {}, -- list of parameter
|
||||
returns = {} -- list of return
|
||||
}
|
||||
end
|
||||
|
||||
function M._parameter(name)
|
||||
return {
|
||||
tag = "parameter",
|
||||
name = name, -- string (mandatory)
|
||||
description = "", -- string
|
||||
type = nil -- typeref (external or internal or primitive typeref)
|
||||
}
|
||||
end
|
||||
|
||||
function M._item(name)
|
||||
return {
|
||||
-- FIELDS
|
||||
tag = "item",
|
||||
name = name, -- string (mandatory)
|
||||
shortdescription = "", -- string
|
||||
description = "", -- string
|
||||
type = nil, -- typeref (external or internal or primitive typeref)
|
||||
occurrences = {}, -- list of identifier (see internalmodel)
|
||||
sourcerange = {min=0, max=0},
|
||||
|
||||
-- This is A TRICK
|
||||
-- This value is ALWAYS nil, except for internal purposes (short references).
|
||||
external = nil,
|
||||
|
||||
-- FUNCTIONS
|
||||
addoccurence = function (self,occ)
|
||||
table.insert(self.occurrences,occ)
|
||||
occ.definition = self
|
||||
end,
|
||||
|
||||
resolvetype = function (self,file)
|
||||
if self and self.type then
|
||||
if self.type.tag =="internaltyperef" then
|
||||
-- if file is not given try to retrieve it.
|
||||
if not file then
|
||||
if self.parent and self.parent.tag == 'recordtypedef' then
|
||||
file = self.parent.parent
|
||||
elseif self.parent.tag == 'file' then
|
||||
file = self.parent
|
||||
end
|
||||
end
|
||||
if file then return file.types[self.type.typename] end
|
||||
elseif self.type.tag =="inlinetyperef" then
|
||||
return self.type.def
|
||||
end
|
||||
end
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
function M._externaltypref(modulename, typename)
|
||||
return {
|
||||
tag = "externaltyperef",
|
||||
modulename = modulename, -- string
|
||||
typename = typename -- string
|
||||
}
|
||||
end
|
||||
|
||||
function M._internaltyperef(typename)
|
||||
return {
|
||||
tag = "internaltyperef",
|
||||
typename = typename -- string
|
||||
}
|
||||
end
|
||||
|
||||
function M._primitivetyperef(typename)
|
||||
return {
|
||||
tag = "primitivetyperef",
|
||||
typename = typename -- string
|
||||
}
|
||||
end
|
||||
|
||||
function M._moduletyperef(modulename,returnposition)
|
||||
return {
|
||||
tag = "moduletyperef",
|
||||
modulename = modulename, -- string
|
||||
returnposition = returnposition -- number
|
||||
}
|
||||
end
|
||||
|
||||
function M._exprtyperef(expression,returnposition)
|
||||
return {
|
||||
tag = "exprtyperef",
|
||||
expression = expression, -- expression (see internal model)
|
||||
returnposition = returnposition -- number
|
||||
}
|
||||
end
|
||||
|
||||
function M._inlinetyperef(definition)
|
||||
return {
|
||||
tag = "inlinetyperef",
|
||||
def = definition, -- expression (see internal model)
|
||||
|
||||
}
|
||||
end
|
||||
|
||||
function M._return(description)
|
||||
return {
|
||||
tag = "return",
|
||||
description = description or "", -- string
|
||||
types = {} -- list of typref (external or internal or primitive typeref)
|
||||
}
|
||||
end
|
||||
return M
|
||||
@@ -0,0 +1,459 @@
|
||||
--------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2011-2012 Sierra Wireless.
|
||||
-- All rights reserved. This program and the accompanying materials
|
||||
-- are made available under the terms of the Eclipse Public License v1.0
|
||||
-- which accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Simon BERNARD <sbernard@sierrawireless.com>
|
||||
-- - initial API and implementation and initial documentation
|
||||
--------------------------------------------------------------------------------
|
||||
local apimodel = require "models.apimodel"
|
||||
local ldp = require "models.ldparser"
|
||||
local Q = require "metalua.treequery"
|
||||
|
||||
local M = {}
|
||||
|
||||
local handledcomments={} -- cache to know the comment already handled
|
||||
|
||||
----
|
||||
-- UTILITY METHODS
|
||||
local primitivetypes = {
|
||||
['boolean'] = true,
|
||||
['function'] = true,
|
||||
['nil'] = true,
|
||||
['number'] = true,
|
||||
['string'] = true,
|
||||
['table'] = true,
|
||||
['thread'] = true,
|
||||
['userdata'] = true
|
||||
}
|
||||
|
||||
-- get or create the typedef with the name "name"
|
||||
local function gettypedef(_file,name,kind,sourcerangemin,sourcerangemax)
|
||||
local kind = kind or "recordtypedef"
|
||||
local _typedef = _file.types[name]
|
||||
if _typedef then
|
||||
if _typedef.tag == kind then return _typedef end
|
||||
else
|
||||
if kind == "recordtypedef" and name ~= "global" then
|
||||
local _recordtypedef = apimodel._recordtypedef(name)
|
||||
|
||||
-- define sourcerange
|
||||
_recordtypedef.sourcerange.min = sourcerangemin
|
||||
_recordtypedef.sourcerange.max = sourcerangemax
|
||||
|
||||
-- add to file if a name is defined
|
||||
if _recordtypedef.name then _file:addtype(_recordtypedef) end
|
||||
return _recordtypedef
|
||||
elseif kind == "functiontypedef" then
|
||||
-- TODO support function
|
||||
return nil
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
-- create a typeref from the typref doc_tag
|
||||
local function createtyperef(dt_typeref,_file,sourcerangemin,sourcerangemax)
|
||||
local _typeref
|
||||
if dt_typeref.tag == "typeref" then
|
||||
if dt_typeref.module then
|
||||
-- manage external type
|
||||
_typeref = apimodel._externaltypref()
|
||||
_typeref.modulename = dt_typeref.module
|
||||
_typeref.typename = dt_typeref.type
|
||||
else
|
||||
if primitivetypes[dt_typeref.type] then
|
||||
-- manage primitive type
|
||||
_typeref = apimodel._primitivetyperef()
|
||||
_typeref.typename = dt_typeref.type
|
||||
else
|
||||
-- manage internal type
|
||||
_typeref = apimodel._internaltyperef()
|
||||
_typeref.typename = dt_typeref.type
|
||||
if _file then
|
||||
gettypedef(_file, _typeref.typename, "recordtypedef", sourcerangemin,sourcerangemax)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return _typeref
|
||||
end
|
||||
|
||||
-- create a return from the return doc_tag
|
||||
local function createreturn(dt_return,_file,sourcerangemin,sourcerangemax)
|
||||
local _return = apimodel._return()
|
||||
|
||||
_return.description = dt_return.description
|
||||
|
||||
-- manage typeref
|
||||
if dt_return.types then
|
||||
for _, dt_typeref in ipairs(dt_return.types) do
|
||||
local _typeref = createtyperef(dt_typeref,_file,sourcerangemin,sourcerangemax)
|
||||
if _typeref then
|
||||
table.insert(_return.types,_typeref)
|
||||
end
|
||||
end
|
||||
end
|
||||
return _return
|
||||
end
|
||||
|
||||
-- create a item from the field doc_tag
|
||||
local function createfield(dt_field,_file,sourcerangemin,sourcerangemax)
|
||||
local _item = apimodel._item(dt_field.name)
|
||||
|
||||
if dt_field.shortdescription then
|
||||
_item.shortdescription = dt_field.shortdescription
|
||||
_item.description = dt_field.description
|
||||
else
|
||||
_item.shortdescription = dt_field.description
|
||||
end
|
||||
|
||||
-- manage typeref
|
||||
local dt_typeref = dt_field.type
|
||||
if dt_typeref then
|
||||
_item.type = createtyperef(dt_typeref,_file,sourcerangemin,sourcerangemax)
|
||||
end
|
||||
return _item
|
||||
end
|
||||
|
||||
-- create a param from the param doc_tag
|
||||
local function createparam(dt_param,_file,sourcerangemin,sourcerangemax)
|
||||
if not dt_param.name then return nil end
|
||||
|
||||
local _parameter = apimodel._parameter(dt_param.name)
|
||||
_parameter.description = dt_param.description
|
||||
|
||||
-- manage typeref
|
||||
local dt_typeref = dt_param.type
|
||||
if dt_typeref then
|
||||
_parameter.type = createtyperef(dt_typeref,_file,sourcerangemin,sourcerangemax)
|
||||
end
|
||||
return _parameter
|
||||
end
|
||||
|
||||
-- get or create the typedef with the name "name"
|
||||
function M.additemtoparent(_file,_item,scope,sourcerangemin,sourcerangemax)
|
||||
if scope and not scope.module then
|
||||
if _item.name then
|
||||
if scope.type == "global" then
|
||||
_file:addglobalvar(_item)
|
||||
else
|
||||
local _recordtypedef = gettypedef (_file, scope.type ,"recordtypedef",sourcerangemin,sourcerangemax)
|
||||
_recordtypedef:addfield(_item)
|
||||
end
|
||||
else
|
||||
-- if no item name precise we store the scope in the item to be able to add it to the right parent later
|
||||
_item.scope = scope
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Function type counter
|
||||
local i = 0
|
||||
|
||||
-- Reset function type counter
|
||||
local function resetfunctiontypeidgenerator()
|
||||
i = 0
|
||||
end
|
||||
|
||||
-- Provides an unique index for a function type
|
||||
local function generatefunctiontypeid()
|
||||
i = i + 1
|
||||
return i
|
||||
end
|
||||
|
||||
-- generate a function type name
|
||||
local function generatefunctiontypename(_functiontypedef)
|
||||
local name = {"__"}
|
||||
if _functiontypedef.returns and _functiontypedef.returns[1] then
|
||||
local ret = _functiontypedef.returns[1]
|
||||
for _, type in ipairs(ret.types) do
|
||||
if type.typename then
|
||||
if type.modulename then
|
||||
table.insert(name,type.modulename)
|
||||
end
|
||||
table.insert(name,"#")
|
||||
table.insert(name,type.typename)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
table.insert(name,"=")
|
||||
if _functiontypedef.params then
|
||||
for _, param in ipairs(_functiontypedef.params) do
|
||||
local type = param.type
|
||||
if type then
|
||||
if type.typename then
|
||||
if type.modulename then
|
||||
table.insert(name,type.modulename)
|
||||
end
|
||||
table.insert(name,"#")
|
||||
table.insert(name,type.typename)
|
||||
else
|
||||
table.insert(name,"#unknown")
|
||||
end
|
||||
end
|
||||
table.insert(name,"[")
|
||||
table.insert(name,param.name)
|
||||
table.insert(name,"]")
|
||||
end
|
||||
end
|
||||
table.insert(name,"__")
|
||||
table.insert(name, generatefunctiontypeid())
|
||||
return table.concat(name)
|
||||
end
|
||||
|
||||
|
||||
|
||||
------------------------------------------------------
|
||||
-- create the module api
|
||||
function M.createmoduleapi(ast,modulename)
|
||||
|
||||
-- Initialise function type naming
|
||||
resetfunctiontypeidgenerator()
|
||||
|
||||
local _file = apimodel._file()
|
||||
|
||||
local _comment2apiobj = {}
|
||||
|
||||
local function handlecomment(comment)
|
||||
|
||||
-- Extract information from tagged comments
|
||||
local parsedcomment = ldp.parse(comment[1])
|
||||
if not parsedcomment then return nil end
|
||||
|
||||
-- Get tags from the languages
|
||||
local regulartags = parsedcomment.tags
|
||||
|
||||
-- Will contain last API object generated from comments
|
||||
local _lastapiobject
|
||||
|
||||
-- if comment is an ld comment
|
||||
if regulartags then
|
||||
-- manage "module" comment
|
||||
if regulartags["module"] then
|
||||
-- get name
|
||||
_file.name = regulartags["module"][1].name or modulename
|
||||
_lastapiobject = _file
|
||||
|
||||
-- manage descriptions
|
||||
_file.shortdescription = parsedcomment.shortdescription
|
||||
_file.description = parsedcomment.description
|
||||
|
||||
local sourcerangemin = comment.lineinfo.first.offset
|
||||
local sourcerangemax = comment.lineinfo.last.offset
|
||||
|
||||
-- manage returns
|
||||
if regulartags ["return"] then
|
||||
for _, dt_return in ipairs(regulartags ["return"]) do
|
||||
local _return = createreturn(dt_return,_file,sourcerangemin,sourcerangemax)
|
||||
table.insert(_file.returns,_return)
|
||||
end
|
||||
end
|
||||
-- if no returns on module create a defaultreturn of type #modulename
|
||||
if #_file.returns == 0 and _file.name then
|
||||
-- create internal type ref
|
||||
local _typeref = apimodel._internaltyperef()
|
||||
_typeref.typename = _file.name
|
||||
|
||||
-- create return
|
||||
local _return = apimodel._return()
|
||||
table.insert(_return.types,_typeref)
|
||||
|
||||
-- add return
|
||||
table.insert(_file.returns,_return)
|
||||
|
||||
--create recordtypedef is not define
|
||||
gettypedef(_file,_typeref.typename,"recordtypedef",sourcerangemin,sourcerangemax)
|
||||
end
|
||||
-- manage "type" comment
|
||||
elseif regulartags["type"] and regulartags["type"][1].name ~= "global" then
|
||||
local dt_type = regulartags["type"][1];
|
||||
-- create record type if it doesn't exist
|
||||
local sourcerangemin = comment.lineinfo.first.offset
|
||||
local sourcerangemax = comment.lineinfo.last.offset
|
||||
local _recordtypedef = gettypedef (_file, dt_type.name ,"recordtypedef",sourcerangemin,sourcerangemax)
|
||||
_lastapiobject = _recordtypedef
|
||||
|
||||
-- re-set sourcerange in case the type was created before the type tag
|
||||
_recordtypedef.sourcerange.min = sourcerangemin
|
||||
_recordtypedef.sourcerange.max = sourcerangemax
|
||||
|
||||
-- manage description
|
||||
_recordtypedef.shortdescription = parsedcomment.shortdescription
|
||||
_recordtypedef.description = parsedcomment.description
|
||||
|
||||
-- manage fields
|
||||
if regulartags["field"] then
|
||||
for _, dt_field in ipairs(regulartags["field"]) do
|
||||
local _item = createfield(dt_field,_file,sourcerangemin,sourcerangemax)
|
||||
-- define sourcerange only if we create it
|
||||
_item.sourcerange.min = sourcerangemin
|
||||
_item.sourcerange.max = sourcerangemax
|
||||
if _item then _recordtypedef:addfield(_item) end
|
||||
end
|
||||
end
|
||||
elseif regulartags["field"] then
|
||||
local dt_field = regulartags["field"][1]
|
||||
|
||||
-- create item
|
||||
local sourcerangemin = comment.lineinfo.first.offset
|
||||
local sourcerangemax = comment.lineinfo.last.offset
|
||||
local _item = createfield(dt_field,_file,sourcerangemin,sourcerangemax)
|
||||
_item.shortdescription = parsedcomment.shortdescription
|
||||
_item.description = parsedcomment.description
|
||||
_lastapiobject = _item
|
||||
|
||||
-- define sourcerange
|
||||
_item.sourcerange.min = sourcerangemin
|
||||
_item.sourcerange.max = sourcerangemax
|
||||
|
||||
-- add item to its parent
|
||||
local scope = regulartags["field"][1].parent
|
||||
M.additemtoparent(_file,_item,scope,sourcerangemin,sourcerangemax)
|
||||
elseif regulartags["function"] or regulartags["param"] or regulartags["return"] then
|
||||
-- create item
|
||||
local _item = apimodel._item()
|
||||
_item.shortdescription = parsedcomment.shortdescription
|
||||
_item.description = parsedcomment.description
|
||||
_lastapiobject = _item
|
||||
|
||||
-- set name
|
||||
if regulartags["function"] then _item.name = regulartags["function"][1].name end
|
||||
|
||||
-- define sourcerange
|
||||
local sourcerangemin = comment.lineinfo.first.offset
|
||||
local sourcerangemax = comment.lineinfo.last.offset
|
||||
_item.sourcerange.min = sourcerangemin
|
||||
_item.sourcerange.max = sourcerangemax
|
||||
|
||||
|
||||
-- create function type
|
||||
local _functiontypedef = apimodel._functiontypedef()
|
||||
_functiontypedef.shortdescription = parsedcomment.shortdescription
|
||||
_functiontypedef.description = parsedcomment.description
|
||||
|
||||
|
||||
-- manage params
|
||||
if regulartags["param"] then
|
||||
for _, dt_param in ipairs(regulartags["param"]) do
|
||||
local _param = createparam(dt_param,_file,sourcerangemin,sourcerangemax)
|
||||
table.insert(_functiontypedef.params,_param)
|
||||
end
|
||||
end
|
||||
|
||||
-- manage returns
|
||||
if regulartags["return"] then
|
||||
for _, dt_return in ipairs(regulartags["return"]) do
|
||||
local _return = createreturn(dt_return,_file,sourcerangemin,sourcerangemax)
|
||||
table.insert(_functiontypedef.returns,_return)
|
||||
end
|
||||
end
|
||||
|
||||
-- add type name
|
||||
_functiontypedef.name = generatefunctiontypename(_functiontypedef)
|
||||
_file:addtype(_functiontypedef)
|
||||
|
||||
-- create ref to this type
|
||||
local _internaltyperef = apimodel._internaltyperef()
|
||||
_internaltyperef.typename = _functiontypedef.name
|
||||
_item.type=_internaltyperef
|
||||
|
||||
-- add item to its parent
|
||||
local sourcerangemin = comment.lineinfo.first.offset
|
||||
local sourcerangemax = comment.lineinfo.last.offset
|
||||
local scope = (regulartags["function"] and regulartags["function"][1].parent) or nil
|
||||
M.additemtoparent(_file,_item,scope,sourcerangemin,sourcerangemax)
|
||||
end
|
||||
end
|
||||
|
||||
-- when we could not know which type of api object it is, we suppose this is an item
|
||||
if not _lastapiobject then
|
||||
_lastapiobject = apimodel._item()
|
||||
_lastapiobject.shortdescription = parsedcomment.shortdescription
|
||||
_lastapiobject.description = parsedcomment.description
|
||||
_lastapiobject.sourcerange.min = comment.lineinfo.first.offset
|
||||
_lastapiobject.sourcerange.max = comment.lineinfo.last.offset
|
||||
end
|
||||
|
||||
--
|
||||
-- Store user defined tags
|
||||
--
|
||||
local thirdtags = parsedcomment and parsedcomment.unknowntags
|
||||
if thirdtags then
|
||||
-- Define a storage index for user defined tags on current API element
|
||||
if not _lastapiobject.metadata then _lastapiobject.metadata = {} end
|
||||
|
||||
-- Loop over user defined tags
|
||||
for usertag, taglist in pairs(thirdtags) do
|
||||
if not _lastapiobject.metadata[ usertag ] then
|
||||
_lastapiobject.metadata[ usertag ] = {
|
||||
tag = usertag
|
||||
}
|
||||
end
|
||||
for _, tag in ipairs( taglist ) do
|
||||
table.insert(_lastapiobject.metadata[usertag], tag)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- if we create an api object linked it to
|
||||
_comment2apiobj[comment] =_lastapiobject
|
||||
end
|
||||
|
||||
local function parsecomment(node, parent, ...)
|
||||
-- check for comments before this node
|
||||
if node.lineinfo and node.lineinfo.first.comments then
|
||||
local comments = node.lineinfo.first.comments
|
||||
-- check all comments
|
||||
for _,comment in ipairs(comments) do
|
||||
-- if not already handled
|
||||
if not handledcomments[comment] then
|
||||
handlecomment(comment)
|
||||
handledcomments[comment]=true
|
||||
end
|
||||
end
|
||||
end
|
||||
-- check for comments after this node
|
||||
if node.lineinfo and node.lineinfo.last.comments then
|
||||
local comments = node.lineinfo.last.comments
|
||||
-- check all comments
|
||||
for _,comment in ipairs(comments) do
|
||||
-- if not already handled
|
||||
if not handledcomments[comment] then
|
||||
handlecomment(comment)
|
||||
handledcomments[comment]=true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Q(ast):filter(function(x) return x.tag~=nil end):foreach(parsecomment)
|
||||
return _file, _comment2apiobj
|
||||
end
|
||||
|
||||
|
||||
function M.extractlocaltype ( commentblock,_file)
|
||||
if not commentblock then return nil end
|
||||
|
||||
local stringcomment = commentblock[1]
|
||||
|
||||
local parsedtag = ldp.parseinlinecomment(stringcomment)
|
||||
if parsedtag then
|
||||
local sourcerangemin = commentblock.lineinfo.first.offset
|
||||
local sourcerangemax = commentblock.lineinfo.last.offset
|
||||
|
||||
return createtyperef(parsedtag,_file,sourcerangemin,sourcerangemax), parsedtag.description
|
||||
end
|
||||
|
||||
return nil, stringcomment
|
||||
end
|
||||
|
||||
M.generatefunctiontypename = generatefunctiontypename
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,65 @@
|
||||
--------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2012 Sierra Wireless.
|
||||
-- All rights reserved. This program and the accompanying materials
|
||||
-- are made available under the terms of the Eclipse Public License v1.0
|
||||
-- which accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Kevin KIN-FOO <kkinfoo@sierrawireless.com>
|
||||
-- - initial API and implementation and initial documentation
|
||||
--------------------------------------------------------------------------------
|
||||
local M = {}
|
||||
|
||||
function M._internalcontent()
|
||||
return {
|
||||
content = nil, -- block
|
||||
unknownglobalvars = {}, -- list of item
|
||||
tag = "MInternalContent"
|
||||
}
|
||||
end
|
||||
|
||||
function M._block()
|
||||
return {
|
||||
content = {}, -- list of expr (identifier, index, call, invoke, block)
|
||||
localvars = {}, -- list of {var=item, scope ={min,max}}
|
||||
sourcerange = {min=0,max=0},
|
||||
tag = "MBlock"
|
||||
}
|
||||
end
|
||||
|
||||
function M._identifier()
|
||||
return {
|
||||
definition = nil, -- item
|
||||
sourcerange = {min=0,max=0},
|
||||
tag = "MIdentifier"
|
||||
}
|
||||
end
|
||||
|
||||
function M._index(key, value)
|
||||
return {
|
||||
left= key, -- expr (identifier, index, call, invoke, block)
|
||||
right= value, -- string
|
||||
sourcerange = {min=0,max=0},
|
||||
tag = "MIndex"
|
||||
}
|
||||
end
|
||||
|
||||
function M._call(funct)
|
||||
return {
|
||||
func = funct, -- expr (identifier, index, call, invoke, block)
|
||||
sourcerange = {min=0,max=0},
|
||||
tag = "MCall"
|
||||
}
|
||||
end
|
||||
|
||||
function M._invoke(name, expr)
|
||||
return {
|
||||
functionname = name, -- string
|
||||
record = expr, -- expr (identifier, index, call, invoke, block)
|
||||
sourcerange = {min=0,max=0},
|
||||
tag = "MInvoke"
|
||||
}
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,861 @@
|
||||
--------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2011-2012 Sierra Wireless.
|
||||
-- All rights reserved. This program and the accompanying materials
|
||||
-- are made available under the terms of the Eclipse Public License v1.0
|
||||
-- which accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Simon BERNARD <sbernard@sierrawireless.com>
|
||||
-- - initial API and implementation and initial documentation
|
||||
--------------------------------------------------------------------------------
|
||||
-{ extension ('match', ...) }
|
||||
|
||||
local Q = require 'metalua.treequery'
|
||||
|
||||
local internalmodel = require 'models.internalmodel'
|
||||
local apimodel = require 'models.apimodel'
|
||||
local apimodelbuilder = require 'models.apimodelbuilder'
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Analyzes an AST and returns two tables
|
||||
-- * `locals`, which associates `Id{ } nodes which create a local variable
|
||||
-- to a list of the `Id{ } occurrence nodes of that variable;
|
||||
-- * `globals` which associates variable names to occurrences of
|
||||
-- global variables having that name.
|
||||
function bindings(ast)
|
||||
local locals, globals = { }, { }
|
||||
local function f(id, ...)
|
||||
local name = id[1]
|
||||
if Q.is_binder(id, ...) then
|
||||
local binder = ... -- parent is the binder
|
||||
locals[binder] = locals[binder] or { }
|
||||
locals[binder][name]={ }
|
||||
else
|
||||
local _, binder = Q.get_binder(id, ...)
|
||||
if binder then -- this is a local
|
||||
table.insert(locals[binder][name], id)
|
||||
else
|
||||
local g = globals[name]
|
||||
if g then table.insert(g, id) else globals[name]={id} end
|
||||
end
|
||||
end
|
||||
end
|
||||
Q(ast) :filter('Id') :foreach(f)
|
||||
return locals, globals
|
||||
end
|
||||
|
||||
-- --------------------------------------
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- return the comment linked before to this node
|
||||
-- ----------------------------------------------------------
|
||||
local function getlinkedcommentbefore(node)
|
||||
local function _getlinkedcomment(node,line)
|
||||
if node and node.lineinfo and node.lineinfo.first.line == line then
|
||||
-- get the last comment before (the nearest of code)
|
||||
local comments = node.lineinfo.first.comments
|
||||
local comment = comments and comments[#comments]
|
||||
if comment and comment.lineinfo.last.line == line-1 then
|
||||
-- ignore the comment if there are code before on the same line
|
||||
if node.lineinfo.first.facing and (node.lineinfo.first.facing.line ~= comment.lineinfo.first.line) then
|
||||
return comment
|
||||
end
|
||||
else
|
||||
return _getlinkedcomment(node.parent,line)
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
if node.lineinfo and node.lineinfo.first.line then
|
||||
return _getlinkedcomment(node,node.lineinfo.first.line)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- return the comment linked after to this node
|
||||
-- ----------------------------------------------------------
|
||||
local function getlinkedcommentafter(node)
|
||||
local function _getlinkedcomment(node,line)
|
||||
if node and node.lineinfo and node.lineinfo.last.line == line then
|
||||
-- get the first comment after (the nearest of code)
|
||||
local comments = node.lineinfo.last.comments
|
||||
local comment = comments and comments[1]
|
||||
if comment and comment.lineinfo.first.line == line then
|
||||
return comment
|
||||
else
|
||||
return _getlinkedcomment(node.parent,line)
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
if node.lineinfo and node.lineinfo.last.line then
|
||||
return _getlinkedcomment(node,node.lineinfo.last.line)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- return true if this node is a block for the internal representation
|
||||
-- ----------------------------------------------------------
|
||||
local supported_b = {
|
||||
Function = true,
|
||||
Do = true,
|
||||
While = true,
|
||||
Fornum = true,
|
||||
Forin = true,
|
||||
Repeat = true,
|
||||
}
|
||||
local function supportedblock(node, parent)
|
||||
return supported_b[ node.tag ] or
|
||||
(parent and parent.tag == "If" and node.tag == nil)
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- create a block from the metalua node
|
||||
-- ----------------------------------------------------------
|
||||
local function createblock(block, parent)
|
||||
local _block = internalmodel._block()
|
||||
match block with
|
||||
| `Function{param, body}
|
||||
| `Do{...}
|
||||
| `Fornum {identifier, min, max, body}
|
||||
| `Forin {identifiers, exprs, body}
|
||||
| `Repeat {body, expr} ->
|
||||
_block.sourcerange.min = block.lineinfo.first.offset
|
||||
_block.sourcerange.max = block.lineinfo.last.offset
|
||||
| `While {expr, body} ->
|
||||
_block.sourcerange.min = body.lineinfo.first.facing.offset
|
||||
_block.sourcerange.max = body.lineinfo.last.facing.offset
|
||||
| _ ->
|
||||
if parent and parent.tag == "If" and block.tag == nil then
|
||||
_block.sourcerange.min = block.lineinfo.first.facing.offset
|
||||
_block.sourcerange.max = block.lineinfo.last.facing.offset
|
||||
end
|
||||
end
|
||||
return _block
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- return true if this node is a expression in the internal representation
|
||||
-- ----------------------------------------------------------
|
||||
local supported_e = {
|
||||
Index = true,
|
||||
Id = true,
|
||||
Call = true,
|
||||
Invoke = true
|
||||
}
|
||||
local function supportedexpr(node)
|
||||
return supported_e[ node.tag ]
|
||||
end
|
||||
|
||||
local idto_block = {} -- cache from metalua id to internal model block
|
||||
local idto_identifier = {} -- cache from metalua id to internal model indentifier
|
||||
local expreto_expression = {} -- cache from metalua expression to internal model expression
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- create an expression from a metalua node
|
||||
-- ----------------------------------------------------------
|
||||
local function createexpr(expr,_block)
|
||||
local _expr = nil
|
||||
|
||||
match expr with
|
||||
| `Id { name } ->
|
||||
-- we store the block which hold this node
|
||||
-- to be able to define
|
||||
idto_block[expr]= _block
|
||||
|
||||
-- if expr has not line info, it means expr has no representation in the code
|
||||
-- so we don't need it.
|
||||
if not expr.lineinfo then return nil end
|
||||
|
||||
-- create identifier
|
||||
local _identifier = internalmodel._identifier()
|
||||
idto_identifier[expr]= _identifier
|
||||
_expr = _identifier
|
||||
| `Index { innerexpr, `String{fieldname} } ->
|
||||
if not expr.lineinfo then return nil end
|
||||
-- create index
|
||||
local _expression = createexpr(innerexpr,_block)
|
||||
if _expression then _expr = internalmodel._index(_expression,fieldname) end
|
||||
| `Call{innerexpr, ...} ->
|
||||
if not expr.lineinfo then return nil end
|
||||
-- create call
|
||||
local _expression = createexpr(innerexpr,_block)
|
||||
if _expression then _expr = internalmodel._call(_expression) end
|
||||
| `Invoke{innerexpr,`String{functionname},...} ->
|
||||
if not expr.lineinfo then return nil end
|
||||
-- create invoke
|
||||
local _expression = createexpr(innerexpr,_block)
|
||||
if _expression then _expr = internalmodel._invoke(functionname,_expression) end
|
||||
| _ ->
|
||||
end
|
||||
|
||||
if _expr then
|
||||
_expr.sourcerange.min = expr.lineinfo.first.offset
|
||||
_expr.sourcerange.max = expr.lineinfo.last.offset
|
||||
|
||||
expreto_expression[expr] = _expr
|
||||
end
|
||||
|
||||
return _expr
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- create block and expression node
|
||||
-- ----------------------------------------------------------
|
||||
local function createtreestructure(ast)
|
||||
-- create internal content
|
||||
local _internalcontent = internalmodel._internalcontent()
|
||||
|
||||
-- create root block
|
||||
local _block = internalmodel._block()
|
||||
local _blocks = { _block }
|
||||
_block.sourcerange.min = ast.lineinfo.first.facing.offset
|
||||
-- TODO remove the math.max when we support partial AST
|
||||
_block.sourcerange.max = math.max(ast.lineinfo.last.facing.offset, 10000)
|
||||
|
||||
_internalcontent.content = _block
|
||||
|
||||
-- visitor function (down)
|
||||
local function down (node,parent)
|
||||
if supportedblock(node,parent) then
|
||||
-- create the block
|
||||
local _block = createblock(node,parent)
|
||||
-- add it to parent block
|
||||
table.insert(_blocks[#_blocks].content, _block)
|
||||
-- enqueue the last block to know the "current" block
|
||||
table.insert(_blocks,_block)
|
||||
elseif supportedexpr(node) then
|
||||
-- we handle expression only if it was not already do
|
||||
if not expreto_expression[node] then
|
||||
-- create expr
|
||||
local _expression = createexpr(node,_blocks[#_blocks])
|
||||
-- add it to parent block
|
||||
if _expression then
|
||||
table.insert(_blocks[#_blocks].content, _expression)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- visitor function (up)
|
||||
local function up (node, parent)
|
||||
if supportedblock(node,parent) then
|
||||
-- dequeue the last block to know the "current" block
|
||||
table.remove(_blocks,#_blocks)
|
||||
end
|
||||
end
|
||||
|
||||
-- visit ast and build internal model
|
||||
Q(ast):foreach(down,up)
|
||||
|
||||
return _internalcontent
|
||||
end
|
||||
|
||||
local getitem
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- create the type from the node and position
|
||||
-- ----------------------------------------------------------
|
||||
local function createtype(node,position,comment2apiobj,file)
|
||||
-- create module type ref
|
||||
match node with
|
||||
| `Call{ `Id "require", `String {modulename}} ->
|
||||
return apimodel._moduletyperef(modulename,position)
|
||||
| `Function {params, body} ->
|
||||
-- create the functiontypedef from code
|
||||
local _functiontypedef = apimodel._functiontypedef()
|
||||
for _, p in ipairs(params) do
|
||||
-- create parameters
|
||||
local paramname
|
||||
if p.tag=="Dots" then
|
||||
paramname = "..."
|
||||
else
|
||||
paramname = p[1]
|
||||
end
|
||||
local _param = apimodel._parameter(paramname)
|
||||
table.insert(_functiontypedef.params,_param)
|
||||
end
|
||||
_functiontypedef.name = "___" -- no name for inline type
|
||||
|
||||
return apimodel._inlinetyperef(_functiontypedef)
|
||||
| `String {value} ->
|
||||
local typeref = apimodel._primitivetyperef("string")
|
||||
return typeref
|
||||
| `Number {value} ->
|
||||
local typeref = apimodel._primitivetyperef("number")
|
||||
return typeref
|
||||
| `True | `False ->
|
||||
local typeref = apimodel._primitivetyperef("boolean")
|
||||
return typeref
|
||||
| `Table {...} ->
|
||||
-- create recordtypedef from code
|
||||
local _recordtypedef = apimodel._recordtypedef("___") -- no name for inline type
|
||||
-- for each element of the table
|
||||
for i=1,select("#", ...) do
|
||||
local pair = select(i, ...)
|
||||
-- if this is a pair we create a new item in the type
|
||||
if pair.tag == "Pair" then
|
||||
-- create an item
|
||||
local _item = getitem(pair,nil, comment2apiobj,file)
|
||||
if _item then
|
||||
_recordtypedef:addfield(_item)
|
||||
end
|
||||
end
|
||||
end
|
||||
return apimodel._inlinetyperef(_recordtypedef)
|
||||
| _ ->
|
||||
end
|
||||
-- if node is an expression supported
|
||||
local supportedexpr = expreto_expression[node]
|
||||
if supportedexpr then
|
||||
-- create expression type ref
|
||||
return apimodel._exprtyperef(supportedexpr,position)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
local function completeapidoctype(apidoctype,itemname,init,file,comment2apiobj)
|
||||
if not apidoctype.name then
|
||||
apidoctype.name = itemname
|
||||
file:mergetype(apidoctype)
|
||||
end
|
||||
|
||||
-- create type from code
|
||||
local typeref = createtype(init,1,comment2apiobj,file)
|
||||
if typeref and typeref.tag == "inlinetyperef"
|
||||
and typeref.def.tag == "recordtypedef" then
|
||||
|
||||
-- set the name
|
||||
typeref.def.name = apidoctype.name
|
||||
|
||||
-- merge the type with priority to documentation except for source range
|
||||
file:mergetype(typeref.def,false,true)
|
||||
end
|
||||
end
|
||||
|
||||
local function completeapidocitem (apidocitem, itemname, init, file, binder, comment2apiobj)
|
||||
-- manage the case item has no name
|
||||
if not apidocitem.name then
|
||||
apidocitem.name = itemname
|
||||
|
||||
-- if item has no name this means it could not be attach to a parent
|
||||
if apidocitem.scope then
|
||||
apimodelbuilder.additemtoparent(file,apidocitem,apidocitem.scope,apidocitem.sourcerange.min,apidocitem.sourcerange.max)
|
||||
apidocitem.scope = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- for function try to merge definition
|
||||
local apitype = apidocitem:resolvetype(file)
|
||||
if apitype and apitype.tag == "functiontypedef" then
|
||||
local codetype = createtype(init,1,comment2apiobj,file)
|
||||
if codetype and codetype.tag =="inlinetyperef" then
|
||||
codetype.def.name = apitype.name
|
||||
file:mergetype(codetype.def)
|
||||
end
|
||||
end
|
||||
|
||||
-- manage the case item has no type
|
||||
if not apidocitem.type then
|
||||
-- extract typing from comment
|
||||
local type, desc = apimodelbuilder.extractlocaltype(getlinkedcommentafter(binder),file)
|
||||
|
||||
if type then
|
||||
apidocitem.type = type
|
||||
else
|
||||
-- if not found extracttype from code
|
||||
apidocitem.type = createtype(init,1,comment2apiobj,file)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- create or get the item finding in the binder with the given itemname
|
||||
-- return also the ast node corresponding to this item
|
||||
-- ----------------------------------------------------------
|
||||
getitem = function (binder, itemname, comment2apiobj, file)
|
||||
|
||||
-- local function to create item
|
||||
local function createitem(itemname, astnode, itemtype, description)
|
||||
local _item = apimodel._item(itemname)
|
||||
if description then _item.description = description end
|
||||
_item.type = itemtype
|
||||
if astnode and astnode.lineinfo then
|
||||
_item.sourcerange.min = astnode.lineinfo.first.offset
|
||||
_item.sourcerange.max = astnode.lineinfo.last.offset
|
||||
end
|
||||
return _item, astnode
|
||||
end
|
||||
|
||||
-- try to match binder with known patter of item declaration
|
||||
match binder with
|
||||
| `Pair {string, init}
|
||||
| `Set { {`Index { right , string}}, {init,...}} if string and string.tag =="String" ->
|
||||
-- Pair and set is for searching field from type ..
|
||||
-- if the itemname is given this mean we search for a local or a global not a field type.
|
||||
if not itemname then
|
||||
local itemname = string[1]
|
||||
|
||||
-- check for luadoc typing
|
||||
local commentbefore = getlinkedcommentbefore(binder)
|
||||
local apiobj = comment2apiobj[commentbefore] -- find apiobj linked to this comment
|
||||
if apiobj then
|
||||
if apiobj.tag=="item" then
|
||||
if not apiobj.name or apiobj.name == itemname then
|
||||
-- use code to complete api information if it's necessary
|
||||
completeapidocitem(apiobj, itemname, init,file,binder,comment2apiobj)
|
||||
-- for item use code source range rather than doc source range
|
||||
if string and string.lineinfo then
|
||||
apiobj.sourcerange.min = string.lineinfo.first.offset
|
||||
apiobj.sourcerange.max = string.lineinfo.last.offset
|
||||
end
|
||||
return apiobj, string
|
||||
end
|
||||
elseif apiobj.tag=="recordtypedef" then
|
||||
-- use code to complete api information if it's necessary
|
||||
completeapidoctype(apiobj, itemname, init,file,comment2apiobj)
|
||||
return createitem(itemname, string, apimodel._internaltyperef(apiobj.name), nil)
|
||||
end
|
||||
|
||||
-- if the apiobj could not be associated to the current obj,
|
||||
-- we do not use the documentation neither
|
||||
commentbefore = nil
|
||||
end
|
||||
|
||||
-- else we use code to extract the type and description
|
||||
-- check for "local" typing
|
||||
local type, desc = apimodelbuilder.extractlocaltype(getlinkedcommentafter(binder),file)
|
||||
local desc = desc or (commentbefore and commentbefore[1])
|
||||
if type then
|
||||
return createitem(itemname, string, type, desc )
|
||||
else
|
||||
-- if no "local typing" extract type from code
|
||||
return createitem(itemname, string, createtype(init,1,comment2apiobj,file), desc)
|
||||
end
|
||||
end
|
||||
| `Set {ids, inits}
|
||||
| `Local {ids, inits} ->
|
||||
-- if this is a single local var declaration
|
||||
-- we check if there are a comment block linked and try to extract the type
|
||||
if #ids == 1 then
|
||||
local currentid, currentinit = ids[1],inits[1]
|
||||
-- ignore non Ids node
|
||||
if currentid.tag ~= 'Id' or currentid[1] ~= itemname then return nil end
|
||||
|
||||
-- check for luadoc typing
|
||||
local commentbefore = getlinkedcommentbefore(binder)
|
||||
local apiobj = comment2apiobj[commentbefore] -- find apiobj linked to this comment
|
||||
if apiobj then
|
||||
if apiobj.tag=="item" then
|
||||
-- use code to complete api information if it's necessary
|
||||
if not apiobj.name or apiobj.name == itemname then
|
||||
completeapidocitem(apiobj, itemname, currentinit,file,binder,comment2apiobj)
|
||||
-- if this is a global var or if is has no parent
|
||||
-- we do not create a new item
|
||||
if not apiobj.parent or apiobj.parent == file then
|
||||
-- for item use code source range rather than doc source range
|
||||
if currentid and currentid.lineinfo then
|
||||
apiobj.sourcerange.min = currentid.lineinfo.first.offset
|
||||
apiobj.sourcerange.max = currentid.lineinfo.last.offset
|
||||
end
|
||||
return apiobj, currentid
|
||||
else
|
||||
return createitem(itemname, currentid, apiobj.type, nil)
|
||||
end
|
||||
end
|
||||
elseif apiobj.tag=="recordtypedef" then
|
||||
-- use code to complete api information if it's necessary
|
||||
completeapidoctype(apiobj, itemname, currentinit,file,comment2apiobj)
|
||||
return createitem(itemname, currentid, apimodel._internaltyperef(apiobj.name), nil)
|
||||
end
|
||||
|
||||
-- if the apiobj could not be associated to the current obj,
|
||||
-- we do not use the documentation neither
|
||||
commentbefore = nil
|
||||
end
|
||||
|
||||
-- else we use code to extract the type and description
|
||||
-- check for "local" typing
|
||||
local type,desc = apimodelbuilder.extractlocaltype(getlinkedcommentafter(binder),file)
|
||||
desc = desc or (commentbefore and commentbefore[1])
|
||||
if type then
|
||||
return createitem(itemname, currentid, type, desc)
|
||||
else
|
||||
-- if no "local typing" extract type from code
|
||||
return createitem(itemname, currentid, createtype(currentinit,1,comment2apiobj,file), desc)
|
||||
end
|
||||
end
|
||||
-- else we use code to extract the type
|
||||
local init,returnposition = nil,1
|
||||
for i,id in ipairs(ids) do
|
||||
-- calculate the current return position
|
||||
if init and (init.tag == "Call" or init.tag == "Invoke") then
|
||||
-- if previous init was a call or an invoke
|
||||
-- we increment the returnposition
|
||||
returnposition= returnposition+1
|
||||
else
|
||||
-- if init is not a function call
|
||||
-- we change the init used to determine the type
|
||||
init = inits[i]
|
||||
end
|
||||
|
||||
-- get the name of the current id
|
||||
local idname = id[1]
|
||||
|
||||
-- if this is the good id
|
||||
if itemname == idname then
|
||||
-- create type from init node and return position
|
||||
return createitem (itemname, id, createtype(init,returnposition,comment2apiobj,file),nil)
|
||||
end
|
||||
end
|
||||
| `Function {params, body} ->
|
||||
for i,id in ipairs(params) do
|
||||
-- get the name of the current id
|
||||
local idname = id[1]
|
||||
-- if this is the good id
|
||||
if itemname == idname then
|
||||
-- extract param's type from luadocumentation
|
||||
local obj = comment2apiobj[getlinkedcommentbefore(binder)]
|
||||
if obj and obj.tag=="item" then
|
||||
local typedef = obj:resolvetype(file)
|
||||
if typedef and typedef.tag =="functiontypedef" then
|
||||
for j, param in ipairs(typedef.params) do
|
||||
if i==j then
|
||||
if i ==1 and itemname == "self" and param.type == nil
|
||||
and obj.parent and obj.parent.tag == "recordtypedef" and obj.parent.name then
|
||||
param.type = apimodel._internaltyperef(obj.parent.name)
|
||||
end
|
||||
-- TODO perhaps we must clone the typeref
|
||||
return createitem(itemname,id, param.type,param.description)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return createitem(itemname,id)
|
||||
end
|
||||
end
|
||||
| `Forin {ids, expr, body} ->
|
||||
for i,id in ipairs(ids) do
|
||||
-- get the name of the current id
|
||||
local idname = id[1]
|
||||
-- if this is the good id
|
||||
if itemname == idname then
|
||||
-- return data : we can not guess the type for now
|
||||
return createitem(itemname,id)
|
||||
end
|
||||
end
|
||||
| `Fornum {id, ...} ->
|
||||
-- get the name of the current id
|
||||
local idname = id[1]
|
||||
-- if this is the good id
|
||||
if itemname == idname then
|
||||
-- return data : we can not guess the type for now
|
||||
return createitem(itemname,id)
|
||||
end
|
||||
| `Localrec {{id}, {func}} ->
|
||||
-- get the name of the current id
|
||||
local idname = id[1]
|
||||
-- if this is the good id
|
||||
if itemname == idname then
|
||||
-- check for luadoc typing
|
||||
local commentbefore = getlinkedcommentbefore(binder)
|
||||
local apiobj = comment2apiobj[commentbefore] -- find apiobj linked to this comment
|
||||
if apiobj then
|
||||
if apiobj.tag=="item" then
|
||||
if not apiobj.name or apiobj.name == itemname then
|
||||
-- use code to complete api information if it's necessary
|
||||
completeapidocitem(apiobj, itemname, func,file,binder,comment2apiobj)
|
||||
return createitem(itemname,id,apiobj.type,nil)
|
||||
end
|
||||
end
|
||||
|
||||
-- if the apiobj could not be associated to the current obj,
|
||||
-- we do not use the documentation neither
|
||||
commentbefore = nil
|
||||
end
|
||||
|
||||
-- else we use code to extract the type and description
|
||||
-- check for "local" typing
|
||||
local type,desc = apimodelbuilder.extractlocaltype(getlinkedcommentafter(binder),file)
|
||||
desc = desc or (commentbefore and commentbefore[1])
|
||||
if type then
|
||||
return createitem(itemname, id, type, desc)
|
||||
else
|
||||
-- if no "local typing" extract type from code
|
||||
return createitem(itemname, id, createtype(func,1,comment2apiobj,file), desc)
|
||||
end
|
||||
end
|
||||
| _ ->
|
||||
end
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- Search from Id node to Set node to find field of type.
|
||||
--
|
||||
-- Lua code : table.field1.field2 = 12
|
||||
-- looks like that in metalua :
|
||||
-- `Set{
|
||||
-- `Index { `Index { `Id "table", `String "field1" },
|
||||
-- `String "field2"},
|
||||
-- `Number "12"}
|
||||
-- ----------------------------------------------------------
|
||||
local function searchtypefield(node,_currentitem,comment2apiobj,file)
|
||||
|
||||
-- we are just interested :
|
||||
-- by item which is field of recordtypedef
|
||||
-- by ast node which are Index
|
||||
if _currentitem then
|
||||
local type = _currentitem:resolvetype(file)
|
||||
if type and type.tag == "recordtypedef" then
|
||||
if node and node.tag == "Index" then
|
||||
local rightpart = node[2]
|
||||
local _newcurrentitem = type.fields[rightpart[1]]
|
||||
|
||||
if _newcurrentitem then
|
||||
-- if this index represent a known field of the type we continue to search
|
||||
searchtypefield (node.parent,_newcurrentitem,comment2apiobj,file)
|
||||
else
|
||||
-- if not, this is perhaps a new field, but
|
||||
-- to be a new field this index must be include in a Set
|
||||
if node.parent and node.parent.tag =="Set" then
|
||||
-- in this case we create the new item ans add it to the type
|
||||
local set = node.parent
|
||||
local item, string = getitem(set,nil, comment2apiobj,file)
|
||||
-- add this item to the type, only if it has no parent and if this type does not contain already this field
|
||||
if item and not item.parent and string and not type.fields[string[1]] then
|
||||
type:addfield(item)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- create local vars, global vars and linked it with theirs occurences
|
||||
-- ----------------------------------------------------------
|
||||
local function createvardefinitions(_internalcontent,ast,file,comment2apiobj)
|
||||
-- use bindings to get locals and globals definition
|
||||
local locals, globals = bindings( ast )
|
||||
|
||||
-- create locals var
|
||||
for binder, namesAndOccurrences in pairs(locals) do
|
||||
for name, occurrences in pairs(namesAndOccurrences) do
|
||||
-- get item, id
|
||||
local _item, id = getitem(binder, name,comment2apiobj,file)
|
||||
if id then
|
||||
-- add definition as occurence
|
||||
-- we consider the identifier in the binder as an occurence
|
||||
local _identifierdef = idto_identifier[id]
|
||||
if _identifierdef then
|
||||
table.insert(_item.occurrences, _identifierdef)
|
||||
_identifierdef.definition = _item
|
||||
end
|
||||
|
||||
-- add occurences
|
||||
for _,occurrence in ipairs(occurrences) do
|
||||
searchtypefield(occurrence.parent, _item,comment2apiobj,file)
|
||||
local _identifier = idto_identifier[occurrence]
|
||||
if _identifier then
|
||||
table.insert(_item.occurrences, _identifier)
|
||||
_identifier.definition = _item
|
||||
end
|
||||
end
|
||||
|
||||
-- add item to block
|
||||
local _block = idto_block[id]
|
||||
table.insert(_block.localvars,{item=_item,scope = {min=0,max=0}})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- create globals var
|
||||
for name, occurrences in pairs( globals ) do
|
||||
|
||||
-- get or create definition
|
||||
local _item = file.globalvars[name]
|
||||
local binder = occurrences[1].parent
|
||||
if not _item then
|
||||
-- global declaration is only if the first occurence in left part of a 'Set'
|
||||
if binder and binder.tag == "Set" then
|
||||
_item = getitem(binder, name,comment2apiobj,file)
|
||||
end
|
||||
|
||||
-- if we find and item this is a global var declaration
|
||||
if _item then
|
||||
file:addglobalvar(_item)
|
||||
else
|
||||
-- else it is an unknown global var
|
||||
_item = apimodel._item(name)
|
||||
local _firstoccurrence = idto_identifier[occurrences[1]]
|
||||
if _firstoccurrence then
|
||||
_item.sourcerange.min = _firstoccurrence.sourcerange.min
|
||||
_item.sourcerange.max = _firstoccurrence.sourcerange.max
|
||||
end
|
||||
table.insert(_internalcontent.unknownglobalvars,_item)
|
||||
end
|
||||
else
|
||||
-- if the global var definition already exists, we just try to it
|
||||
if binder then
|
||||
match binder with
|
||||
| `Set {ids, inits} ->
|
||||
-- manage case only if there are 1 element in the Set
|
||||
if #ids == 1 then
|
||||
local currentid, currentinit = ids[1],inits[1]
|
||||
-- ignore non Ids node and bad name
|
||||
if currentid.tag == 'Id' and currentid[1] == name then
|
||||
completeapidocitem(_item, name, currentinit,file,binder,comment2apiobj)
|
||||
|
||||
if currentid and currentid.lineinfo then
|
||||
_item.sourcerange.min = currentid.lineinfo.first.offset
|
||||
_item.sourcerange.max = currentid.lineinfo.last.offset
|
||||
end
|
||||
end
|
||||
end
|
||||
| _ ->
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- add occurences
|
||||
for _,occurence in ipairs(occurrences) do
|
||||
local _identifier = idto_identifier[occurence]
|
||||
searchtypefield(occurence.parent, _item,comment2apiobj,file)
|
||||
if _identifier then
|
||||
table.insert(_item.occurrences, _identifier)
|
||||
_identifier.definition = _item
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- add parent to all ast node
|
||||
-- ----------------------------------------------------------
|
||||
local function addparents(ast)
|
||||
-- visitor function (down)
|
||||
local function down (node,parent)
|
||||
node.parent = parent
|
||||
end
|
||||
|
||||
-- visit ast and build internal model
|
||||
Q(ast):foreach(down,up)
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- try to detect a module declaration from code
|
||||
-- ----------------------------------------------------------
|
||||
local function searchmodule(ast,file,comment2apiobj,modulename)
|
||||
-- if the last statement is a return
|
||||
if ast then
|
||||
local laststatement = ast[#ast]
|
||||
if laststatement and laststatement.tag == "Return" then
|
||||
-- and if the first expression returned is an identifier.
|
||||
local firstexpr = laststatement[1]
|
||||
if firstexpr and firstexpr.tag == "Id" then
|
||||
-- get identifier in internal model
|
||||
local _identifier = idto_identifier [firstexpr]
|
||||
-- the definition should be an inline type
|
||||
if _identifier
|
||||
and _identifier.definition
|
||||
and _identifier.definition.type
|
||||
and _identifier.definition.type.tag == "inlinetyperef"
|
||||
and _identifier.definition.type.def.tag == "recordtypedef" then
|
||||
|
||||
--set modulename if needed
|
||||
if not file.name then file.name = modulename end
|
||||
|
||||
-- create or merge type
|
||||
local _type = _identifier.definition.type.def
|
||||
_type.name = modulename
|
||||
|
||||
-- if file (module) has no documentation add item documentation to it
|
||||
-- else add it to the type.
|
||||
if not file.description or file.description == "" then
|
||||
file.description = _identifier.definition.description
|
||||
else
|
||||
_type.description = _identifier.definition.description
|
||||
end
|
||||
_identifier.definition.description = ""
|
||||
if not file.shortdescription or file.shortdescription == "" then
|
||||
file.shortdescription = _identifier.definition.shortdescription
|
||||
else
|
||||
_type.shortdescription = _identifier.definition.shortdescription
|
||||
end
|
||||
_identifier.definition.shortdescription = ""
|
||||
|
||||
-- WORKAROUND FOR BUG 421622: [outline]module selection in outline does not select it in texteditor
|
||||
--_type.sourcerange.min = _identifier.definition.sourcerange.min
|
||||
--_type.sourcerange.max = _identifier.definition.sourcerange.max
|
||||
|
||||
-- merge the type with priority to documentation except for source range
|
||||
file:mergetype(_type,false,true)
|
||||
|
||||
-- create return if needed
|
||||
if not file.returns[1] then
|
||||
file.returns[1] = apimodel._return()
|
||||
file.returns[1].types = { apimodel._internaltyperef(modulename) }
|
||||
end
|
||||
|
||||
-- change the type of the identifier
|
||||
_identifier.definition.type = apimodel._internaltyperef(modulename)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------------
|
||||
-- create the internalcontent from an ast metalua
|
||||
-- ----------------------------------------------------------
|
||||
function M.createinternalcontent (ast,file,comment2apiobj,modulename)
|
||||
-- init cache
|
||||
idto_block = {}
|
||||
idto_identifier = {}
|
||||
expreto_expression = {}
|
||||
comment2apiobj = comment2apiobj or {}
|
||||
file = file or apimodel._file()
|
||||
|
||||
-- execute code safely to be sure to clean cache correctly
|
||||
local internalcontent
|
||||
local ok, errmsg = pcall(function ()
|
||||
-- add parent to all node
|
||||
addparents(ast)
|
||||
|
||||
-- create block and expression node
|
||||
internalcontent = createtreestructure(ast)
|
||||
|
||||
-- create Local vars, global vars and linked occurences (Items)
|
||||
createvardefinitions(internalcontent,ast,file,comment2apiobj)
|
||||
|
||||
-- try to dectect module information from code
|
||||
local moduletyperef = file:moduletyperef()
|
||||
if moduletyperef and moduletyperef.tag == "internaltyperef" then
|
||||
modulename = moduletyperef.typename or modulename
|
||||
end
|
||||
if modulename then
|
||||
searchmodule(ast,file,comment2apiobj,modulename)
|
||||
end
|
||||
end)
|
||||
|
||||
-- clean cache
|
||||
idto_block = {}
|
||||
idto_identifier = {}
|
||||
expreto_expression = {}
|
||||
|
||||
-- if not ok raise an error
|
||||
if not ok then error (errmsg) end
|
||||
|
||||
return internalcontent
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,656 @@
|
||||
-------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2011-2013 Sierra Wireless and others.
|
||||
-- All rights reserved. This program and the accompanying materials
|
||||
-- are made available under the terms of the Eclipse Public License v1.0
|
||||
-- which accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Sierra Wireless - initial API and implementation
|
||||
-------------------------------------------------------------------------------
|
||||
local mlc = require ('metalua.compiler').new()
|
||||
local gg = require 'metalua.grammar.generator'
|
||||
local lexer = require 'metalua.grammar.lexer'
|
||||
local mlp = mlc.parser
|
||||
|
||||
local M = {} -- module
|
||||
local lx -- lexer used to parse tag
|
||||
local registeredparsers -- table {tagname => {list de parsers}}
|
||||
|
||||
-- ----------------------------------------------------
|
||||
-- raise an error if result contains a node error
|
||||
-- ----------------------------------------------------
|
||||
local function raiserror(result)
|
||||
for i, node in ipairs(result) do
|
||||
assert(not node or node.tag ~= "Error")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- ----------------------------------------------------
|
||||
-- copy key and value from one table to an other
|
||||
-- ----------------------------------------------------
|
||||
local function copykey(tablefrom, tableto)
|
||||
for key, value in pairs(tablefrom) do
|
||||
if key ~= "lineinfos" then
|
||||
tableto[key] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------
|
||||
-- Handle keyword and identifiers as word
|
||||
-- ----------------------------------------------------
|
||||
local function parseword(lx)
|
||||
local word = lx :peek()
|
||||
local tag = word.tag
|
||||
|
||||
if tag=='Keyword' or tag=='Id' then
|
||||
lx:next()
|
||||
return {tag='Word', lineinfo=word.lineinfo, word[1]}
|
||||
else
|
||||
return gg.parse_error(lx,'Id or Keyword expected')
|
||||
end
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------
|
||||
-- parse an id
|
||||
-- return a table {name, lineinfo)
|
||||
-- ----------------------------------------------------
|
||||
local idparser = gg.sequence({
|
||||
builder = function (result)
|
||||
raiserror(result)
|
||||
return { name = result[1][1] }
|
||||
end,
|
||||
parseword
|
||||
})
|
||||
|
||||
-- ----------------------------------------------------
|
||||
-- parse a modulename (id.)?id
|
||||
-- return a table {name, lineinfo)
|
||||
-- ----------------------------------------------------
|
||||
local modulenameparser = gg.list({
|
||||
builder = function (result)
|
||||
raiserror(result)
|
||||
local ids = {}
|
||||
for i, id in ipairs(result) do
|
||||
table.insert(ids,id.name)
|
||||
end
|
||||
return {name = table.concat(ids,".")}
|
||||
end,
|
||||
primary = idparser,
|
||||
separators = '.'
|
||||
})
|
||||
-- ----------------------------------------------------
|
||||
-- parse a typename (id.)?id
|
||||
-- return a table {name, lineinfo)
|
||||
-- ----------------------------------------------------
|
||||
local typenameparser= modulenameparser
|
||||
|
||||
-- ----------------------------------------------------
|
||||
-- parse an internaltype ref
|
||||
-- return a table {name, lineinfo)
|
||||
-- ----------------------------------------------------
|
||||
local internaltyperefparser = gg.sequence({
|
||||
builder = function(result)
|
||||
raiserror(result)
|
||||
return {tag = "typeref",type=result[1].name}
|
||||
end,
|
||||
"#", typenameparser
|
||||
})
|
||||
|
||||
-- ----------------------------------------------------
|
||||
-- parse en external type ref
|
||||
-- return a table {name, lineinfo)
|
||||
-- ----------------------------------------------------
|
||||
local externaltyperefparser = gg.sequence({
|
||||
builder = function(result)
|
||||
raiserror(result)
|
||||
return {tag = "typeref",module=result[1].name,type=result[2].name}
|
||||
end,
|
||||
modulenameparser,"#", typenameparser
|
||||
})
|
||||
|
||||
|
||||
-- ----------------------------------------------------
|
||||
-- parse a typeref
|
||||
-- return a table {name, lineinfo)
|
||||
-- ----------------------------------------------------
|
||||
local typerefparser = gg.multisequence{
|
||||
internaltyperefparser,
|
||||
externaltyperefparser}
|
||||
|
||||
-- ----------------------------------------------------
|
||||
-- parse a list of typeref
|
||||
-- return a list of table {name, lineinfo)
|
||||
-- ----------------------------------------------------
|
||||
local typereflistparser = gg.list({
|
||||
primary = typerefparser,
|
||||
separators = ','
|
||||
})
|
||||
|
||||
-- ----------------------------------------------------
|
||||
-- TODO use a more generic way to parse (modifier if not always a typeref)
|
||||
-- TODO support more than one modifier
|
||||
-- ----------------------------------------------------
|
||||
local modifiersparser = gg.sequence({
|
||||
builder = function(result)
|
||||
raiserror(result)
|
||||
return {[result[1].name]=result[2]}
|
||||
end,
|
||||
"[", idparser , "=" , internaltyperefparser , "]"
|
||||
})
|
||||
|
||||
-- ----------------------------------------------------
|
||||
-- parse a return tag
|
||||
-- ----------------------------------------------------
|
||||
local returnparsers = {
|
||||
-- full parser
|
||||
gg.sequence({
|
||||
builder = function (result)
|
||||
raiserror(result)
|
||||
return { types= result[1]}
|
||||
end,
|
||||
'@','return', typereflistparser
|
||||
}),
|
||||
-- parser without typerefs
|
||||
gg.sequence({
|
||||
builder = function (result)
|
||||
raiserror(result)
|
||||
return { types = {}}
|
||||
end,
|
||||
'@','return'
|
||||
})
|
||||
}
|
||||
|
||||
-- ----------------------------------------------------
|
||||
-- parse a param tag
|
||||
-- ----------------------------------------------------
|
||||
local paramparsers = {
|
||||
-- full parser
|
||||
gg.sequence({
|
||||
builder = function (result)
|
||||
raiserror(result)
|
||||
return { name = result[2].name, type = result[1]}
|
||||
end,
|
||||
'@','param', typerefparser, idparser
|
||||
}),
|
||||
|
||||
-- full parser without type
|
||||
gg.sequence({
|
||||
builder = function (result)
|
||||
raiserror(result)
|
||||
return { name = result[1].name}
|
||||
end,
|
||||
'@','param', idparser
|
||||
}),
|
||||
|
||||
-- Parser for `Dots
|
||||
gg.sequence({
|
||||
builder = function (result)
|
||||
raiserror(result)
|
||||
return { name = '...' }
|
||||
end,
|
||||
'@','param', '...'
|
||||
}),
|
||||
}
|
||||
-- ----------------------------------------------------
|
||||
-- parse a field tag
|
||||
-- ----------------------------------------------------
|
||||
local fieldparsers = {
|
||||
-- full parser
|
||||
gg.sequence({
|
||||
builder = function (result)
|
||||
raiserror(result)
|
||||
local tag = {}
|
||||
copykey(result[1],tag)
|
||||
tag.type = result[2]
|
||||
tag.name = result[3].name
|
||||
return tag
|
||||
end,
|
||||
'@','field', modifiersparser, typerefparser, idparser
|
||||
}),
|
||||
|
||||
-- parser without name
|
||||
gg.sequence({
|
||||
builder = function (result)
|
||||
raiserror(result)
|
||||
local tag = {}
|
||||
copykey(result[1],tag)
|
||||
tag.type = result[2]
|
||||
return tag
|
||||
end,
|
||||
'@','field', modifiersparser, typerefparser
|
||||
}),
|
||||
|
||||
-- parser without type
|
||||
gg.sequence({
|
||||
builder = function (result)
|
||||
raiserror(result)
|
||||
local tag = {}
|
||||
copykey(result[1],tag)
|
||||
tag.name = result[2].name
|
||||
return tag
|
||||
end,
|
||||
'@','field', modifiersparser, idparser
|
||||
}),
|
||||
|
||||
-- parser without type and name
|
||||
gg.sequence({
|
||||
builder = function (result)
|
||||
raiserror(result)
|
||||
local tag = {}
|
||||
copykey(result[1],tag)
|
||||
return tag
|
||||
end,
|
||||
'@','field', modifiersparser
|
||||
}),
|
||||
|
||||
-- parser without modifiers
|
||||
gg.sequence({
|
||||
builder = function (result)
|
||||
raiserror(result)
|
||||
return { name = result[2].name, type = result[1]}
|
||||
end,
|
||||
'@','field', typerefparser, idparser
|
||||
}),
|
||||
|
||||
-- parser without modifiers and name
|
||||
gg.sequence({
|
||||
builder = function (result)
|
||||
raiserror(result)
|
||||
return {type = result[1]}
|
||||
end,
|
||||
'@','field', typerefparser
|
||||
}),
|
||||
|
||||
-- parser without type and modifiers
|
||||
gg.sequence({
|
||||
builder = function (result)
|
||||
raiserror(result)
|
||||
return { name = result[1].name}
|
||||
end,
|
||||
'@','field', idparser
|
||||
}),
|
||||
|
||||
-- parser with nothing
|
||||
gg.sequence({
|
||||
builder = function (result)
|
||||
raiserror(result)
|
||||
return {}
|
||||
end,
|
||||
'@','field'
|
||||
})
|
||||
}
|
||||
|
||||
-- ----------------------------------------------------
|
||||
-- parse a function tag
|
||||
-- TODO use a more generic way to parse modifier !
|
||||
-- ----------------------------------------------------
|
||||
local functionparsers = {
|
||||
-- full parser
|
||||
gg.sequence({
|
||||
builder = function (result)
|
||||
raiserror(result)
|
||||
local tag = {}
|
||||
copykey(result[1],tag)
|
||||
tag.name = result[2].name
|
||||
return tag
|
||||
end,
|
||||
'@','function', modifiersparser, idparser
|
||||
}),
|
||||
|
||||
-- parser without name
|
||||
gg.sequence({
|
||||
builder = function (result)
|
||||
raiserror(result)
|
||||
local tag = {}
|
||||
copykey(result[1],tag)
|
||||
return tag
|
||||
end,
|
||||
'@','function', modifiersparser
|
||||
}),
|
||||
|
||||
-- parser without modifier
|
||||
gg.sequence({
|
||||
builder = function (result)
|
||||
raiserror(result)
|
||||
local tag = {}
|
||||
tag.name = result[1].name
|
||||
return tag
|
||||
end,
|
||||
'@','function', idparser
|
||||
}),
|
||||
|
||||
-- empty parser
|
||||
gg.sequence({
|
||||
builder = function (result)
|
||||
raiserror(result)
|
||||
return {}
|
||||
end,
|
||||
'@','function'
|
||||
})
|
||||
}
|
||||
|
||||
-- ----------------------------------------------------
|
||||
-- parse a type tag
|
||||
-- ----------------------------------------------------
|
||||
local typeparsers = {
|
||||
-- full parser
|
||||
gg.sequence({
|
||||
builder = function (result)
|
||||
raiserror(result)
|
||||
return { name = result[1].name}
|
||||
end,
|
||||
'@','type',typenameparser
|
||||
}),
|
||||
-- parser without name
|
||||
gg.sequence({
|
||||
builder = function (result)
|
||||
raiserror(result)
|
||||
return {}
|
||||
end,
|
||||
'@','type'
|
||||
})
|
||||
}
|
||||
|
||||
-- ----------------------------------------------------
|
||||
-- parse a module tag
|
||||
-- ----------------------------------------------------
|
||||
local moduleparsers = {
|
||||
-- full parser
|
||||
gg.sequence({
|
||||
builder = function (result)
|
||||
raiserror(result)
|
||||
return { name = result[1].name }
|
||||
end,
|
||||
'@','module', modulenameparser
|
||||
}),
|
||||
-- parser without name
|
||||
gg.sequence({
|
||||
builder = function (result)
|
||||
raiserror(result)
|
||||
return {}
|
||||
end,
|
||||
'@','module'
|
||||
})
|
||||
}
|
||||
|
||||
-- ----------------------------------------------------
|
||||
-- parse a third tag
|
||||
-- ----------------------------------------------------
|
||||
local thirdtagsparser = gg.sequence({
|
||||
builder = function (result)
|
||||
raiserror(result)
|
||||
return { name = result[1][1] }
|
||||
end,
|
||||
'@', mlp.id
|
||||
})
|
||||
-- ----------------------------------------------------
|
||||
-- init parser
|
||||
-- ----------------------------------------------------
|
||||
local function initparser()
|
||||
-- register parsers
|
||||
-- each tag name has several parsers
|
||||
registeredparsers = {
|
||||
["module"] = moduleparsers,
|
||||
["return"] = returnparsers,
|
||||
["type"] = typeparsers,
|
||||
["field"] = fieldparsers,
|
||||
["function"] = functionparsers,
|
||||
["param"] = paramparsers
|
||||
}
|
||||
|
||||
-- create lexer used for parsing
|
||||
lx = lexer.lexer:clone()
|
||||
lx.extractors = {
|
||||
-- "extract_long_comment",
|
||||
-- "extract_short_comment",
|
||||
-- "extract_long_string",
|
||||
"extract_short_string",
|
||||
"extract_word",
|
||||
"extract_number",
|
||||
"extract_symbol"
|
||||
}
|
||||
|
||||
-- Add dots as keyword
|
||||
local tagnames = { '...' }
|
||||
|
||||
-- Add tag names as key word
|
||||
for tagname, _ in pairs(registeredparsers) do
|
||||
table.insert(tagnames,tagname)
|
||||
end
|
||||
lx:add(tagnames)
|
||||
|
||||
return lx, parsers
|
||||
end
|
||||
|
||||
initparser()
|
||||
|
||||
-- ----------------------------------------------------
|
||||
-- get the string pattern to remove for each line of description
|
||||
-- the goal is to fix the indentation problems
|
||||
-- ----------------------------------------------------
|
||||
local function getstringtoremove (stringcomment,commentstart)
|
||||
local _,_,capture = string.find(stringcomment,"\n?([ \t]*)@[^{]+",commentstart)
|
||||
if not capture then
|
||||
_,_,capture = string.find(stringcomment,"^([ \t]*)",commentstart)
|
||||
end
|
||||
capture = string.gsub(capture,"(.)","%1?")
|
||||
return capture
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------
|
||||
-- parse comment tag partition and return table structure
|
||||
-- ----------------------------------------------------
|
||||
local function parsetag(part)
|
||||
if part.comment:find("^@") then
|
||||
-- check if the part start by a supported tag
|
||||
for tagname,parsers in pairs(registeredparsers) do
|
||||
if (part.comment:find("^@"..tagname)) then
|
||||
-- try the registered parsers for this tag
|
||||
local result
|
||||
for i, parser in ipairs(parsers) do
|
||||
local valid, tag = pcall(parser, lx:newstream(part.comment, tagname .. 'tag lexer'))
|
||||
if valid then
|
||||
-- add tagname
|
||||
tag.tagname = tagname
|
||||
|
||||
-- add description
|
||||
local endoffset = tag.lineinfo.last.offset
|
||||
tag.description = part.comment:sub(endoffset+2,-1)
|
||||
return tag
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------
|
||||
-- Parse third party tags.
|
||||
--
|
||||
-- Enable to parse a tag not defined in language.
|
||||
-- So for, accepted format is: @sometagname adescription
|
||||
-- ----------------------------------------------------
|
||||
local function parsethirdtag( part )
|
||||
|
||||
-- Check it there is someting to process
|
||||
if not part.comment:find("^@") then
|
||||
return nil, 'No tag to parse'
|
||||
end
|
||||
|
||||
-- Apply parser
|
||||
local status, parsedtag = pcall(thirdtagsparser, lx:newstream(part.comment, 'Third party tag lexer'))
|
||||
if not status then
|
||||
return nil, "Unable to parse given string."
|
||||
end
|
||||
|
||||
-- Retrieve description
|
||||
local endoffset = parsedtag.lineinfo.last.offset
|
||||
local tag = {
|
||||
description = part.comment:sub(endoffset+2,-1)
|
||||
}
|
||||
return parsedtag.name, tag
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------
|
||||
-- split string comment in several part
|
||||
-- return list of {comment = string, offset = number}
|
||||
-- the first part is the part before the first tag
|
||||
-- the others are the part from a tag to the next one
|
||||
-- ----------------------------------------------------
|
||||
local function split(stringcomment,commentstart)
|
||||
local partstart = commentstart
|
||||
local result = {}
|
||||
|
||||
-- manage case where the comment start by @
|
||||
-- (we must ignore the inline see tag @{..})
|
||||
local at_startoffset, at_endoffset = stringcomment:find("^[ \t]*@[^{]",partstart)
|
||||
if at_endoffset then
|
||||
partstart = at_endoffset-1 -- we start before the @ and the non '{' character
|
||||
end
|
||||
|
||||
-- split comment
|
||||
-- (we must ignore the inline see tag @{..})
|
||||
repeat
|
||||
at_startoffset, at_endoffset = stringcomment:find("\n[ \t]*@[^{]",partstart)
|
||||
local partend
|
||||
if at_startoffset then
|
||||
partend= at_startoffset-1 -- the end is before the separator pattern (just before the \n)
|
||||
else
|
||||
partend = #stringcomment -- we don't find any pattern so the end is the end of the string
|
||||
end
|
||||
table.insert(result, { comment = stringcomment:sub (partstart,partend) ,
|
||||
offset = partstart})
|
||||
if at_endoffset then
|
||||
partstart = at_endoffset-1 -- the new start is befire the @ and the non { char
|
||||
end
|
||||
until not at_endoffset
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
-- ----------------------------------------------------
|
||||
-- parse a comment block and return a table
|
||||
-- ----------------------------------------------------
|
||||
function M.parse(stringcomment)
|
||||
|
||||
local _comment = {description="", shortdescription=""}
|
||||
|
||||
-- clean windows carriage return
|
||||
stringcomment = string.gsub(stringcomment,"\r\n","\n")
|
||||
|
||||
-- check if it's a ld comment
|
||||
-- get the begin of the comment
|
||||
-- ============================
|
||||
if not stringcomment:find("^-") then
|
||||
-- if this comment don't start by -, we will not handle it.
|
||||
return nil
|
||||
end
|
||||
|
||||
-- retrieve the real start
|
||||
local commentstart = 2 --after the first hyphen
|
||||
-- if the first line is an empty comment line with at least 3 hyphens we ignore it
|
||||
local _ , endoffset = stringcomment:find("^-+[ \t]*\n")
|
||||
if endoffset then
|
||||
commentstart = endoffset+1
|
||||
end
|
||||
|
||||
-- clean comments
|
||||
-- ===================
|
||||
-- remove line of "-"
|
||||
stringcomment = string.sub(stringcomment,commentstart)
|
||||
-- clean indentation
|
||||
local pattern = getstringtoremove (stringcomment,1)
|
||||
stringcomment = string.gsub(stringcomment,"^"..pattern,"")
|
||||
stringcomment = string.gsub(stringcomment,"\n"..pattern,"\n")
|
||||
|
||||
-- split comment part
|
||||
-- ====================
|
||||
local commentparts = split(stringcomment, 1)
|
||||
|
||||
-- Extract descriptions
|
||||
-- ====================
|
||||
local firstpart = commentparts[1].comment
|
||||
if firstpart:find("^[^@]") or firstpart:find("^@{") then
|
||||
-- if the comment part don't start by @
|
||||
-- it's the part which contains descriptions
|
||||
-- (there are an exception for the in-line see tag @{..})
|
||||
local shortdescription, description = string.match(firstpart,'^(.-[.?])(%s.+)')
|
||||
-- store description
|
||||
if shortdescription then
|
||||
_comment.shortdescription = shortdescription
|
||||
-- clean description
|
||||
-- remove always the first space character
|
||||
-- (this manage the case short and long description is on the same line)
|
||||
description = string.gsub(description, "^[ \t]","")
|
||||
-- if first line is only an empty string remove it
|
||||
description = string.gsub(description, "^[ \t]*\n","")
|
||||
_comment.description = description
|
||||
else
|
||||
_comment.shortdescription = firstpart
|
||||
_comment.description = ""
|
||||
end
|
||||
end
|
||||
|
||||
-- Extract tags
|
||||
-- ===================
|
||||
-- Parse regular tags
|
||||
local tag
|
||||
for i, part in ipairs(commentparts) do
|
||||
tag = parsetag(part)
|
||||
--if it's a supported tag (so tag is not nil, it's a table)
|
||||
if tag then
|
||||
if not _comment.tags then _comment.tags = {} end
|
||||
if not _comment.tags[tag.tagname] then
|
||||
_comment.tags[tag.tagname] = {}
|
||||
end
|
||||
table.insert(_comment.tags[tag.tagname], tag)
|
||||
else
|
||||
|
||||
-- Try user defined tags, so far they will look like
|
||||
-- @identifier description
|
||||
local tagname, thirdtag = parsethirdtag( part )
|
||||
if tagname then
|
||||
--
|
||||
-- Append found tag
|
||||
--
|
||||
local reservedname = 'unknowntags'
|
||||
if not _comment.unknowntags then
|
||||
_comment.unknowntags = {}
|
||||
end
|
||||
|
||||
-- Create specific section for parsed tag
|
||||
if not _comment.unknowntags[tagname] then
|
||||
_comment.unknowntags[tagname] = {}
|
||||
end
|
||||
-- Append to specific section
|
||||
table.insert(_comment.unknowntags[tagname], thirdtag)
|
||||
end
|
||||
end
|
||||
end
|
||||
return _comment
|
||||
end
|
||||
|
||||
|
||||
function M.parseinlinecomment(stringcomment)
|
||||
--TODO this code is use to activate typage only on --- comments. (deactivate for now)
|
||||
-- if not stringcomment or not stringcomment:find("^-") then
|
||||
-- -- if this comment don't start by -, we will not handle it.
|
||||
-- return nil
|
||||
-- end
|
||||
-- -- remove the first '-'
|
||||
-- stringcomment = string.sub(stringcomment,2)
|
||||
-- print (stringcomment)
|
||||
-- io.flush()
|
||||
local valid, parsedtag = pcall(typerefparser, lx:newstream(stringcomment, 'typeref parser'))
|
||||
if valid then
|
||||
local endoffset = parsedtag.lineinfo.last.offset
|
||||
parsedtag.description = stringcomment:sub(endoffset+2,-1)
|
||||
return parsedtag
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,546 @@
|
||||
--- Date and Date Format classes.
|
||||
-- See <a href="../../index.html#date">the Guide</a>.
|
||||
-- @class module
|
||||
-- @name pl.Date
|
||||
-- @pragma nostrip
|
||||
|
||||
--[[
|
||||
module("pl.Date")
|
||||
]]
|
||||
|
||||
local class = require 'pl.class'
|
||||
local os_time, os_date = os.time, os.date
|
||||
local stringx = require 'pl.stringx'
|
||||
|
||||
local Date = class()
|
||||
Date.Format = class()
|
||||
|
||||
--- Date constructor.
|
||||
-- @param t this can be either <ul>
|
||||
-- <li>nil - use current date and time</li>
|
||||
-- <li>number - seconds since epoch (as returned by @{os.time})</li>
|
||||
-- <li>Date - copy constructor</li>
|
||||
-- <li>table - table containing year, month, etc as for os.time()
|
||||
-- You may leave out year, month or day, in which case current values will be used.
|
||||
-- </li>
|
||||
-- <li> two to six numbers: year, month, day, hour, min, sec
|
||||
-- </ul>
|
||||
-- @function Date
|
||||
function Date:_init(t,...)
|
||||
local time
|
||||
if select('#',...) > 0 then
|
||||
local extra = {...}
|
||||
local year = t
|
||||
t = {
|
||||
year = year,
|
||||
month = extra[1],
|
||||
day = extra[2],
|
||||
hour = extra[3],
|
||||
min = extra[4],
|
||||
sec = extra[5]
|
||||
}
|
||||
end
|
||||
if t == nil then
|
||||
time = os_time()
|
||||
elseif type(t) == 'number' then
|
||||
time = t
|
||||
elseif type(t) == 'table' then
|
||||
if getmetatable(t) == Date then -- copy ctor
|
||||
time = t.time
|
||||
else
|
||||
if not (t.year and t.month and t.year) then
|
||||
local lt = os.date('*t')
|
||||
if not t.year and not t.month and not t.day then
|
||||
t.year = lt.year
|
||||
t.month = lt.month
|
||||
t.day = lt.day
|
||||
else
|
||||
t.year = t.year or lt.year
|
||||
t.month = t.month or (t.day and lt.month or 1)
|
||||
t.day = t.day or 1
|
||||
end
|
||||
end
|
||||
time = os_time(t)
|
||||
end
|
||||
end
|
||||
self:set(time)
|
||||
end
|
||||
|
||||
local thour,tmin
|
||||
|
||||
--- get the time zone offset from UTC.
|
||||
-- @return hours ahead of UTC
|
||||
-- @return minutes ahead of UTC
|
||||
function Date.tzone ()
|
||||
if not thour then
|
||||
local t = os.time()
|
||||
local ut = os.date('!*t',t)
|
||||
local lt = os.date('*t',t)
|
||||
thour = lt.hour - ut.hour
|
||||
tmin = lt.min - ut.min
|
||||
end
|
||||
return thour, tmin
|
||||
end
|
||||
|
||||
--- convert this date to UTC.
|
||||
function Date:toUTC ()
|
||||
local th, tm = Date.tzone()
|
||||
self:add { hour = -th }
|
||||
|
||||
if tm > 0 then self:add {min = -tm} end
|
||||
end
|
||||
|
||||
--- convert this UTC date to local.
|
||||
function Date:toLocal ()
|
||||
local th, tm = Date.tzone()
|
||||
self:add { hour = th }
|
||||
if tm > 0 then self:add {min = tm} end
|
||||
end
|
||||
|
||||
--- set the current time of this Date object.
|
||||
-- @param t seconds since epoch
|
||||
function Date:set(t)
|
||||
self.time = t
|
||||
self.tab = os_date('*t',self.time)
|
||||
end
|
||||
|
||||
--- set the year.
|
||||
-- @param y Four-digit year
|
||||
-- @class function
|
||||
-- @name Date:year
|
||||
|
||||
--- set the month.
|
||||
-- @param m month
|
||||
-- @class function
|
||||
-- @name Date:month
|
||||
|
||||
--- set the day.
|
||||
-- @param d day
|
||||
-- @class function
|
||||
-- @name Date:day
|
||||
|
||||
--- set the hour.
|
||||
-- @param h hour
|
||||
-- @class function
|
||||
-- @name Date:hour
|
||||
|
||||
--- set the minutes.
|
||||
-- @param min minutes
|
||||
-- @class function
|
||||
-- @name Date:min
|
||||
|
||||
--- set the seconds.
|
||||
-- @param sec seconds
|
||||
-- @class function
|
||||
-- @name Date:sec
|
||||
|
||||
--- set the day of year.
|
||||
-- @class function
|
||||
-- @param yday day of year
|
||||
-- @name Date:yday
|
||||
|
||||
--- get the year.
|
||||
-- @param y Four-digit year
|
||||
-- @class function
|
||||
-- @name Date:year
|
||||
|
||||
--- get the month.
|
||||
-- @class function
|
||||
-- @name Date:month
|
||||
|
||||
--- get the day.
|
||||
-- @class function
|
||||
-- @name Date:day
|
||||
|
||||
--- get the hour.
|
||||
-- @class function
|
||||
-- @name Date:hour
|
||||
|
||||
--- get the minutes.
|
||||
-- @class function
|
||||
-- @name Date:min
|
||||
|
||||
--- get the seconds.
|
||||
-- @class function
|
||||
-- @name Date:sec
|
||||
|
||||
--- get the day of year.
|
||||
-- @class function
|
||||
-- @name Date:yday
|
||||
|
||||
|
||||
for _,c in ipairs{'year','month','day','hour','min','sec','yday'} do
|
||||
Date[c] = function(self,val)
|
||||
if val then
|
||||
self.tab[c] = val
|
||||
self:set(os_time(self.tab))
|
||||
return self
|
||||
else
|
||||
return self.tab[c]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- name of day of week.
|
||||
-- @param full abbreviated if true, full otherwise.
|
||||
-- @return string name
|
||||
function Date:weekday_name(full)
|
||||
return os_date(full and '%A' or '%a',self.time)
|
||||
end
|
||||
|
||||
--- name of month.
|
||||
-- @param full abbreviated if true, full otherwise.
|
||||
-- @return string name
|
||||
function Date:month_name(full)
|
||||
return os_date(full and '%B' or '%b',self.time)
|
||||
end
|
||||
|
||||
--- is this day on a weekend?.
|
||||
function Date:is_weekend()
|
||||
return self.tab.wday == 0 or self.tab.wday == 6
|
||||
end
|
||||
|
||||
--- add to a date object.
|
||||
-- @param t a table containing one of the following keys and a value:<br>
|
||||
-- year,month,day,hour,min,sec
|
||||
-- @return this date
|
||||
function Date:add(t)
|
||||
local key,val = next(t)
|
||||
self.tab[key] = self.tab[key] + val
|
||||
self:set(os_time(self.tab))
|
||||
return self
|
||||
end
|
||||
|
||||
--- last day of the month.
|
||||
-- @return int day
|
||||
function Date:last_day()
|
||||
local d = 28
|
||||
local m = self.tab.month
|
||||
while self.tab.month == m do
|
||||
d = d + 1
|
||||
self:add{day=1}
|
||||
end
|
||||
self:add{day=-1}
|
||||
return self
|
||||
end
|
||||
|
||||
--- difference between two Date objects.
|
||||
-- Note: currently the result is a regular @{Date} object,
|
||||
-- but also has `interval` field set, which means a more
|
||||
-- appropriate string rep is used.
|
||||
-- @param other Date object
|
||||
-- @return a Date object
|
||||
function Date:diff(other)
|
||||
local dt = self.time - other.time
|
||||
if dt < 0 then error("date difference is negative!",2) end
|
||||
local date = Date(dt)
|
||||
date.interval = true
|
||||
return date
|
||||
end
|
||||
|
||||
--- long numerical ISO data format version of this date.
|
||||
function Date:__tostring()
|
||||
if not self.interval then
|
||||
return os_date('%Y-%m-%d %H:%M:%S',self.time)
|
||||
else
|
||||
local t, res = self.tab, ''
|
||||
local y,m,d = t.year - 1970, t.month - 1, t.day - 1
|
||||
if y > 0 then res = res .. y .. ' years ' end
|
||||
if m > 0 then res = res .. m .. ' months ' end
|
||||
if d > 0 then res = res .. d .. ' days ' end
|
||||
if y == 0 and m == 0 then
|
||||
local h = t.hour - Date.tzone() -- not accounting for UTC mins!
|
||||
if h > 0 then res = res .. h .. ' hours ' end
|
||||
if t.min > 0 then res = res .. t.min .. ' min ' end
|
||||
if t.sec > 0 then res = res .. t.sec .. ' sec ' end
|
||||
end
|
||||
return res
|
||||
end
|
||||
end
|
||||
|
||||
--- equality between Date objects.
|
||||
function Date:__eq(other)
|
||||
return self.time == other.time
|
||||
end
|
||||
|
||||
--- equality between Date objects.
|
||||
function Date:__lt(other)
|
||||
return self.time < other.time
|
||||
end
|
||||
|
||||
|
||||
------------ Date.Format class: parsing and renderinig dates ------------
|
||||
|
||||
-- short field names, explicit os.date names, and a mask for allowed field repeats
|
||||
local formats = {
|
||||
d = {'day',{true,true}},
|
||||
y = {'year',{false,true,false,true}},
|
||||
m = {'month',{true,true}},
|
||||
H = {'hour',{true,true}},
|
||||
M = {'min',{true,true}},
|
||||
S = {'sec',{true,true}},
|
||||
}
|
||||
|
||||
--
|
||||
|
||||
--- Date.Format constructor.
|
||||
-- @param fmt. A string where the following fields are significant: <ul>
|
||||
-- <li>d day (either d or dd)</li>
|
||||
-- <li>y year (either yy or yyy)</li>
|
||||
-- <li>m month (either m or mm)</li>
|
||||
-- <li>H hour (either H or HH)</li>
|
||||
-- <li>M minute (either M or MM)</li>
|
||||
-- <li>S second (either S or SS)</li>
|
||||
-- </ul>
|
||||
-- Alternatively, if fmt is nil then this returns a flexible date parser
|
||||
-- that tries various date/time schemes in turn:
|
||||
-- <ol>
|
||||
-- <li> <a href="http://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a>,
|
||||
-- like 2010-05-10 12:35:23Z or 2008-10-03T14:30+02<li>
|
||||
-- <li> times like 15:30 or 8.05pm (assumed to be today's date)</li>
|
||||
-- <li> dates like 28/10/02 (European order!) or 5 Feb 2012 </li>
|
||||
-- <li> month name like march or Mar (case-insensitive, first 3 letters);
|
||||
-- here the day will be 1 and the year this current year </li>
|
||||
-- </ol>
|
||||
-- A date in format 3 can be optionally followed by a time in format 2.
|
||||
-- Please see test-date.lua in the tests folder for more examples.
|
||||
-- @usage df = Date.Format("yyyy-mm-dd HH:MM:SS")
|
||||
-- @class function
|
||||
-- @name Date.Format
|
||||
function Date.Format:_init(fmt)
|
||||
if not fmt then return end
|
||||
local append = table.insert
|
||||
local D,PLUS,OPENP,CLOSEP = '\001','\002','\003','\004'
|
||||
local vars,used = {},{}
|
||||
local patt,outf = {},{}
|
||||
local i = 1
|
||||
while i < #fmt do
|
||||
local ch = fmt:sub(i,i)
|
||||
local df = formats[ch]
|
||||
if df then
|
||||
if used[ch] then error("field appeared twice: "..ch,2) end
|
||||
used[ch] = true
|
||||
-- this field may be repeated
|
||||
local _,inext = fmt:find(ch..'+',i+1)
|
||||
local cnt = not _ and 1 or inext-i+1
|
||||
if not df[2][cnt] then error("wrong number of fields: "..ch,2) end
|
||||
-- single chars mean 'accept more than one digit'
|
||||
local p = cnt==1 and (D..PLUS) or (D):rep(cnt)
|
||||
append(patt,OPENP..p..CLOSEP)
|
||||
append(vars,ch)
|
||||
if ch == 'y' then
|
||||
append(outf,cnt==2 and '%y' or '%Y')
|
||||
else
|
||||
append(outf,'%'..ch)
|
||||
end
|
||||
i = i + cnt
|
||||
else
|
||||
append(patt,ch)
|
||||
append(outf,ch)
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
-- escape any magic characters
|
||||
fmt = table.concat(patt):gsub('[%-%.%+%[%]%(%)%$%^%%%?%*]','%%%1')
|
||||
-- replace markers with their magic equivalents
|
||||
fmt = fmt:gsub(D,'%%d'):gsub(PLUS,'+'):gsub(OPENP,'('):gsub(CLOSEP,')')
|
||||
self.fmt = fmt
|
||||
self.outf = table.concat(outf)
|
||||
self.vars = vars
|
||||
|
||||
end
|
||||
|
||||
local parse_date
|
||||
|
||||
--- parse a string into a Date object.
|
||||
-- @param str a date string
|
||||
-- @return date object
|
||||
function Date.Format:parse(str)
|
||||
if not self.fmt then
|
||||
return parse_date(str,self.us)
|
||||
end
|
||||
local res = {str:match(self.fmt)}
|
||||
if #res==0 then return nil, 'cannot parse '..str end
|
||||
local tab = {}
|
||||
for i,v in ipairs(self.vars) do
|
||||
local name = formats[v][1] -- e.g. 'y' becomes 'year'
|
||||
tab[name] = tonumber(res[i])
|
||||
end
|
||||
-- os.date() requires these fields; if not present, we assume
|
||||
-- that the time set is for the current day.
|
||||
if not (tab.year and tab.month and tab.year) then
|
||||
local today = Date()
|
||||
tab.year = tab.year or today:year()
|
||||
tab.month = tab.month or today:month()
|
||||
tab.day = tab.day or today:month()
|
||||
end
|
||||
local Y = tab.year
|
||||
if Y < 100 then -- classic Y2K pivot
|
||||
tab.year = Y + (Y < 35 and 2000 or 1999)
|
||||
elseif not Y then
|
||||
tab.year = 1970
|
||||
end
|
||||
--dump(tab)
|
||||
return Date(tab)
|
||||
end
|
||||
|
||||
--- convert a Date object into a string.
|
||||
-- @param d a date object, or a time value as returned by @{os.time}
|
||||
-- @return string
|
||||
function Date.Format:tostring(d)
|
||||
local tm = type(d) == 'number' and d or d.time
|
||||
if self.outf then
|
||||
return os.date(self.outf,tm)
|
||||
else
|
||||
return tostring(Date(d))
|
||||
end
|
||||
end
|
||||
|
||||
function Date.Format:US_order(yesno)
|
||||
self.us = yesno
|
||||
end
|
||||
|
||||
local months = {jan=1,feb=2,mar=3,apr=4,may=5,jun=6,jul=7,aug=8,sep=9,oct=10,nov=11,dec=12}
|
||||
|
||||
--[[
|
||||
Allowed patterns:
|
||||
- [day] [monthname] [year] [time]
|
||||
- [day]/[month][/year] [time]
|
||||
|
||||
]]
|
||||
|
||||
|
||||
local is_word = stringx.isalpha
|
||||
local is_number = stringx.isdigit
|
||||
local function tonum(s,l1,l2,kind)
|
||||
kind = kind or ''
|
||||
local n = tonumber(s)
|
||||
if not n then error(("%snot a number: '%s'"):format(kind,s)) end
|
||||
if n < l1 or n > l2 then
|
||||
error(("%s out of range: %s is not between %d and %d"):format(kind,s,l1,l2))
|
||||
end
|
||||
return n
|
||||
end
|
||||
|
||||
local function parse_iso_end(p,ns,sec)
|
||||
-- may be fractional part of seconds
|
||||
local _,nfrac,secfrac = p:find('^%.%d+',ns+1)
|
||||
if secfrac then
|
||||
sec = sec .. secfrac
|
||||
p = p:sub(nfrac+1)
|
||||
else
|
||||
p = p:sub(ns+1)
|
||||
end
|
||||
-- ISO 8601 dates may end in Z (for UTC) or [+-][isotime]
|
||||
if p:match 'z$' then return sec, {h=0,m=0} end -- we're UTC!
|
||||
p = p:gsub(':','') -- turn 00:30 to 0030
|
||||
local _,_,sign,offs = p:find('^([%+%-])(%d+)')
|
||||
if not sign then return sec, nil end -- not UTC
|
||||
|
||||
if #offs == 2 then offs = offs .. '00' end -- 01 to 0100
|
||||
local tz = { h = tonumber(offs:sub(1,2)), m = tonumber(offs:sub(3,4)) }
|
||||
if sign == '-' then tz.h = -tz.h; tz.m = -tz.m end
|
||||
return sec, tz
|
||||
end
|
||||
|
||||
local function parse_date_unsafe (s,US)
|
||||
s = s:gsub('T',' ') -- ISO 8601
|
||||
local parts = stringx.split(s:lower())
|
||||
local i,p = 1,parts[1]
|
||||
local function nextp() i = i + 1; p = parts[i] end
|
||||
local year,min,hour,sec,apm
|
||||
local tz
|
||||
local _,nxt,day, month = p:find '^(%d+)/(%d+)'
|
||||
if day then
|
||||
-- swop for US case
|
||||
if US then
|
||||
day, month = month, day
|
||||
end
|
||||
_,_,year = p:find('^/(%d+)',nxt+1)
|
||||
nextp()
|
||||
else -- ISO
|
||||
year,month,day = p:match('^(%d+)%-(%d+)%-(%d+)')
|
||||
if year then
|
||||
nextp()
|
||||
end
|
||||
end
|
||||
if p and not year and is_number(p) then -- has to be date
|
||||
day = p
|
||||
nextp()
|
||||
end
|
||||
if p and is_word(p) then
|
||||
p = p:sub(1,3)
|
||||
local mon = months[p]
|
||||
if mon then
|
||||
month = mon
|
||||
else error("not a month: " .. p) end
|
||||
nextp()
|
||||
end
|
||||
if p and not year and is_number(p) then
|
||||
year = p
|
||||
nextp()
|
||||
end
|
||||
|
||||
if p then -- time is hh:mm[:ss], hhmm[ss] or H.M[am|pm]
|
||||
_,nxt,hour,min = p:find '^(%d+):(%d+)'
|
||||
local ns
|
||||
if nxt then -- are there seconds?
|
||||
_,ns,sec = p:find ('^:(%d+)',nxt+1)
|
||||
--if ns then
|
||||
sec,tz = parse_iso_end(p,ns or nxt,sec)
|
||||
--end
|
||||
else -- might be h.m
|
||||
_,ns,hour,min = p:find '^(%d+)%.(%d+)'
|
||||
if ns then
|
||||
apm = p:match '[ap]m$'
|
||||
else -- or hhmm[ss]
|
||||
local hourmin
|
||||
_,nxt,hourmin = p:find ('^(%d+)')
|
||||
if nxt then
|
||||
hour = hourmin:sub(1,2)
|
||||
min = hourmin:sub(3,4)
|
||||
sec = hourmin:sub(5,6)
|
||||
if #sec == 0 then sec = nil end
|
||||
sec,tz = parse_iso_end(p,nxt,sec)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local today
|
||||
if not (year and month and day) then
|
||||
today = Date()
|
||||
end
|
||||
day = day and tonum(day,1,31,'day') or (month and 1 or today:day())
|
||||
month = month and tonum(month,1,12,'month') or today:month()
|
||||
year = year and tonumber(year) or today:year()
|
||||
if year < 100 then -- two-digit year pivot
|
||||
year = year + (year < 35 and 2000 or 1900)
|
||||
end
|
||||
hour = hour and tonum(hour,1,apm and 12 or 24,'hour') or 12
|
||||
if apm == 'pm' then
|
||||
hour = hour + 12
|
||||
end
|
||||
min = min and tonum(min,1,60) or 0
|
||||
sec = sec and tonum(sec,1,60) or 0
|
||||
local res = Date {year = year, month = month, day = day, hour = hour, min = min, sec = sec}
|
||||
if tz then -- ISO 8601 UTC time
|
||||
res:toUTC()
|
||||
res:add {hour = tz.h}
|
||||
if tz.m ~= 0 then res:add {min = tz.m} end
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
function parse_date (s)
|
||||
local ok, d = pcall(parse_date_unsafe,s)
|
||||
if not ok then -- error
|
||||
d = d:gsub('.-:%d+: ','')
|
||||
return nil, d
|
||||
else
|
||||
return d
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
return Date
|
||||
|
||||
@@ -0,0 +1,553 @@
|
||||
--- Python-style list class. <p>
|
||||
-- Based on original code by Nick Trout.
|
||||
-- <p>
|
||||
-- <b>Please Note</b>: methods that change the list will return the list.
|
||||
-- This is to allow for method chaining, but please note that <tt>ls = ls:sort()</tt>
|
||||
-- does not mean that a new copy of the list is made. In-place (mutable) methods
|
||||
-- are marked as returning 'the list' in this documentation.
|
||||
-- <p>
|
||||
-- See the Guide for further <a href="../../index.html#list">discussion</a>
|
||||
-- <p>
|
||||
-- See <a href="http://www.python.org/doc/current/tut/tut.html">http://www.python.org/doc/current/tut/tut.html</a>, section 5.1
|
||||
-- <p>
|
||||
-- <b>Note</b>: The comments before some of the functions are from the Python docs
|
||||
-- and contain Python code.
|
||||
-- <p>
|
||||
-- Written for Lua version 4.0 <br />
|
||||
-- Redone for Lua 5.1, Steve Donovan.
|
||||
-- @class module
|
||||
-- @name pl.List
|
||||
-- @pragma nostrip
|
||||
|
||||
local tinsert,tremove,concat,tsort = table.insert,table.remove,table.concat,table.sort
|
||||
local setmetatable, getmetatable,type,tostring,assert,string,next = setmetatable,getmetatable,type,tostring,assert,string,next
|
||||
local write = io.write
|
||||
local tablex = require 'pl.tablex'
|
||||
local filter,imap,imap2,reduce,transform,tremovevalues = tablex.filter,tablex.imap,tablex.imap2,tablex.reduce,tablex.transform,tablex.removevalues
|
||||
local tablex = tablex
|
||||
local tsub = tablex.sub
|
||||
local utils = require 'pl.utils'
|
||||
local function_arg = utils.function_arg
|
||||
local is_type = utils.is_type
|
||||
local split = utils.split
|
||||
local assert_arg = utils.assert_arg
|
||||
local normalize_slice = tablex._normalize_slice
|
||||
|
||||
--[[
|
||||
module ('pl.List',utils._module)
|
||||
]]
|
||||
|
||||
local Multimap = utils.stdmt.MultiMap
|
||||
-- metatable for our list objects
|
||||
local List = utils.stdmt.List
|
||||
List.__index = List
|
||||
List._class = List
|
||||
|
||||
local iter
|
||||
|
||||
-- we give the metatable its own metatable so that we can call it like a function!
|
||||
setmetatable(List,{
|
||||
__call = function (tbl,arg)
|
||||
return List.new(arg)
|
||||
end,
|
||||
})
|
||||
|
||||
local function makelist (t,obj)
|
||||
local klass = List
|
||||
if obj then
|
||||
klass = getmetatable(obj)
|
||||
end
|
||||
return setmetatable(t,klass)
|
||||
end
|
||||
|
||||
local function is_list(t)
|
||||
return getmetatable(t) == List
|
||||
end
|
||||
|
||||
local function simple_table(t)
|
||||
return type(t) == 'table' and not is_list(t) and #t > 0
|
||||
end
|
||||
|
||||
function List:_init (src)
|
||||
if src then
|
||||
for v in iter(src) do
|
||||
tinsert(self,v)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Create a new list. Can optionally pass a table;
|
||||
-- passing another instance of List will cause a copy to be created
|
||||
-- we pass anything which isn't a simple table to iterate() to work out
|
||||
-- an appropriate iterator @see List.iterate
|
||||
-- @param t An optional list-like table
|
||||
-- @return a new List
|
||||
-- @usage ls = List(); ls = List {1,2,3,4}
|
||||
function List.new(t)
|
||||
local ls
|
||||
if not simple_table(t) then
|
||||
ls = {}
|
||||
List._init(ls,t)
|
||||
else
|
||||
ls = t
|
||||
end
|
||||
makelist(ls)
|
||||
return ls
|
||||
end
|
||||
|
||||
function List:clone()
|
||||
local ls = makelist({},self)
|
||||
List._init(ls,self)
|
||||
return ls
|
||||
end
|
||||
|
||||
function List.default_map_with(T)
|
||||
return function(self,name)
|
||||
local f = T[name]
|
||||
if f then
|
||||
return function(self,...)
|
||||
return self:map(f,...)
|
||||
end
|
||||
else
|
||||
error("method not found: "..name,2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
---Add an item to the end of the list.
|
||||
-- @param i An item
|
||||
-- @return the list
|
||||
function List:append(i)
|
||||
tinsert(self,i)
|
||||
return self
|
||||
end
|
||||
|
||||
List.push = tinsert
|
||||
|
||||
--- Extend the list by appending all the items in the given list.
|
||||
-- equivalent to 'a[len(a):] = L'.
|
||||
-- @param L Another List
|
||||
-- @return the list
|
||||
function List:extend(L)
|
||||
assert_arg(1,L,'table')
|
||||
for i = 1,#L do tinsert(self,L[i]) end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Insert an item at a given position. i is the index of the
|
||||
-- element before which to insert.
|
||||
-- @param i index of element before whichh to insert
|
||||
-- @param x A data item
|
||||
-- @return the list
|
||||
function List:insert(i, x)
|
||||
assert_arg(1,i,'number')
|
||||
tinsert(self,i,x)
|
||||
return self
|
||||
end
|
||||
|
||||
--- Insert an item at the begining of the list.
|
||||
-- @param x a data item
|
||||
-- @return the list
|
||||
function List:put (x)
|
||||
return self:insert(1,x)
|
||||
end
|
||||
|
||||
--- Remove an element given its index.
|
||||
-- (equivalent of Python's del s[i])
|
||||
-- @param i the index
|
||||
-- @return the list
|
||||
function List:remove (i)
|
||||
assert_arg(1,i,'number')
|
||||
tremove(self,i)
|
||||
return self
|
||||
end
|
||||
|
||||
--- Remove the first item from the list whose value is given.
|
||||
-- (This is called 'remove' in Python; renamed to avoid confusion
|
||||
-- with table.remove)
|
||||
-- Return nil if there is no such item.
|
||||
-- @param x A data value
|
||||
-- @return the list
|
||||
function List:remove_value(x)
|
||||
for i=1,#self do
|
||||
if self[i]==x then tremove(self,i) return self end
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Remove the item at the given position in the list, and return it.
|
||||
-- If no index is specified, a:pop() returns the last item in the list.
|
||||
-- The item is also removed from the list.
|
||||
-- @param i An index
|
||||
-- @return the item
|
||||
function List:pop(i)
|
||||
if not i then i = #self end
|
||||
assert_arg(1,i,'number')
|
||||
return tremove(self,i)
|
||||
end
|
||||
|
||||
List.get = List.pop
|
||||
|
||||
--- Return the index in the list of the first item whose value is given.
|
||||
-- Return nil if there is no such item.
|
||||
-- @class function
|
||||
-- @name List:index
|
||||
-- @param x A data value
|
||||
-- @param idx where to start search (default 1)
|
||||
-- @return the index, or nil if not found.
|
||||
|
||||
local tfind = tablex.find
|
||||
List.index = tfind
|
||||
|
||||
--- does this list contain the value?.
|
||||
-- @param x A data value
|
||||
-- @return true or false
|
||||
function List:contains(x)
|
||||
return tfind(self,x) and true or false
|
||||
end
|
||||
|
||||
--- Return the number of times value appears in the list.
|
||||
-- @param x A data value
|
||||
-- @return number of times x appears
|
||||
function List:count(x)
|
||||
local cnt=0
|
||||
for i=1,#self do
|
||||
if self[i]==x then cnt=cnt+1 end
|
||||
end
|
||||
return cnt
|
||||
end
|
||||
|
||||
--- Sort the items of the list, in place.
|
||||
-- @param cmp an optional comparison function; '<' is used if not given.
|
||||
-- @return the list
|
||||
function List:sort(cmp)
|
||||
tsort(self,cmp)
|
||||
return self
|
||||
end
|
||||
|
||||
--- Reverse the elements of the list, in place.
|
||||
-- @return the list
|
||||
function List:reverse()
|
||||
local t = self
|
||||
local n = #t
|
||||
local n2 = n/2
|
||||
for i = 1,n2 do
|
||||
local k = n-i+1
|
||||
t[i],t[k] = t[k],t[i]
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Emulate list slicing. like 'list[first:last]' in Python.
|
||||
-- If first or last are negative then they are relative to the end of the list
|
||||
-- eg. slice(-2) gives last 2 entries in a list, and
|
||||
-- slice(-4,-2) gives from -4th to -2nd
|
||||
-- @param first An index
|
||||
-- @param last An index
|
||||
-- @return a new List
|
||||
function List:slice(first,last)
|
||||
return tsub(self,first,last)
|
||||
end
|
||||
|
||||
--- empty the list.
|
||||
-- @return the list
|
||||
function List:clear()
|
||||
for i=1,#self do tremove(self,i) end
|
||||
return self
|
||||
end
|
||||
|
||||
local eps = 1.0e-10
|
||||
|
||||
--- Emulate Python's range(x) function.
|
||||
-- Include it in List table for tidiness
|
||||
-- @param start A number
|
||||
-- @param finish A number greater than start; if zero, then 0..start-1
|
||||
-- @param incr an optional increment (may be less than 1)
|
||||
-- @usage List.range(0,3) == List {0,1,2,3}
|
||||
function List.range(start,finish,incr)
|
||||
if not finish then
|
||||
start = 0
|
||||
finish = finish - 1
|
||||
end
|
||||
if incr then
|
||||
if not utils.is_integer(incr) then finish = finish + eps end
|
||||
else
|
||||
incr = 1
|
||||
end
|
||||
assert_arg(1,start,'number')
|
||||
assert_arg(2,finish,'number')
|
||||
local t = List.new()
|
||||
for i=start,finish,incr do tinsert(t,i) end
|
||||
return t
|
||||
end
|
||||
|
||||
--- list:len() is the same as #list.
|
||||
function List:len()
|
||||
return #self
|
||||
end
|
||||
|
||||
-- Extended operations --
|
||||
|
||||
--- Remove a subrange of elements.
|
||||
-- equivalent to 'del s[i1:i2]' in Python.
|
||||
-- @param i1 start of range
|
||||
-- @param i2 end of range
|
||||
-- @return the list
|
||||
function List:chop(i1,i2)
|
||||
return tremovevalues(self,i1,i2)
|
||||
end
|
||||
|
||||
--- Insert a sublist into a list
|
||||
-- equivalent to 's[idx:idx] = list' in Python
|
||||
-- @param idx index
|
||||
-- @param list list to insert
|
||||
-- @return the list
|
||||
-- @usage l = List{10,20}; l:splice(2,{21,22}); assert(l == List{10,21,22,20})
|
||||
function List:splice(idx,list)
|
||||
assert_arg(1,idx,'number')
|
||||
idx = idx - 1
|
||||
local i = 1
|
||||
for v in iter(list) do
|
||||
tinsert(self,i+idx,v)
|
||||
i = i + 1
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- general slice assignment s[i1:i2] = seq.
|
||||
-- @param i1 start index
|
||||
-- @param i2 end index
|
||||
-- @param seq a list
|
||||
-- @return the list
|
||||
function List:slice_assign(i1,i2,seq)
|
||||
assert_arg(1,i1,'number')
|
||||
assert_arg(1,i2,'number')
|
||||
i1,i2 = normalize_slice(self,i1,i2)
|
||||
if i2 >= i1 then self:chop(i1,i2) end
|
||||
self:splice(i1,seq)
|
||||
return self
|
||||
end
|
||||
|
||||
--- concatenation operator.
|
||||
-- @param L another List
|
||||
-- @return a new list consisting of the list with the elements of the new list appended
|
||||
function List:__concat(L)
|
||||
assert_arg(1,L,'table')
|
||||
local ls = self:clone()
|
||||
ls:extend(L)
|
||||
return ls
|
||||
end
|
||||
|
||||
--- equality operator ==. True iff all elements of two lists are equal.
|
||||
-- @param L another List
|
||||
-- @return true or false
|
||||
function List:__eq(L)
|
||||
if #self ~= #L then return false end
|
||||
for i = 1,#self do
|
||||
if self[i] ~= L[i] then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- join the elements of a list using a delimiter. <br>
|
||||
-- This method uses tostring on all elements.
|
||||
-- @param delim a delimiter string, can be empty.
|
||||
-- @return a string
|
||||
function List:join (delim)
|
||||
delim = delim or ''
|
||||
assert_arg(1,delim,'string')
|
||||
return concat(imap(tostring,self),delim)
|
||||
end
|
||||
|
||||
--- join a list of strings. <br>
|
||||
-- Uses table.concat directly.
|
||||
-- @class function
|
||||
-- @name List:concat
|
||||
-- @param delim a delimiter
|
||||
-- @return a string
|
||||
List.concat = concat
|
||||
|
||||
local function tostring_q(val)
|
||||
local s = tostring(val)
|
||||
if type(val) == 'string' then
|
||||
s = '"'..s..'"'
|
||||
end
|
||||
return s
|
||||
end
|
||||
|
||||
--- how our list should be rendered as a string. Uses join().
|
||||
-- @see List:join
|
||||
function List:__tostring()
|
||||
return '{'..self:join(',',tostring_q)..'}'
|
||||
end
|
||||
|
||||
--[[
|
||||
-- NOTE: this works, but is unreliable. If you leave the loop before finishing,
|
||||
-- then the iterator is not reset.
|
||||
--- can iterate over a list directly.
|
||||
-- @usage for v in ls do print(v) end
|
||||
function List:__call()
|
||||
if not self.key then self.key = 1 end
|
||||
local value = self[self.key]
|
||||
self.key = self.key + 1
|
||||
if not value then self.key = nil end
|
||||
return value
|
||||
end
|
||||
--]]
|
||||
|
||||
--[[
|
||||
function List.__call(t,v,i)
|
||||
i = (i or 0) + 1
|
||||
v = t[i]
|
||||
if v then return i, v end
|
||||
end
|
||||
--]]
|
||||
|
||||
--- call the function for each element of the list.
|
||||
-- @param fun a function or callable object
|
||||
-- @param ... optional values to pass to function
|
||||
function List:foreach (fun,...)
|
||||
local t = self
|
||||
fun = function_arg(1,fun)
|
||||
for i = 1,#t do
|
||||
fun(t[i],...)
|
||||
end
|
||||
end
|
||||
|
||||
--- create a list of all elements which match a function.
|
||||
-- @param fun a boolean function
|
||||
-- @param arg optional argument to be passed as second argument of the predicate
|
||||
-- @return a new filtered list.
|
||||
function List:filter (fun,arg)
|
||||
return makelist(filter(self,fun,arg),self)
|
||||
end
|
||||
|
||||
--- split a string using a delimiter.
|
||||
-- @param s the string
|
||||
-- @param delim the delimiter (default spaces)
|
||||
-- @return a List of strings
|
||||
-- @see pl.utils.split
|
||||
function List.split (s,delim)
|
||||
assert_arg(1,s,'string')
|
||||
return makelist(split(s,delim))
|
||||
end
|
||||
|
||||
--- apply a function to all elements.
|
||||
-- Any extra arguments will be passed to the function
|
||||
-- @param fun a function of at least one argument
|
||||
-- @param ... arbitrary extra arguments.
|
||||
-- @return a new list: {f(x) for x in self}
|
||||
-- @see pl.tablex.imap
|
||||
function List:map (fun,...)
|
||||
return makelist(imap(fun,self,...),self)
|
||||
end
|
||||
|
||||
--- apply a function to all elements, in-place.
|
||||
-- Any extra arguments are passed to the function.
|
||||
-- @param fun A function that takes at least one argument
|
||||
-- @param ... arbitrary extra arguments.
|
||||
function List:transform (fun,...)
|
||||
transform(fun,self,...)
|
||||
end
|
||||
|
||||
--- apply a function to elements of two lists.
|
||||
-- Any extra arguments will be passed to the function
|
||||
-- @param fun a function of at least two arguments
|
||||
-- @param ls another list
|
||||
-- @param ... arbitrary extra arguments.
|
||||
-- @return a new list: {f(x,y) for x in self, for x in arg1}
|
||||
-- @see pl.tablex.imap2
|
||||
function List:map2 (fun,ls,...)
|
||||
return makelist(imap2(fun,self,ls,...),self)
|
||||
end
|
||||
|
||||
--- apply a named method to all elements.
|
||||
-- Any extra arguments will be passed to the method.
|
||||
-- @param name name of method
|
||||
-- @param ... extra arguments
|
||||
-- @return a new list of the results
|
||||
-- @see pl.seq.mapmethod
|
||||
function List:mapm (name,...)
|
||||
local res = {}
|
||||
local t = self
|
||||
for i = 1,#t do
|
||||
local val = t[i]
|
||||
local fn = val[name]
|
||||
if not fn then error(type(val).." does not have method "..name,2) end
|
||||
res[i] = fn(val,...)
|
||||
end
|
||||
return makelist(res,self)
|
||||
end
|
||||
|
||||
--- 'reduce' a list using a binary function.
|
||||
-- @param fun a function of two arguments
|
||||
-- @return result of the function
|
||||
-- @see pl.tablex.reduce
|
||||
function List:reduce (fun)
|
||||
return reduce(fun,self)
|
||||
end
|
||||
|
||||
--- partition a list using a classifier function.
|
||||
-- The function may return nil, but this will be converted to the string key '<nil>'.
|
||||
-- @param fun a function of at least one argument
|
||||
-- @param ... will also be passed to the function
|
||||
-- @return a table where the keys are the returned values, and the values are Lists
|
||||
-- of values where the function returned that key. It is given the type of Multimap.
|
||||
-- @see pl.MultiMap
|
||||
function List:partition (fun,...)
|
||||
fun = function_arg(1,fun)
|
||||
local res = {}
|
||||
for i = 1,#self do
|
||||
local val = self[i]
|
||||
local klass = fun(val,...)
|
||||
if klass == nil then klass = '<nil>' end
|
||||
if not res[klass] then res[klass] = List() end
|
||||
res[klass]:append(val)
|
||||
end
|
||||
return setmetatable(res,Multimap)
|
||||
end
|
||||
|
||||
--- return an iterator over all values.
|
||||
function List:iter ()
|
||||
return iter(self)
|
||||
end
|
||||
|
||||
--- Create an iterator over a seqence.
|
||||
-- This captures the Python concept of 'sequence'.
|
||||
-- For tables, iterates over all values with integer indices.
|
||||
-- @param seq a sequence; a string (over characters), a table, a file object (over lines) or an iterator function
|
||||
-- @usage for x in iterate {1,10,22,55} do io.write(x,',') end ==> 1,10,22,55
|
||||
-- @usage for ch in iterate 'help' do do io.write(ch,' ') end ==> h e l p
|
||||
function List.iterate(seq)
|
||||
if type(seq) == 'string' then
|
||||
local idx = 0
|
||||
local n = #seq
|
||||
local sub = string.sub
|
||||
return function ()
|
||||
idx = idx + 1
|
||||
if idx > n then return nil
|
||||
else
|
||||
return sub(seq,idx,idx)
|
||||
end
|
||||
end
|
||||
elseif type(seq) == 'table' then
|
||||
local idx = 0
|
||||
local n = #seq
|
||||
return function()
|
||||
idx = idx + 1
|
||||
if idx > n then return nil
|
||||
else
|
||||
return seq[idx]
|
||||
end
|
||||
end
|
||||
elseif type(seq) == 'function' then
|
||||
return seq
|
||||
elseif type(seq) == 'userdata' and io.type(seq) == 'file' then
|
||||
return seq:lines()
|
||||
end
|
||||
end
|
||||
iter = List.iterate
|
||||
|
||||
return List
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
--- A Map class.
|
||||
-- @class module
|
||||
-- @name pl.Map
|
||||
|
||||
--[[
|
||||
module ('pl.Map')
|
||||
]]
|
||||
local tablex = require 'pl.tablex'
|
||||
local utils = require 'pl.utils'
|
||||
local stdmt = utils.stdmt
|
||||
local is_callable = utils.is_callable
|
||||
local tmakeset,deepcompare,merge,keys,difference,tupdate = tablex.makeset,tablex.deepcompare,tablex.merge,tablex.keys,tablex.difference,tablex.update
|
||||
|
||||
local pretty_write = require 'pl.pretty' . write
|
||||
local Map = stdmt.Map
|
||||
local Set = stdmt.Set
|
||||
local List = stdmt.List
|
||||
|
||||
local class = require 'pl.class'
|
||||
|
||||
-- the Map class ---------------------
|
||||
class(nil,nil,Map)
|
||||
|
||||
local function makemap (m)
|
||||
return setmetatable(m,Map)
|
||||
end
|
||||
|
||||
function Map:_init (t)
|
||||
local mt = getmetatable(t)
|
||||
if mt == Set or mt == Map then
|
||||
self:update(t)
|
||||
else
|
||||
return t -- otherwise assumed to be a map-like table
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function makelist(t)
|
||||
return setmetatable(t,List)
|
||||
end
|
||||
|
||||
--- list of keys.
|
||||
Map.keys = tablex.keys
|
||||
|
||||
--- list of values.
|
||||
Map.values = tablex.values
|
||||
|
||||
--- return an iterator over all key-value pairs.
|
||||
function Map:iter ()
|
||||
return pairs(self)
|
||||
end
|
||||
|
||||
--- return a List of all key-value pairs, sorted by the keys.
|
||||
function Map:items()
|
||||
local ls = makelist(tablex.pairmap (function (k,v) return makelist {k,v} end, self))
|
||||
ls:sort(function(t1,t2) return t1[1] < t2[1] end)
|
||||
return ls
|
||||
end
|
||||
|
||||
-- Will return the existing value, or if it doesn't exist it will set
|
||||
-- a default value and return it.
|
||||
function Map:setdefault(key, defaultval)
|
||||
return self[key] or self:set(key,defaultval) or defaultval
|
||||
end
|
||||
|
||||
--- size of map.
|
||||
-- note: this is a relatively expensive operation!
|
||||
-- @class function
|
||||
-- @name Map:len
|
||||
Map.len = tablex.size
|
||||
|
||||
--- put a value into the map.
|
||||
-- @param key the key
|
||||
-- @param val the value
|
||||
function Map:set (key,val)
|
||||
self[key] = val
|
||||
end
|
||||
|
||||
--- get a value from the map.
|
||||
-- @param key the key
|
||||
-- @return the value, or nil if not found.
|
||||
function Map:get (key)
|
||||
return rawget(self,key)
|
||||
end
|
||||
|
||||
local index_by = tablex.index_by
|
||||
|
||||
-- get a list of values indexed by a list of keys.
|
||||
-- @param keys a list-like table of keys
|
||||
-- @return a new list
|
||||
function Map:getvalues (keys)
|
||||
return makelist(index_by(self,keys))
|
||||
end
|
||||
|
||||
Map.iter = pairs
|
||||
|
||||
Map.update = tablex.update
|
||||
|
||||
function Map:__eq (m)
|
||||
-- note we explicitly ask deepcompare _not_ to use __eq!
|
||||
return deepcompare(self,m,true)
|
||||
end
|
||||
|
||||
function Map:__tostring ()
|
||||
return pretty_write(self,'')
|
||||
end
|
||||
|
||||
return Map
|
||||
@@ -0,0 +1,65 @@
|
||||
--- MultiMap, a Map which has multiple values per key. <br>
|
||||
-- @class module
|
||||
-- @name pl.MultiMap
|
||||
|
||||
--[[
|
||||
module ('pl.MultiMap')
|
||||
]]
|
||||
|
||||
local classes = require 'pl.class'
|
||||
local tablex = require 'pl.tablex'
|
||||
local utils = require 'pl.utils'
|
||||
local List = require 'pl.List'
|
||||
|
||||
local index_by,tsort,concat = tablex.index_by,table.sort,table.concat
|
||||
local append,extend,slice = List.append,List.extend,List.slice
|
||||
local append = table.insert
|
||||
local is_type = utils.is_type
|
||||
|
||||
local class = require 'pl.class'
|
||||
local Map = require 'pl.Map'
|
||||
|
||||
-- MultiMap is a standard MT
|
||||
local MultiMap = utils.stdmt.MultiMap
|
||||
|
||||
class(Map,nil,MultiMap)
|
||||
MultiMap._name = 'MultiMap'
|
||||
|
||||
function MultiMap:_init (t)
|
||||
if not t then return end
|
||||
self:update(t)
|
||||
end
|
||||
|
||||
--- update a MultiMap using a table.
|
||||
-- @param t either a Multimap or a map-like table.
|
||||
-- @return the map
|
||||
function MultiMap:update (t)
|
||||
utils.assert_arg(1,t,'table')
|
||||
if Map:class_of(t) then
|
||||
for k,v in pairs(t) do
|
||||
self[k] = List()
|
||||
self[k]:append(v)
|
||||
end
|
||||
else
|
||||
for k,v in pairs(t) do
|
||||
self[k] = List(v)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- add a new value to a key. Setting a nil value removes the key.
|
||||
-- @param key the key
|
||||
-- @param val the value
|
||||
-- @return the map
|
||||
function MultiMap:set (key,val)
|
||||
if val == nil then
|
||||
self[key] = nil
|
||||
else
|
||||
if not self[key] then
|
||||
self[key] = List()
|
||||
end
|
||||
self[key]:append(val)
|
||||
end
|
||||
end
|
||||
|
||||
return MultiMap
|
||||
@@ -0,0 +1,150 @@
|
||||
--- OrderedMap, a pl.Map which preserves ordering.
|
||||
-- @class module
|
||||
-- @name pl.OrderedMap
|
||||
|
||||
--[[
|
||||
module ('pl.OrderedMap')
|
||||
]]
|
||||
|
||||
local tablex = require 'pl.tablex'
|
||||
local utils = require 'pl.utils'
|
||||
local List = require 'pl.List'
|
||||
local index_by,tsort,concat = tablex.index_by,table.sort,table.concat
|
||||
|
||||
local class = require 'pl.class'
|
||||
local Map = require 'pl.Map'
|
||||
|
||||
local OrderedMap = class(Map)
|
||||
OrderedMap._name = 'OrderedMap'
|
||||
|
||||
--- construct an OrderedMap.
|
||||
-- Will throw an error if the argument is bad.
|
||||
-- @param t optional initialization table, same as for @{OrderedMap:update}
|
||||
function OrderedMap:_init (t)
|
||||
self._keys = List()
|
||||
if t then
|
||||
local map,err = self:update(t)
|
||||
if not map then error(err,2) end
|
||||
end
|
||||
end
|
||||
|
||||
local assert_arg,raise = utils.assert_arg,utils.raise
|
||||
|
||||
--- update an OrderedMap using a table. <br>
|
||||
-- If the table is itself an OrderedMap, then its entries will be appended. <br>
|
||||
-- if it s a table of the form <code>{{key1=val1},{key2=val2},...}</code> these will be appended. <br>
|
||||
-- Otherwise, it is assumed to be a map-like table, and order of extra entries is arbitrary.
|
||||
-- @param t a table.
|
||||
-- @return the map, or nil in case of error
|
||||
-- @return the error message
|
||||
function OrderedMap:update (t)
|
||||
assert_arg(1,t,'table')
|
||||
if OrderedMap:class_of(t) then
|
||||
for k,v in t:iter() do
|
||||
self:set(k,v)
|
||||
end
|
||||
elseif #t > 0 then -- an array must contain {key=val} tables
|
||||
if type(t[1]) == 'table' then
|
||||
for _,pair in ipairs(t) do
|
||||
local key,value = next(pair)
|
||||
if not key then return raise 'empty pair initialization table' end
|
||||
self:set(key,value)
|
||||
end
|
||||
else
|
||||
return raise 'cannot use an array to initialize an OrderedMap'
|
||||
end
|
||||
else
|
||||
for k,v in pairs(t) do
|
||||
self:set(k,v)
|
||||
end
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- set the key's value. This key will be appended at the end of the map. <br>
|
||||
-- If the value is nil, then the key is removed.
|
||||
-- @param key the key
|
||||
-- @param val the value
|
||||
-- @return the map
|
||||
function OrderedMap:set (key,val)
|
||||
if not self[key] and val ~= nil then -- ensure that keys are unique
|
||||
self._keys:append(key)
|
||||
elseif val == nil then -- removing a key-value pair
|
||||
self._keys:remove_value(key)
|
||||
end
|
||||
self[key] = val
|
||||
return self
|
||||
end
|
||||
|
||||
--- insert a key/value pair before a given position.
|
||||
-- Note: if the map already contains the key, then this effectively
|
||||
-- moves the item to the new position by first removing at the old position.
|
||||
-- Has no effect if the key does not exist and val is nil
|
||||
-- @param pos a position starting at 1
|
||||
-- @param key the key
|
||||
-- @param val the value; if nil use the old value
|
||||
function OrderedMap:insert (pos,key,val)
|
||||
local oldval = self[key]
|
||||
val = val or oldval
|
||||
if oldval then
|
||||
self._keys:remove_value(key)
|
||||
end
|
||||
if val then
|
||||
self._keys:insert(pos,key)
|
||||
self[key] = val
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- return the keys in order.
|
||||
-- (Not a copy!)
|
||||
-- @return List
|
||||
function OrderedMap:keys ()
|
||||
return self._keys
|
||||
end
|
||||
|
||||
--- return the values in order.
|
||||
-- this is relatively expensive.
|
||||
-- @return List
|
||||
function OrderedMap:values ()
|
||||
return List(index_by(self,self._keys))
|
||||
end
|
||||
|
||||
--- sort the keys.
|
||||
-- @param cmp a comparison function as for @{table.sort}
|
||||
-- @return the map
|
||||
function OrderedMap:sort (cmp)
|
||||
tsort(self._keys,cmp)
|
||||
return self
|
||||
end
|
||||
|
||||
--- iterate over key-value pairs in order.
|
||||
function OrderedMap:iter ()
|
||||
local i = 0
|
||||
local keys = self._keys
|
||||
local n,idx = #keys
|
||||
return function()
|
||||
i = i + 1
|
||||
if i > #keys then return nil end
|
||||
idx = keys[i]
|
||||
return idx,self[idx]
|
||||
end
|
||||
end
|
||||
|
||||
function OrderedMap:__tostring ()
|
||||
local res = {}
|
||||
for i,v in ipairs(self._keys) do
|
||||
local val = self[v]
|
||||
local vs = tostring(val)
|
||||
if type(val) ~= 'number' then
|
||||
vs = '"'..vs..'"'
|
||||
end
|
||||
res[i] = tostring(v)..'='..vs
|
||||
end
|
||||
return '{'..concat(res,',')..'}'
|
||||
end
|
||||
|
||||
return OrderedMap
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
---- A Set class.
|
||||
-- @class module
|
||||
-- @name pl.Set
|
||||
|
||||
--[[
|
||||
module ('pl.Set')
|
||||
]]
|
||||
local tablex = require 'pl.tablex'
|
||||
local utils = require 'pl.utils'
|
||||
local stdmt = utils.stdmt
|
||||
local tmakeset,deepcompare,merge,keys,difference,tupdate = tablex.makeset,tablex.deepcompare,tablex.merge,tablex.keys,tablex.difference,tablex.update
|
||||
local Map = stdmt.Map
|
||||
local Set = stdmt.Set
|
||||
local List = stdmt.List
|
||||
local class = require 'pl.class'
|
||||
|
||||
-- the Set class --------------------
|
||||
class(Map,nil,Set)
|
||||
|
||||
local function makeset (t)
|
||||
return setmetatable(t,Set)
|
||||
end
|
||||
|
||||
--- create a set. <br>
|
||||
-- @param t may be a Set, Map or list-like table.
|
||||
-- @class function
|
||||
-- @name Set
|
||||
function Set:_init (t)
|
||||
local mt = getmetatable(t)
|
||||
if mt == Set or mt == Map then
|
||||
for k in pairs(t) do self[k] = true end
|
||||
else
|
||||
for _,v in ipairs(t) do self[v] = true end
|
||||
end
|
||||
end
|
||||
|
||||
function Set:__tostring ()
|
||||
return '['..self:keys():join ','..']'
|
||||
end
|
||||
|
||||
--- add a value to a set.
|
||||
-- @param key a value
|
||||
function Set:set (key)
|
||||
self[key] = true
|
||||
end
|
||||
|
||||
--- remove a value from a set.
|
||||
-- @param key a value
|
||||
function Set:unset (key)
|
||||
self[key] = nil
|
||||
end
|
||||
|
||||
--- get a list of the values in a set.
|
||||
-- @class function
|
||||
-- @name Set:values
|
||||
Set.values = Map.keys
|
||||
|
||||
--- map a function over the values of a set.
|
||||
-- @param fn a function
|
||||
-- @param ... extra arguments to pass to the function.
|
||||
-- @return a new set
|
||||
function Set:map (fn,...)
|
||||
fn = utils.function_arg(1,fn)
|
||||
local res = {}
|
||||
for k in pairs(self) do
|
||||
res[fn(k,...)] = true
|
||||
end
|
||||
return makeset(res)
|
||||
end
|
||||
|
||||
--- union of two sets (also +).
|
||||
-- @param set another set
|
||||
-- @return a new set
|
||||
function Set:union (set)
|
||||
return merge(self,set,true)
|
||||
end
|
||||
Set.__add = Set.union
|
||||
|
||||
--- intersection of two sets (also *).
|
||||
-- @param set another set
|
||||
-- @return a new set
|
||||
function Set:intersection (set)
|
||||
return merge(self,set,false)
|
||||
end
|
||||
Set.__mul = Set.intersection
|
||||
|
||||
--- new set with elements in the set that are not in the other (also -).
|
||||
-- @param set another set
|
||||
-- @return a new set
|
||||
function Set:difference (set)
|
||||
return difference(self,set,false)
|
||||
end
|
||||
Set.__sub = Set.difference
|
||||
|
||||
-- a new set with elements in _either_ the set _or_ other but not both (also ^).
|
||||
-- @param set another set
|
||||
-- @return a new set
|
||||
function Set:symmetric_difference (set)
|
||||
return difference(self,set,true)
|
||||
end
|
||||
Set.__pow = Set.symmetric_difference
|
||||
|
||||
--- is the first set a subset of the second?.
|
||||
-- @return true or false
|
||||
function Set:issubset (set)
|
||||
for k in pairs(self) do
|
||||
if not set[k] then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
Set.__lt = Set.subset
|
||||
|
||||
--- is the set empty?.
|
||||
-- @return true or false
|
||||
function Set:issempty ()
|
||||
return next(self) == nil
|
||||
end
|
||||
|
||||
--- are the sets disjoint? (no elements in common).
|
||||
-- Uses naive definition, i.e. that intersection is empty
|
||||
-- @param set another set
|
||||
-- @return true or false
|
||||
function Set:isdisjoint (set)
|
||||
return self:intersection(set):isempty()
|
||||
end
|
||||
|
||||
return Set
|
||||
@@ -0,0 +1,143 @@
|
||||
--- Application support functions.
|
||||
-- <p>See <a href="../../index.html#app">the Guide</a>
|
||||
-- @class module
|
||||
-- @name pl.app
|
||||
|
||||
local io,package,require = _G.io, _G.package, _G.require
|
||||
local utils = require 'pl.utils'
|
||||
local path = require 'pl.path'
|
||||
local lfs = require 'lfs'
|
||||
|
||||
|
||||
local app = {}
|
||||
|
||||
local function check_script_name ()
|
||||
if _G.arg == nil then error('no command line args available\nWas this run from a main script?') end
|
||||
return _G.arg[0]
|
||||
end
|
||||
|
||||
--- add the current script's path to the Lua module path.
|
||||
-- Applies to both the source and the binary module paths. It makes it easy for
|
||||
-- the main file of a multi-file program to access its modules in the same directory.
|
||||
-- `base` allows these modules to be put in a specified subdirectory, to allow for
|
||||
-- cleaner deployment and resolve potential conflicts between a script name and its
|
||||
-- library directory.
|
||||
-- @param base optional base directory.
|
||||
-- @return the current script's path with a trailing slash
|
||||
function app.require_here (base)
|
||||
local p = path.dirname(check_script_name())
|
||||
if not path.isabs(p) then
|
||||
p = path.join(lfs.currentdir(),p)
|
||||
end
|
||||
if p:sub(-1,-1) ~= path.sep then
|
||||
p = p..path.sep
|
||||
end
|
||||
if base then
|
||||
p = p..base..path.sep
|
||||
end
|
||||
local so_ext = path.is_windows and 'dll' or 'so'
|
||||
local lsep = package.path:find '^;' and '' or ';'
|
||||
local csep = package.cpath:find '^;' and '' or ';'
|
||||
package.path = ('%s?.lua;%s?%sinit.lua%s%s'):format(p,p,path.sep,lsep,package.path)
|
||||
package.cpath = ('%s?.%s%s%s'):format(p,so_ext,csep,package.cpath)
|
||||
return p
|
||||
end
|
||||
|
||||
--- return a suitable path for files private to this application.
|
||||
-- These will look like '~/.SNAME/file', with '~' as with expanduser and
|
||||
-- SNAME is the name of the script without .lua extension.
|
||||
-- @param file a filename (w/out path)
|
||||
-- @return a full pathname, or nil
|
||||
-- @return 'cannot create' error
|
||||
function app.appfile (file)
|
||||
local sname = path.basename(check_script_name())
|
||||
local name,ext = path.splitext(sname)
|
||||
local dir = path.join(path.expanduser('~'),'.'..name)
|
||||
if not path.isdir(dir) then
|
||||
local ret = lfs.mkdir(dir)
|
||||
if not ret then return utils.raise ('cannot create '..dir) end
|
||||
end
|
||||
return path.join(dir,file)
|
||||
end
|
||||
|
||||
--- return string indicating operating system.
|
||||
-- @return 'Windows','OSX' or whatever uname returns (e.g. 'Linux')
|
||||
function app.platform()
|
||||
if path.is_windows then
|
||||
return 'Windows'
|
||||
else
|
||||
local f = io.popen('uname')
|
||||
local res = f:read()
|
||||
if res == 'Darwin' then res = 'OSX' end
|
||||
f:close()
|
||||
return res
|
||||
end
|
||||
end
|
||||
|
||||
--- parse command-line arguments into flags and parameters.
|
||||
-- Understands GNU-style command-line flags; short (-f) and long (--flag).
|
||||
-- These may be given a value with either '=' or ':' (-k:2,--alpha=3.2,-n2);
|
||||
-- note that a number value can be given without a space.
|
||||
-- Multiple short args can be combined like so: (-abcd).
|
||||
-- @param args an array of strings (default is the global 'arg')
|
||||
-- @param flags_with_values any flags that take values, e.g. <code>{out=true}</code>
|
||||
-- @return a table of flags (flag=value pairs)
|
||||
-- @return an array of parameters
|
||||
-- @raise if args is nil, then the global `args` must be available!
|
||||
function app.parse_args (args,flags_with_values)
|
||||
if not args then
|
||||
args = _G.arg
|
||||
if not args then error "Not in a main program: 'arg' not found" end
|
||||
end
|
||||
flags_with_values = flags_with_values or {}
|
||||
local _args = {}
|
||||
local flags = {}
|
||||
local i = 1
|
||||
while i <= #args do
|
||||
local a = args[i]
|
||||
local v = a:match('^-(.+)')
|
||||
local is_long
|
||||
if v then -- we have a flag
|
||||
if v:find '^-' then
|
||||
is_long = true
|
||||
v = v:sub(2)
|
||||
end
|
||||
if flags_with_values[v] then
|
||||
if i == #_args or args[i+1]:find '^-' then
|
||||
return utils.raise ("no value for '"..v.."'")
|
||||
end
|
||||
flags[v] = args[i+1]
|
||||
i = i + 1
|
||||
else
|
||||
-- a value can be indicated with = or :
|
||||
local var,val = utils.splitv (v,'[=:]')
|
||||
var = var or v
|
||||
val = val or true
|
||||
if not is_long then
|
||||
if #var > 1 then
|
||||
if var:find '.%d+' then -- short flag, number value
|
||||
val = var:sub(2)
|
||||
var = var:sub(1,1)
|
||||
else -- multiple short flags
|
||||
for i = 1,#var do
|
||||
flags[var:sub(i,i)] = true
|
||||
end
|
||||
val = nil -- prevents use of var as a flag below
|
||||
end
|
||||
else -- single short flag (can have value, defaults to true)
|
||||
val = val or true
|
||||
end
|
||||
end
|
||||
if val then
|
||||
flags[var] = val
|
||||
end
|
||||
end
|
||||
else
|
||||
_args[#_args+1] = a
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
return flags,_args
|
||||
end
|
||||
|
||||
return app
|
||||
@@ -0,0 +1,391 @@
|
||||
--- Operations on two-dimensional arrays.
|
||||
-- @class module
|
||||
-- @name pl.array2d
|
||||
|
||||
local require, type,tonumber,assert,tostring,io,ipairs,string,table =
|
||||
_G.require, _G.type,_G.tonumber,_G.assert,_G.tostring,_G.io,_G.ipairs,_G.string,_G.table
|
||||
local ops = require 'pl.operator'
|
||||
local tablex = require 'pl.tablex'
|
||||
local utils = require 'pl.utils'
|
||||
|
||||
local imap,tmap,reduce,keys,tmap2,tset,index_by = tablex.imap,tablex.map,tablex.reduce,tablex.keys,tablex.map2,tablex.set,tablex.index_by
|
||||
local remove = table.remove
|
||||
local perm = require 'pl.permute'
|
||||
local splitv,fprintf,assert_arg = utils.splitv,utils.fprintf,utils.assert_arg
|
||||
local byte = string.byte
|
||||
local stdout = io.stdout
|
||||
|
||||
--[[
|
||||
module ('pl.array2d',utils._module)
|
||||
]]
|
||||
|
||||
local array2d = {}
|
||||
|
||||
--- extract a column from the 2D array.
|
||||
-- @param a 2d array
|
||||
-- @param key an index or key
|
||||
-- @return 1d array
|
||||
function array2d.column (a,key)
|
||||
assert_arg(1,a,'table')
|
||||
return imap(ops.index,a,key)
|
||||
end
|
||||
local column = array2d.column
|
||||
|
||||
--- map a function over a 2D array
|
||||
-- @param f a function of at least one argument
|
||||
-- @param a 2d array
|
||||
-- @param arg an optional extra argument to be passed to the function.
|
||||
-- @return 2d array
|
||||
function array2d.map (f,a,arg)
|
||||
assert_arg(1,a,'table')
|
||||
f = utils.function_arg(1,f)
|
||||
return imap(function(row) return imap(f,row,arg) end, a)
|
||||
end
|
||||
|
||||
--- reduce the rows using a function.
|
||||
-- @param f a binary function
|
||||
-- @param a 2d array
|
||||
-- @return 1d array
|
||||
-- @see pl.tablex.reduce
|
||||
function array2d.reduce_rows (f,a)
|
||||
assert_arg(1,a,'table')
|
||||
return tmap(function(row) return reduce(f,row) end, a)
|
||||
end
|
||||
|
||||
|
||||
|
||||
--- reduce the columns using a function.
|
||||
-- @param f a binary function
|
||||
-- @param a 2d array
|
||||
-- @return 1d array
|
||||
-- @see pl.tablex.reduce
|
||||
function array2d.reduce_cols (f,a)
|
||||
assert_arg(1,a,'table')
|
||||
return tmap(function(c) return reduce(f,column(a,c)) end, keys(a[1]))
|
||||
end
|
||||
|
||||
--- reduce a 2D array into a scalar, using two operations.
|
||||
-- @param opc operation to reduce the final result
|
||||
-- @param opr operation to reduce the rows
|
||||
-- @param a 2D array
|
||||
function array2d.reduce2 (opc,opr,a)
|
||||
assert_arg(3,a,'table')
|
||||
local tmp = array2d.reduce_rows(opr,a)
|
||||
return reduce(opc,tmp)
|
||||
end
|
||||
|
||||
local function dimension (t)
|
||||
return type(t[1])=='table' and 2 or 1
|
||||
end
|
||||
|
||||
--- map a function over two arrays.
|
||||
-- They can be both or either 2D arrays
|
||||
-- @param f function of at least two arguments
|
||||
-- @param ad order of first array
|
||||
-- @param bd order of second array
|
||||
-- @param a 1d or 2d array
|
||||
-- @param b 1d or 2d array
|
||||
-- @param arg optional extra argument to pass to function
|
||||
-- @return 2D array, unless both arrays are 1D
|
||||
function array2d.map2 (f,ad,bd,a,b,arg)
|
||||
assert_arg(1,a,'table')
|
||||
assert_arg(2,b,'table')
|
||||
f = utils.function_arg(1,f)
|
||||
--local ad,bd = dimension(a),dimension(b)
|
||||
if ad == 1 and bd == 2 then
|
||||
return imap(function(row)
|
||||
return tmap2(f,a,row,arg)
|
||||
end, b)
|
||||
elseif ad == 2 and bd == 1 then
|
||||
return imap(function(row)
|
||||
return tmap2(f,row,b,arg)
|
||||
end, a)
|
||||
elseif ad == 1 and bd == 1 then
|
||||
return tmap2(f,a,b)
|
||||
elseif ad == 2 and bd == 2 then
|
||||
return tmap2(function(rowa,rowb)
|
||||
return tmap2(f,rowa,rowb,arg)
|
||||
end, a,b)
|
||||
end
|
||||
end
|
||||
|
||||
--- cartesian product of two 1d arrays.
|
||||
-- @param f a function of 2 arguments
|
||||
-- @param t1 a 1d table
|
||||
-- @param t2 a 1d table
|
||||
-- @return 2d table
|
||||
-- @usage product('..',{1,2},{'a','b'}) == {{'1a','2a'},{'1b','2b'}}
|
||||
function array2d.product (f,t1,t2)
|
||||
f = utils.function_arg(1,f)
|
||||
assert_arg(2,t1,'table')
|
||||
assert_arg(3,t2,'table')
|
||||
local res, map = {}, tablex.map
|
||||
for i,v in ipairs(t2) do
|
||||
res[i] = map(f,t1,v)
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
--- flatten a 2D array.
|
||||
-- (this goes over columns first.)
|
||||
-- @param t 2d table
|
||||
-- @return a 1d table
|
||||
-- @usage flatten {{1,2},{3,4},{5,6}} == {1,2,3,4,5,6}
|
||||
function array2d.flatten (t)
|
||||
local res = {}
|
||||
local k = 1
|
||||
for _,a in ipairs(t) do -- for all rows
|
||||
for i = 1,#a do
|
||||
res[k] = a[i]
|
||||
k = k + 1
|
||||
end
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
--- swap two rows of an array.
|
||||
-- @param t a 2d array
|
||||
-- @param i1 a row index
|
||||
-- @param i2 a row index
|
||||
function array2d.swap_rows (t,i1,i2)
|
||||
assert_arg(1,t,'table')
|
||||
t[i1],t[i2] = t[i2],t[i1]
|
||||
end
|
||||
|
||||
--- swap two columns of an array.
|
||||
-- @param t a 2d array
|
||||
-- @param j1 a column index
|
||||
-- @param j2 a column index
|
||||
function array2d.swap_cols (t,j1,j2)
|
||||
assert_arg(1,t,'table')
|
||||
for i = 1,#t do
|
||||
local row = t[i]
|
||||
row[j1],row[j2] = row[j2],row[j1]
|
||||
end
|
||||
end
|
||||
|
||||
--- extract the specified rows.
|
||||
-- @param t 2d array
|
||||
-- @param ridx a table of row indices
|
||||
function array2d.extract_rows (t,ridx)
|
||||
return index_by(t,ridx)
|
||||
end
|
||||
|
||||
--- extract the specified columns.
|
||||
-- @param t 2d array
|
||||
-- @param cidx a table of column indices
|
||||
function array2d.extract_cols (t,cidx)
|
||||
assert_arg(1,t,'table')
|
||||
for i = 1,#t do
|
||||
t[i] = index_by(t[i],cidx)
|
||||
end
|
||||
end
|
||||
|
||||
--- remove a row from an array.
|
||||
-- @class function
|
||||
-- @name array2d.remove_row
|
||||
-- @param t a 2d array
|
||||
-- @param i a row index
|
||||
array2d.remove_row = remove
|
||||
|
||||
--- remove a column from an array.
|
||||
-- @param t a 2d array
|
||||
-- @param j a column index
|
||||
function array2d.remove_col (t,j)
|
||||
assert_arg(1,t,'table')
|
||||
for i = 1,#t do
|
||||
remove(t[i],j)
|
||||
end
|
||||
end
|
||||
|
||||
local Ai = byte 'A'
|
||||
|
||||
local function _parse (s)
|
||||
local c,r
|
||||
if s:sub(1,1) == 'R' then
|
||||
r,c = s:match 'R(%d+)C(%d+)'
|
||||
r,c = tonumber(r),tonumber(c)
|
||||
else
|
||||
c,r = s:match '(.)(.)'
|
||||
c = byte(c) - byte 'A' + 1
|
||||
r = tonumber(r)
|
||||
end
|
||||
assert(c ~= nil and r ~= nil,'bad cell specifier: '..s)
|
||||
return r,c
|
||||
end
|
||||
|
||||
--- parse a spreadsheet range.
|
||||
-- The range can be specified either as 'A1:B2' or 'R1C1:R2C2';
|
||||
-- a special case is a single element (e.g 'A1' or 'R1C1')
|
||||
-- @param s a range.
|
||||
-- @return start col
|
||||
-- @return start row
|
||||
-- @return end col
|
||||
-- @return end row
|
||||
function array2d.parse_range (s)
|
||||
if s:find ':' then
|
||||
local start,finish = splitv(s,':')
|
||||
local i1,j1 = _parse(start)
|
||||
local i2,j2 = _parse(finish)
|
||||
return i1,j1,i2,j2
|
||||
else -- single value
|
||||
local i,j = _parse(s)
|
||||
return i,j
|
||||
end
|
||||
end
|
||||
|
||||
--- get a slice of a 2D array using spreadsheet range notation. @see parse_range
|
||||
-- @param t a 2D array
|
||||
-- @param rstr range expression
|
||||
-- @return a slice
|
||||
-- @see array2d.parse_range
|
||||
-- @see array2d.slice
|
||||
function array2d.range (t,rstr)
|
||||
assert_arg(1,t,'table')
|
||||
local i1,j1,i2,j2 = array2d.parse_range(rstr)
|
||||
if i2 then
|
||||
return array2d.slice(t,i1,j1,i2,j2)
|
||||
else -- single value
|
||||
return t[i1][j1]
|
||||
end
|
||||
end
|
||||
|
||||
local function default_range (t,i1,j1,i2,j2)
|
||||
assert(t and type(t)=='table','not a table')
|
||||
i1,j1 = i1 or 1, j1 or 1
|
||||
i2,j2 = i2 or #t, j2 or #t[1]
|
||||
return i1,j1,i2,j2
|
||||
end
|
||||
|
||||
--- get a slice of a 2D array. Note that if the specified range has
|
||||
-- a 1D result, the rank of the result will be 1.
|
||||
-- @param t a 2D array
|
||||
-- @param i1 start row (default 1)
|
||||
-- @param j1 start col (default 1)
|
||||
-- @param i2 end row (default N)
|
||||
-- @param j2 end col (default M)
|
||||
-- @return an array, 2D in general but 1D in special cases.
|
||||
function array2d.slice (t,i1,j1,i2,j2)
|
||||
assert_arg(1,t,'table')
|
||||
i1,j1,i2,j2 = default_range(t,i1,j1,i2,j2)
|
||||
local res = {}
|
||||
for i = i1,i2 do
|
||||
local val
|
||||
local row = t[i]
|
||||
if j1 == j2 then
|
||||
val = row[j1]
|
||||
else
|
||||
val = {}
|
||||
for j = j1,j2 do
|
||||
val[#val+1] = row[j]
|
||||
end
|
||||
end
|
||||
res[#res+1] = val
|
||||
end
|
||||
if i1 == i2 then res = res[1] end
|
||||
return res
|
||||
end
|
||||
|
||||
--- set a specified range of an array to a value.
|
||||
-- @param t a 2D array
|
||||
-- @param value the value
|
||||
-- @param i1 start row (default 1)
|
||||
-- @param j1 start col (default 1)
|
||||
-- @param i2 end row (default N)
|
||||
-- @param j2 end col (default M)
|
||||
function array2d.set (t,value,i1,j1,i2,j2)
|
||||
i1,j1,i2,j2 = default_range(t,i1,j1,i2,j2)
|
||||
for i = i1,i2 do
|
||||
tset(t[i],value)
|
||||
end
|
||||
end
|
||||
|
||||
--- write a 2D array to a file.
|
||||
-- @param t a 2D array
|
||||
-- @param f a file object (default stdout)
|
||||
-- @param fmt a format string (default is just to use tostring)
|
||||
-- @param i1 start row (default 1)
|
||||
-- @param j1 start col (default 1)
|
||||
-- @param i2 end row (default N)
|
||||
-- @param j2 end col (default M)
|
||||
function array2d.write (t,f,fmt,i1,j1,i2,j2)
|
||||
assert_arg(1,t,'table')
|
||||
f = f or stdout
|
||||
local rowop
|
||||
if fmt then
|
||||
rowop = function(row,j) fprintf(f,fmt,row[j]) end
|
||||
else
|
||||
rowop = function(row,j) f:write(tostring(row[j]),' ') end
|
||||
end
|
||||
local function newline()
|
||||
f:write '\n'
|
||||
end
|
||||
array2d.forall(t,rowop,newline,i1,j1,i2,j2)
|
||||
end
|
||||
|
||||
--- perform an operation for all values in a 2D array.
|
||||
-- @param t 2D array
|
||||
-- @param row_op function to call on each value
|
||||
-- @param end_row_op function to call at end of each row
|
||||
-- @param i1 start row (default 1)
|
||||
-- @param j1 start col (default 1)
|
||||
-- @param i2 end row (default N)
|
||||
-- @param j2 end col (default M)
|
||||
function array2d.forall (t,row_op,end_row_op,i1,j1,i2,j2)
|
||||
assert_arg(1,t,'table')
|
||||
i1,j1,i2,j2 = default_range(t,i1,j1,i2,j2)
|
||||
for i = i1,i2 do
|
||||
local row = t[i]
|
||||
for j = j1,j2 do
|
||||
row_op(row,j)
|
||||
end
|
||||
if end_row_op then end_row_op(i) end
|
||||
end
|
||||
end
|
||||
|
||||
--- iterate over all elements in a 2D array, with optional indices.
|
||||
-- @param a 2D array
|
||||
-- @param indices with indices (default false)
|
||||
-- @param i1 start row (default 1)
|
||||
-- @param j1 start col (default 1)
|
||||
-- @param i2 end row (default N)
|
||||
-- @param j2 end col (default M)
|
||||
-- @return either value or i,j,value depending on indices
|
||||
function array2d.iter (a,indices,i1,j1,i2,j2)
|
||||
assert_arg(1,a,'table')
|
||||
local norowset = not (i2 and j2)
|
||||
i1,j1,i2,j2 = default_range(a,i1,j1,i2,j2)
|
||||
local n,i,j = i2-i1+1,i1-1,j1-1
|
||||
local row,nr = nil,0
|
||||
local onr = j2 - j1 + 1
|
||||
return function()
|
||||
j = j + 1
|
||||
if j > nr then
|
||||
j = j1
|
||||
i = i + 1
|
||||
if i > i2 then return nil end
|
||||
row = a[i]
|
||||
nr = norowset and #row or onr
|
||||
end
|
||||
if indices then
|
||||
return i,j,row[j]
|
||||
else
|
||||
return row[j]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function array2d.columns (a)
|
||||
assert_arg(1,a,'table')
|
||||
local n = a[1][1]
|
||||
local i = 0
|
||||
return function()
|
||||
i = i + 1
|
||||
if i > n then return nil end
|
||||
return column(a,i)
|
||||
end
|
||||
end
|
||||
|
||||
return array2d
|
||||
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
--- Provides a reuseable and convenient framework for creating classes in Lua.
|
||||
-- Two possible notations: <br> <code> B = class(A) </code> or <code> class.B(A) </code>. <br>
|
||||
-- <p>The latter form creates a named class. </p>
|
||||
-- See the Guide for further <a href="../../index.html#class">discussion</a>
|
||||
-- @module pl.class
|
||||
|
||||
local error, getmetatable, io, pairs, rawget, rawset, setmetatable, tostring, type =
|
||||
_G.error, _G.getmetatable, _G.io, _G.pairs, _G.rawget, _G.rawset, _G.setmetatable, _G.tostring, _G.type
|
||||
-- this trickery is necessary to prevent the inheritance of 'super' and
|
||||
-- the resulting recursive call problems.
|
||||
local function call_ctor (c,obj,...)
|
||||
-- nice alias for the base class ctor
|
||||
local base = rawget(c,'_base')
|
||||
if base then obj.super = rawget(base,'_init') end
|
||||
local res = c._init(obj,...)
|
||||
obj.super = nil
|
||||
return res
|
||||
end
|
||||
|
||||
local function is_a(self,klass)
|
||||
local m = getmetatable(self)
|
||||
if not m then return false end --*can't be an object!
|
||||
while m do
|
||||
if m == klass then return true end
|
||||
m = rawget(m,'_base')
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function class_of(klass,obj)
|
||||
if type(klass) ~= 'table' or not rawget(klass,'is_a') then return false end
|
||||
return klass.is_a(obj,klass)
|
||||
end
|
||||
|
||||
local function _class_tostring (obj)
|
||||
local mt = obj._class
|
||||
local name = rawget(mt,'_name')
|
||||
setmetatable(obj,nil)
|
||||
local str = tostring(obj)
|
||||
setmetatable(obj,mt)
|
||||
if name then str = name ..str:gsub('table','') end
|
||||
return str
|
||||
end
|
||||
|
||||
local function tupdate(td,ts)
|
||||
for k,v in pairs(ts) do
|
||||
td[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
local function _class(base,c_arg,c)
|
||||
c = c or {} -- a new class instance, which is the metatable for all objects of this type
|
||||
-- the class will be the metatable for all its objects,
|
||||
-- and they will look up their methods in it.
|
||||
local mt = {} -- a metatable for the class instance
|
||||
|
||||
if type(base) == 'table' then
|
||||
-- our new class is a shallow copy of the base class!
|
||||
tupdate(c,base)
|
||||
c._base = base
|
||||
-- inherit the 'not found' handler, if present
|
||||
if rawget(c,'_handler') then mt.__index = c._handler end
|
||||
elseif base ~= nil then
|
||||
error("must derive from a table type",3)
|
||||
end
|
||||
|
||||
c.__index = c
|
||||
setmetatable(c,mt)
|
||||
c._init = nil
|
||||
|
||||
if base and rawget(base,'_class_init') then
|
||||
base._class_init(c,c_arg)
|
||||
end
|
||||
|
||||
-- expose a ctor which can be called by <classname>(<args>)
|
||||
mt.__call = function(class_tbl,...)
|
||||
local obj = {}
|
||||
setmetatable(obj,c)
|
||||
|
||||
if rawget(c,'_init') then -- explicit constructor
|
||||
local res = call_ctor(c,obj,...)
|
||||
if res then -- _if_ a ctor returns a value, it becomes the object...
|
||||
obj = res
|
||||
setmetatable(obj,c)
|
||||
end
|
||||
elseif base and rawget(base,'_init') then -- default constructor
|
||||
-- make sure that any stuff from the base class is initialized!
|
||||
call_ctor(base,obj,...)
|
||||
end
|
||||
|
||||
if base and rawget(base,'_post_init') then
|
||||
base._post_init(obj)
|
||||
end
|
||||
|
||||
if not rawget(c,'__tostring') then
|
||||
c.__tostring = _class_tostring
|
||||
end
|
||||
return obj
|
||||
end
|
||||
-- Call Class.catch to set a handler for methods/properties not found in the class!
|
||||
c.catch = function(handler)
|
||||
c._handler = handler
|
||||
mt.__index = handler
|
||||
end
|
||||
c.is_a = is_a
|
||||
c.class_of = class_of
|
||||
c._class = c
|
||||
-- any object can have a specified delegate which is called with unrecognized methods
|
||||
-- if _handler exists and obj[key] is nil, then pass onto handler!
|
||||
c.delegate = function(self,obj)
|
||||
mt.__index = function(tbl,key)
|
||||
local method = obj[key]
|
||||
if method then
|
||||
return function(self,...)
|
||||
return method(obj,...)
|
||||
end
|
||||
elseif self._handler then
|
||||
return self._handler(tbl,key)
|
||||
end
|
||||
end
|
||||
end
|
||||
return c
|
||||
end
|
||||
|
||||
--- create a new class, derived from a given base class. <br>
|
||||
-- Supporting two class creation syntaxes:
|
||||
-- either <code>Name = class(base)</code> or <code>class.Name(base)</code>
|
||||
-- @class function
|
||||
-- @name class
|
||||
-- @param base optional base class
|
||||
-- @param c_arg optional parameter to class ctor
|
||||
-- @param c optional table to be used as class
|
||||
local class
|
||||
class = setmetatable({},{
|
||||
__call = function(fun,...)
|
||||
return _class(...)
|
||||
end,
|
||||
__index = function(tbl,key)
|
||||
if key == 'class' then
|
||||
io.stderr:write('require("pl.class").class is deprecated. Use require("pl.class")\n')
|
||||
return class
|
||||
end
|
||||
local env = _G
|
||||
return function(...)
|
||||
local c = _class(...)
|
||||
c._name = key
|
||||
rawset(env,key,c)
|
||||
return c
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
|
||||
return class
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
--- List comprehensions implemented in Lua. <p>
|
||||
--
|
||||
-- See the <a href="http://lua-users.org/wiki/ListComprehensions">wiki page</a>
|
||||
-- <pre class=example>
|
||||
-- local C= require 'pl.comprehension' . new()
|
||||
--
|
||||
-- C ('x for x=1,10') ()
|
||||
-- ==> {1,2,3,4,5,6,7,8,9,10}
|
||||
-- C 'x^2 for x=1,4' ()
|
||||
-- ==> {1,4,9,16}
|
||||
-- C '{x,x^2} for x=1,4' ()
|
||||
-- ==> {{1,1},{2,4},{3,9},{4,16}}
|
||||
-- C '2*x for x' {1,2,3}
|
||||
-- ==> {2,4,6}
|
||||
-- dbl = C '2*x for x'
|
||||
-- dbl {10,20,30}
|
||||
-- ==> {20,40,60}
|
||||
-- C 'x for x if x % 2 == 0' {1,2,3,4,5}
|
||||
-- ==> {2,4}
|
||||
-- C '{x,y} for x = 1,2 for y = 1,2' ()
|
||||
-- ==> {{1,1},{1,2},{2,1},{2,2}}
|
||||
-- C '{x,y} for x for y' ({1,2},{10,20})
|
||||
-- ==> {{1,10},{1,20},{2,10},{2,20}}
|
||||
-- assert(C 'sum(x^2 for x)' {2,3,4} == 2^2+3^2+4^2)
|
||||
-- </pre>
|
||||
--
|
||||
-- <p> (c) 2008 David Manura. Licensed under the same terms as Lua (MIT license).
|
||||
-- <p> -- See <a href="../../index.html#T31">the Guide</a>
|
||||
-- @class module
|
||||
-- @name pl.comprehension
|
||||
|
||||
local utils = require 'pl.utils'
|
||||
|
||||
--~ local _VERSION, assert, getfenv, ipairs, load, math, pcall, require, setmetatable, table, tonumber =
|
||||
--~ _G._VERSION, _G.assert, _G.getfenv, _G.ipairs, _G.load, _G.math, _G.pcall, _G.require, _G.setmetatable, _G.table, _G.tonumber
|
||||
|
||||
local status,lb = pcall(require, "pl.luabalanced")
|
||||
if not status then
|
||||
lb = require 'luabalanced'
|
||||
end
|
||||
|
||||
local math_max = math.max
|
||||
local table_concat = table.concat
|
||||
|
||||
-- fold operations
|
||||
-- http://en.wikipedia.org/wiki/Fold_(higher-order_function)
|
||||
local ops = {
|
||||
list = {init=' {} ', accum=' __result[#__result+1] = (%s) '},
|
||||
table = {init=' {} ', accum=' local __k, __v = %s __result[__k] = __v '},
|
||||
sum = {init=' 0 ', accum=' __result = __result + (%s) '},
|
||||
min = {init=' nil ', accum=' local __tmp = %s ' ..
|
||||
' if __result then if __tmp < __result then ' ..
|
||||
'__result = __tmp end else __result = __tmp end '},
|
||||
max = {init=' nil ', accum=' local __tmp = %s ' ..
|
||||
' if __result then if __tmp > __result then ' ..
|
||||
'__result = __tmp end else __result = __tmp end '},
|
||||
}
|
||||
|
||||
|
||||
-- Parses comprehension string expr.
|
||||
-- Returns output expression list <out> string, array of for types
|
||||
-- ('=', 'in' or nil) <fortypes>, array of input variable name
|
||||
-- strings <invarlists>, array of input variable value strings
|
||||
-- <invallists>, array of predicate expression strings <preds>,
|
||||
-- operation name string <opname>, and number of placeholder
|
||||
-- parameters <max_param>.
|
||||
--
|
||||
-- The is equivalent to the mathematical set-builder notation:
|
||||
--
|
||||
-- <opname> { <out> | <invarlist> in <invallist> , <preds> }
|
||||
--
|
||||
-- @usage "x^2 for x" -- array values
|
||||
-- @usage "x^2 for x=1,10,2" -- numeric for
|
||||
-- @usage "k^v for k,v in pairs(_1)" -- iterator for
|
||||
-- @usage "(x+y)^2 for x for y if x > y" -- nested
|
||||
--
|
||||
local function parse_comprehension(expr)
|
||||
local t = {}
|
||||
local pos = 1
|
||||
|
||||
-- extract opname (if exists)
|
||||
local opname
|
||||
local tok, post = expr:match('^%s*([%a_][%w_]*)%s*%(()', pos)
|
||||
local pose = #expr + 1
|
||||
if tok then
|
||||
local tok2, posb = lb.match_bracketed(expr, post-1)
|
||||
assert(tok2, 'syntax error')
|
||||
if expr:match('^%s*$', posb) then
|
||||
opname = tok
|
||||
pose = posb - 1
|
||||
pos = post
|
||||
end
|
||||
end
|
||||
opname = opname or "list"
|
||||
|
||||
-- extract out expression list
|
||||
local out; out, pos = lb.match_explist(expr, pos)
|
||||
assert(out, "syntax error: missing expression list")
|
||||
out = table_concat(out, ', ')
|
||||
|
||||
-- extract "for" clauses
|
||||
local fortypes = {}
|
||||
local invarlists = {}
|
||||
local invallists = {}
|
||||
while 1 do
|
||||
local post = expr:match('^%s*for%s+()', pos)
|
||||
if not post then break end
|
||||
pos = post
|
||||
|
||||
-- extract input vars
|
||||
local iv; iv, pos = lb.match_namelist(expr, pos)
|
||||
assert(#iv > 0, 'syntax error: zero variables')
|
||||
for _,ident in ipairs(iv) do
|
||||
assert(not ident:match'^__',
|
||||
"identifier " .. ident .. " may not contain __ prefix")
|
||||
end
|
||||
invarlists[#invarlists+1] = iv
|
||||
|
||||
-- extract '=' or 'in' (optional)
|
||||
local fortype, post = expr:match('^(=)%s*()', pos)
|
||||
if not fortype then fortype, post = expr:match('^(in)%s+()', pos) end
|
||||
if fortype then
|
||||
pos = post
|
||||
-- extract input value range
|
||||
local il; il, pos = lb.match_explist(expr, pos)
|
||||
assert(#il > 0, 'syntax error: zero expressions')
|
||||
assert(fortype ~= '=' or #il == 2 or #il == 3,
|
||||
'syntax error: numeric for requires 2 or three expressions')
|
||||
fortypes[#invarlists] = fortype
|
||||
invallists[#invarlists] = il
|
||||
else
|
||||
fortypes[#invarlists] = false
|
||||
invallists[#invarlists] = false
|
||||
end
|
||||
end
|
||||
assert(#invarlists > 0, 'syntax error: missing "for" clause')
|
||||
|
||||
-- extract "if" clauses
|
||||
local preds = {}
|
||||
while 1 do
|
||||
local post = expr:match('^%s*if%s+()', pos)
|
||||
if not post then break end
|
||||
pos = post
|
||||
local pred; pred, pos = lb.match_expression(expr, pos)
|
||||
assert(pred, 'syntax error: predicated expression not found')
|
||||
preds[#preds+1] = pred
|
||||
end
|
||||
|
||||
-- extract number of parameter variables (name matching "_%d+")
|
||||
local stmp = ''; lb.gsub(expr, function(u, sin) -- strip comments/strings
|
||||
if u == 'e' then stmp = stmp .. ' ' .. sin .. ' ' end
|
||||
end)
|
||||
local max_param = 0; stmp:gsub('[%a_][%w_]*', function(s)
|
||||
local s = s:match('^_(%d+)$')
|
||||
if s then max_param = math_max(max_param, tonumber(s)) end
|
||||
end)
|
||||
|
||||
if pos ~= pose then
|
||||
assert(false, "syntax error: unrecognized " .. expr:sub(pos))
|
||||
end
|
||||
|
||||
--DEBUG:
|
||||
--print('----\n', string.format("%q", expr), string.format("%q", out), opname)
|
||||
--for k,v in ipairs(invarlists) do print(k,v, invallists[k]) end
|
||||
--for k,v in ipairs(preds) do print(k,v) end
|
||||
|
||||
return out, fortypes, invarlists, invallists, preds, opname, max_param
|
||||
end
|
||||
|
||||
|
||||
-- Create Lua code string representing comprehension.
|
||||
-- Arguments are in the form returned by parse_comprehension.
|
||||
local function code_comprehension(
|
||||
out, fortypes, invarlists, invallists, preds, opname, max_param
|
||||
)
|
||||
local op = assert(ops[opname])
|
||||
local code = op.accum:gsub('%%s', out)
|
||||
|
||||
for i=#preds,1,-1 do local pred = preds[i]
|
||||
code = ' if ' .. pred .. ' then ' .. code .. ' end '
|
||||
end
|
||||
for i=#invarlists,1,-1 do
|
||||
if not fortypes[i] then
|
||||
local arrayname = '__in' .. i
|
||||
local idx = '__idx' .. i
|
||||
code =
|
||||
' for ' .. idx .. ' = 1, #' .. arrayname .. ' do ' ..
|
||||
' local ' .. invarlists[i][1] .. ' = ' .. arrayname .. '['..idx..'] ' ..
|
||||
code .. ' end '
|
||||
else
|
||||
code =
|
||||
' for ' ..
|
||||
table_concat(invarlists[i], ', ') ..
|
||||
' ' .. fortypes[i] .. ' ' ..
|
||||
table_concat(invallists[i], ', ') ..
|
||||
' do ' .. code .. ' end '
|
||||
end
|
||||
end
|
||||
code = ' local __result = ( ' .. op.init .. ' ) ' .. code
|
||||
return code
|
||||
end
|
||||
|
||||
|
||||
-- Convert code string represented by code_comprehension
|
||||
-- into Lua function. Also must pass ninputs = #invarlists,
|
||||
-- max_param, and invallists (from parse_comprehension).
|
||||
-- Uses environment env.
|
||||
local function wrap_comprehension(code, ninputs, max_param, invallists, env)
|
||||
assert(ninputs > 0)
|
||||
local ts = {}
|
||||
for i=1,max_param do
|
||||
ts[#ts+1] = '_' .. i
|
||||
end
|
||||
for i=1,ninputs do
|
||||
if not invallists[i] then
|
||||
local name = '__in' .. i
|
||||
ts[#ts+1] = name
|
||||
end
|
||||
end
|
||||
if #ts > 0 then
|
||||
code = ' local ' .. table_concat(ts, ', ') .. ' = ... ' .. code
|
||||
end
|
||||
code = code .. ' return __result '
|
||||
--print('DEBUG:', code)
|
||||
local f, err = utils.load(code,'tmp','t',env)
|
||||
if not f then assert(false, err .. ' with generated code ' .. code) end
|
||||
return f
|
||||
end
|
||||
|
||||
|
||||
-- Build Lua function from comprehension string.
|
||||
-- Uses environment env.
|
||||
local function build_comprehension(expr, env)
|
||||
local out, fortypes, invarlists, invallists, preds, opname, max_param
|
||||
= parse_comprehension(expr)
|
||||
local code = code_comprehension(
|
||||
out, fortypes, invarlists, invallists, preds, opname, max_param)
|
||||
local f = wrap_comprehension(code, #invarlists, max_param, invallists, env)
|
||||
return f
|
||||
end
|
||||
|
||||
|
||||
-- Creates new comprehension cache.
|
||||
-- Any list comprehension function created are set to the environment
|
||||
-- env (defaults to caller of new).
|
||||
local function new(env)
|
||||
-- Note: using a single global comprehension cache would have had
|
||||
-- security implications (e.g. retrieving cached functions created
|
||||
-- in other environments).
|
||||
-- The cache lookup function could have instead been written to retrieve
|
||||
-- the caller's environment, lookup up the cache private to that
|
||||
-- environment, and then looked up the function in that cache.
|
||||
-- That would avoid the need for this <new> call to
|
||||
-- explicitly manage caches; however, that might also have an undue
|
||||
-- performance penalty.
|
||||
|
||||
if not env then
|
||||
env = getfenv(2)
|
||||
end
|
||||
|
||||
local mt = {}
|
||||
local cache = setmetatable({}, mt)
|
||||
|
||||
-- Index operator builds, caches, and returns Lua function
|
||||
-- corresponding to comprehension expression string.
|
||||
--
|
||||
-- Example: f = comprehension['x^2 for x']
|
||||
--
|
||||
function mt:__index(expr)
|
||||
local f = build_comprehension(expr, env)
|
||||
self[expr] = f -- cache
|
||||
return f
|
||||
end
|
||||
|
||||
-- Convenience syntax.
|
||||
-- Allows comprehension 'x^2 for x' instead of comprehension['x^2 for x'].
|
||||
mt.__call = mt.__index
|
||||
|
||||
cache.new = new
|
||||
|
||||
return cache
|
||||
end
|
||||
|
||||
|
||||
local comprehension = {}
|
||||
comprehension.new = new
|
||||
|
||||
return comprehension
|
||||
@@ -0,0 +1,169 @@
|
||||
--- Reads configuration files into a Lua table. <p>
|
||||
-- Understands INI files, classic Unix config files, and simple
|
||||
-- delimited columns of values. <p>
|
||||
-- <pre class=example>
|
||||
-- # test.config
|
||||
-- # Read timeout in seconds
|
||||
-- read.timeout=10
|
||||
-- # Write timeout in seconds
|
||||
-- write.timeout=5
|
||||
-- #acceptable ports
|
||||
-- ports = 1002,1003,1004
|
||||
--
|
||||
-- -- readconfig.lua
|
||||
-- require 'pl'
|
||||
-- local t = config.read 'test.config'
|
||||
-- print(pretty.write(t))
|
||||
--
|
||||
-- ### output #####
|
||||
-- {
|
||||
-- ports = {
|
||||
-- 1002,
|
||||
-- 1003,
|
||||
-- 1004
|
||||
-- },
|
||||
-- write_timeout = 5,
|
||||
-- read_timeout = 10
|
||||
-- }
|
||||
-- </pre>
|
||||
-- See the Guide for further <a href="../../index.html#config">discussion</a>
|
||||
-- @class module
|
||||
-- @name pl.config
|
||||
|
||||
local type,tonumber,ipairs,io, table = _G.type,_G.tonumber,_G.ipairs,_G.io,_G.table
|
||||
|
||||
local function split(s,re)
|
||||
local res = {}
|
||||
local t_insert = table.insert
|
||||
re = '[^'..re..']+'
|
||||
for k in s:gmatch(re) do t_insert(res,k) end
|
||||
return res
|
||||
end
|
||||
|
||||
local function strip(s)
|
||||
return s:gsub('^%s+',''):gsub('%s+$','')
|
||||
end
|
||||
|
||||
local function strip_quotes (s)
|
||||
return s:gsub("['\"](.*)['\"]",'%1')
|
||||
end
|
||||
|
||||
local config = {}
|
||||
|
||||
--- like io.lines(), but allows for lines to be continued with '\'.
|
||||
-- @param file a file-like object (anything where read() returns the next line) or a filename.
|
||||
-- Defaults to stardard input.
|
||||
-- @return an iterator over the lines, or nil
|
||||
-- @return error 'not a file-like object' or 'file is nil'
|
||||
function config.lines(file)
|
||||
local f,openf,err
|
||||
local line = ''
|
||||
if type(file) == 'string' then
|
||||
f,err = io.open(file,'r')
|
||||
if not f then return nil,err end
|
||||
openf = true
|
||||
else
|
||||
f = file or io.stdin
|
||||
if not file.read then return nil, 'not a file-like object' end
|
||||
end
|
||||
if not f then return nil, 'file is nil' end
|
||||
return function()
|
||||
local l = f:read()
|
||||
while l do
|
||||
-- does the line end with '\'?
|
||||
local i = l:find '\\%s*$'
|
||||
if i then -- if so,
|
||||
line = line..l:sub(1,i-1)
|
||||
elseif line == '' then
|
||||
return l
|
||||
else
|
||||
l = line..l
|
||||
line = ''
|
||||
return l
|
||||
end
|
||||
l = f:read()
|
||||
end
|
||||
if openf then f:close() end
|
||||
end
|
||||
end
|
||||
|
||||
--- read a configuration file into a table
|
||||
-- @param file either a file-like object or a string, which must be a filename
|
||||
-- @param cnfg a configuration table that may contain these fields:
|
||||
-- <ul>
|
||||
-- <li> variablilize make names into valid Lua identifiers (default true)</li>
|
||||
-- <li> convert_numbers try to convert values into numbers (default true)</li>
|
||||
-- <li> trim_space ensure that there is no starting or trailing whitespace with values (default true)</li>
|
||||
-- <li> trim_quotes remove quotes from strings (default false)</li>
|
||||
-- <li> list_delim delimiter to use when separating columns (default ',')</li>
|
||||
-- </ul>
|
||||
-- @return a table containing items, or nil
|
||||
-- @return error message (same as @{config.lines}
|
||||
function config.read(file,cnfg)
|
||||
local f,openf,err
|
||||
cnfg = cnfg or {}
|
||||
local function check_cnfg (var,def)
|
||||
local val = cnfg[var]
|
||||
if val == nil then return def else return val end
|
||||
end
|
||||
local t = {}
|
||||
local top_t = t
|
||||
local variablilize = check_cnfg ('variabilize',true)
|
||||
local list_delim = check_cnfg('list_delim',',')
|
||||
local convert_numbers = check_cnfg('convert_numbers',true)
|
||||
local trim_space = check_cnfg('trim_space',true)
|
||||
local trim_quotes = check_cnfg('trim_quotes',false)
|
||||
local ignore_assign = check_cnfg('ignore_assign',false)
|
||||
|
||||
local function process_name(key)
|
||||
if variablilize then
|
||||
key = key:gsub('[^%w]','_')
|
||||
end
|
||||
return key
|
||||
end
|
||||
|
||||
local function process_value(value)
|
||||
if list_delim and value:find(list_delim) then
|
||||
value = split(value,list_delim)
|
||||
for i,v in ipairs(value) do
|
||||
value[i] = process_value(v)
|
||||
end
|
||||
elseif convert_numbers and value:find('^[%d%+%-]') then
|
||||
local val = tonumber(value)
|
||||
if val then value = val end
|
||||
end
|
||||
if type(value) == 'string' then
|
||||
if trim_space then value = strip(value) end
|
||||
if trim_quotes then value = strip_quotes(value) end
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
local iter,err = config.lines(file)
|
||||
if not iter then return nil,err end
|
||||
for line in iter do
|
||||
-- strips comments
|
||||
local ci = line:find('%s*[#;]')
|
||||
if ci then line = line:sub(1,ci-1) end
|
||||
-- and ignore blank lines
|
||||
if line:find('^%s*$') then
|
||||
elseif line:find('^%[') then -- section!
|
||||
local section = process_name(line:match('%[([^%]]+)%]'))
|
||||
t = top_t
|
||||
t[section] = {}
|
||||
t = t[section]
|
||||
else
|
||||
local i1,i2 = line:find('%s*=%s*')
|
||||
if i1 and not ignore_assign then -- key,value assignment
|
||||
local key = process_name(line:sub(1,i1-1))
|
||||
local value = process_value(line:sub(i2+1))
|
||||
t[key] = value
|
||||
else -- a plain list of values...
|
||||
t[#t+1] = process_value(line)
|
||||
end
|
||||
end
|
||||
end
|
||||
return top_t
|
||||
end
|
||||
|
||||
return config
|
||||
@@ -0,0 +1,588 @@
|
||||
--- Reading and querying simple tabular data.
|
||||
-- <pre class=example>
|
||||
-- data.read 'test.txt'
|
||||
-- ==> {{10,20},{2,5},{40,50},fieldnames={'x','y'},delim=','}
|
||||
-- </pre>
|
||||
-- Provides a way of creating basic SQL-like queries.
|
||||
-- <pre class=example>
|
||||
-- require 'pl'
|
||||
-- local d = data.read('xyz.txt')
|
||||
-- local q = d:select('x,y,z where x > 3 and z < 2 sort by y')
|
||||
-- for x,y,z in q do
|
||||
-- print(x,y,z)
|
||||
-- end
|
||||
-- </pre>
|
||||
-- <p>See <a href="../../index.html#data">the Guide</a>
|
||||
-- @class module
|
||||
-- @name pl.data
|
||||
|
||||
local utils = require 'pl.utils'
|
||||
local _DEBUG = rawget(_G,'_DEBUG')
|
||||
|
||||
local patterns,function_arg,usplit = utils.patterns,utils.function_arg,utils.split
|
||||
local append,concat = table.insert,table.concat
|
||||
local gsub = string.gsub
|
||||
local io = io
|
||||
local _G,print,loadstring,type,tonumber,ipairs,setmetatable,pcall,error,setfenv = _G,print,loadstring,type,tonumber,ipairs,setmetatable,pcall,error,setfenv
|
||||
|
||||
--[[
|
||||
module ('pl.data',utils._module)
|
||||
]]
|
||||
|
||||
local data = {}
|
||||
|
||||
local parse_select
|
||||
|
||||
local function count(s,chr)
|
||||
chr = utils.escape(chr)
|
||||
local _,cnt = s:gsub(chr,' ')
|
||||
return cnt
|
||||
end
|
||||
|
||||
local function rstrip(s)
|
||||
return s:gsub('%s+$','')
|
||||
end
|
||||
|
||||
local function make_list(l)
|
||||
return setmetatable(l,utils.stdmt.List)
|
||||
end
|
||||
|
||||
local function split(s,delim)
|
||||
return make_list(usplit(s,delim))
|
||||
end
|
||||
|
||||
local function map(fun,t)
|
||||
local res = {}
|
||||
for i = 1,#t do
|
||||
append(res,fun(t[i]))
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
local function find(t,v)
|
||||
for i = 1,#t do
|
||||
if v == t[i] then return i end
|
||||
end
|
||||
end
|
||||
|
||||
local DataMT = {
|
||||
column_by_name = function(self,name)
|
||||
if type(name) == 'number' then
|
||||
name = '$'..name
|
||||
end
|
||||
local arr = {}
|
||||
for res in data.query(self,name) do
|
||||
append(arr,res)
|
||||
end
|
||||
return make_list(arr)
|
||||
end,
|
||||
|
||||
copy_select = function(self,condn)
|
||||
condn = parse_select(condn,self)
|
||||
local iter = data.query(self,condn)
|
||||
local res = {}
|
||||
local row = make_list{iter()}
|
||||
while #row > 0 do
|
||||
append(res,row)
|
||||
row = make_list{iter()}
|
||||
end
|
||||
res.delim = self.delim
|
||||
return data.new(res,split(condn.fields,','))
|
||||
end,
|
||||
|
||||
column_names = function(self)
|
||||
return self.fieldnames
|
||||
end,
|
||||
}
|
||||
DataMT.__index = DataMT
|
||||
|
||||
--- return a particular column as a list of values (Method). <br>
|
||||
-- @param name either name of column, or numerical index.
|
||||
-- @class function
|
||||
-- @name Data.column_by_name
|
||||
|
||||
--- return a query iterator on this data object (Method). <br>
|
||||
-- @param condn the query expression
|
||||
-- @class function
|
||||
-- @name Data.select
|
||||
-- @see data.query
|
||||
|
||||
--- return a new data object based on this query (Method). <br>
|
||||
-- @param condn the query expression
|
||||
-- @class function
|
||||
-- @name Data.copy_select
|
||||
|
||||
--- return the field names of this data object (Method). <br>
|
||||
-- @class function
|
||||
-- @name Data.column_names
|
||||
|
||||
--- write out a row (Method). <br>
|
||||
-- @param f file-like object
|
||||
-- @class function
|
||||
-- @name Data.write_row
|
||||
|
||||
--- write data out to file(Method). <br>
|
||||
-- @param f file-like object
|
||||
-- @class function
|
||||
-- @name Data.write
|
||||
|
||||
|
||||
-- [guessing delimiter] We check for comma, tab and spaces in that order.
|
||||
-- [issue] any other delimiters to be checked?
|
||||
local delims = {',','\t',' ',';'}
|
||||
|
||||
local function guess_delim (line)
|
||||
for _,delim in ipairs(delims) do
|
||||
if count(line,delim) > 0 then
|
||||
return delim == ' ' and '%s+' or delim
|
||||
end
|
||||
end
|
||||
return ' '
|
||||
end
|
||||
|
||||
-- [file parameter] If it's a string, we try open as a filename. If nil, then
|
||||
-- either stdin or stdout depending on the mode. Otherwise, check if this is
|
||||
-- a file-like object (implements read or write depending)
|
||||
local function open_file (f,mode)
|
||||
local opened, err
|
||||
local reading = mode == 'r'
|
||||
if type(f) == 'string' then
|
||||
if f == 'stdin' then
|
||||
f = io.stdin
|
||||
elseif f == 'stdout' then
|
||||
f = io.stdout
|
||||
else
|
||||
f,err = io.open(f,mode)
|
||||
if not f then return nil,err end
|
||||
opened = true
|
||||
end
|
||||
end
|
||||
if f and ((reading and not f.read) or (not reading and not f.write)) then
|
||||
return nil, "not a file-like object"
|
||||
end
|
||||
return f,nil,opened
|
||||
end
|
||||
|
||||
local function all_n ()
|
||||
|
||||
end
|
||||
|
||||
--- read a delimited file in a Lua table.
|
||||
-- By default, attempts to treat first line as separated list of fieldnames.
|
||||
-- @param file a filename or a file-like object (default stdin)
|
||||
-- @param cnfg options table: can override delim (a string pattern), fieldnames (a list),
|
||||
-- specify no_convert (default is to convert), numfields (indices of columns known
|
||||
-- to be numbers) and thousands_dot (thousands separator in Excel CSV is '.')
|
||||
function data.read(file,cnfg)
|
||||
local convert,err,opened
|
||||
local D = {}
|
||||
if not cnfg then cnfg = {} end
|
||||
local f,err,opened = open_file(file,'r')
|
||||
if not f then return nil, err end
|
||||
local thousands_dot = cnfg.thousands_dot
|
||||
|
||||
local function try_tonumber(x)
|
||||
if thousands_dot then x = x:gsub('%.(...)','%1') end
|
||||
return tonumber(x)
|
||||
end
|
||||
|
||||
local line = f:read()
|
||||
if not line then return nil, "empty file" end
|
||||
-- first question: what is the delimiter?
|
||||
D.delim = cnfg.delim and cnfg.delim or guess_delim(line)
|
||||
local delim = D.delim
|
||||
local collect_end = cnfg.last_field_collect
|
||||
local numfields = cnfg.numfields
|
||||
-- some space-delimited data starts with a space. This should not be a column,
|
||||
-- although it certainly would be for comma-separated, etc.
|
||||
local strip
|
||||
if delim == '%s+' and line:find(delim) == 1 then
|
||||
strip = function(s) return s:gsub('^%s+','') end
|
||||
line = strip(line)
|
||||
end
|
||||
-- first line will usually be field names. Unless fieldnames are specified,
|
||||
-- we check if it contains purely numerical values for the case of reading
|
||||
-- plain data files.
|
||||
if not cnfg.fieldnames then
|
||||
local fields = split(line,delim)
|
||||
local nums = map(tonumber,fields)
|
||||
if #nums == #fields then
|
||||
convert = tonumber
|
||||
append(D,nums)
|
||||
numfields = {}
|
||||
for i = 1,#nums do numfields[i] = i end
|
||||
else
|
||||
cnfg.fieldnames = fields
|
||||
end
|
||||
line = f:read()
|
||||
if strip then line = strip(line) end
|
||||
elseif type(cnfg.fieldnames) == 'string' then
|
||||
cnfg.fieldnames = split(cnfg.fieldnames,delim)
|
||||
end
|
||||
-- at this point, the column headers have been read in. If the first
|
||||
-- row consisted of numbers, it has already been added to the dataset.
|
||||
if cnfg.fieldnames then
|
||||
D.fieldnames = cnfg.fieldnames
|
||||
-- [conversion] unless @no_convert, we need the numerical field indices
|
||||
-- of the first data row. Can also be specified by @numfields.
|
||||
if not cnfg.no_convert then
|
||||
if not numfields then
|
||||
numfields = {}
|
||||
local fields = split(line,D.delim)
|
||||
for i = 1,#fields do
|
||||
if tonumber(fields[i]) then
|
||||
append(numfields,i)
|
||||
end
|
||||
end
|
||||
end
|
||||
if #numfields > 0 then -- there are numerical fields
|
||||
-- note that using dot as the thousands separator (@thousands_dot)
|
||||
-- requires a special conversion function!
|
||||
convert = thousands_dot and try_tonumber or tonumber
|
||||
end
|
||||
end
|
||||
end
|
||||
-- keep going until finished
|
||||
while line do
|
||||
if not line:find ('^%s*$') then
|
||||
if strip then line = strip(line) end
|
||||
local fields = split(line,delim)
|
||||
if convert then
|
||||
for k = 1,#numfields do
|
||||
local i = numfields[k]
|
||||
local val = convert(fields[i])
|
||||
if val == nil then
|
||||
return nil, "not a number: "..fields[i]
|
||||
else
|
||||
fields[i] = val
|
||||
end
|
||||
end
|
||||
end
|
||||
-- [collecting end field] If @last_field_collect then we will collect
|
||||
-- all extra space-delimited fields into a single last field.
|
||||
if collect_end and #fields > #D.fieldnames then
|
||||
local ends,N = {},#D.fieldnames
|
||||
for i = N+1,#fields do
|
||||
append(ends,fields[i])
|
||||
end
|
||||
ends = concat(ends,' ')
|
||||
local cfields = {}
|
||||
for i = 1,N do cfields[i] = fields[i] end
|
||||
cfields[N] = cfields[N]..' '..ends
|
||||
fields = cfields
|
||||
end
|
||||
append(D,fields)
|
||||
end
|
||||
line = f:read()
|
||||
end
|
||||
if opened then f:close() end
|
||||
if delim == '%s+' then D.delim = ' ' end
|
||||
if not D.fieldnames then D.fieldnames = {} end
|
||||
return data.new(D)
|
||||
end
|
||||
|
||||
local function write_row (data,f,row)
|
||||
f:write(concat(row,data.delim),'\n')
|
||||
end
|
||||
|
||||
DataMT.write_row = write_row
|
||||
|
||||
local function write (data,file)
|
||||
local f,err,opened = open_file(file,'w')
|
||||
if not f then return nil, err end
|
||||
if #data.fieldnames > 0 then
|
||||
f:write(concat(data.fieldnames,data.delim),'\n')
|
||||
end
|
||||
for i = 1,#data do
|
||||
write_row(data,f,data[i])
|
||||
end
|
||||
if opened then f:close() end
|
||||
end
|
||||
|
||||
DataMT.write = write
|
||||
|
||||
local function massage_fieldnames (fields)
|
||||
-- [fieldnames must be valid Lua identifiers] fix 0.8 was %A
|
||||
for i = 1,#fields do
|
||||
fields[i] = fields[i]:gsub('%W','_')
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--- create a new dataset from a table of rows. <br>
|
||||
-- Can specify the fieldnames, else the table must have a field called
|
||||
-- 'fieldnames', which is either a string of delimiter-separated names,
|
||||
-- or a table of names. <br>
|
||||
-- If the table does not have a field called 'delim', then an attempt will be
|
||||
-- made to guess it from the fieldnames string, defaults otherwise to tab.
|
||||
-- @param d the table.
|
||||
-- @param fieldnames optional fieldnames
|
||||
-- @return the table.
|
||||
function data.new (d,fieldnames)
|
||||
d.fieldnames = d.fieldnames or fieldnames
|
||||
if not d.delim and type(d.fieldnames) == 'string' then
|
||||
d.delim = guess_delim(d.fieldnames)
|
||||
d.fieldnames = split(d.fieldnames,d.delim)
|
||||
end
|
||||
d.fieldnames = make_list(d.fieldnames)
|
||||
massage_fieldnames(d.fieldnames)
|
||||
setmetatable(d,DataMT)
|
||||
-- a query with just the fieldname will return a sequence
|
||||
-- of values, which seq.copy turns into a table.
|
||||
return d
|
||||
end
|
||||
|
||||
local sorted_query = [[
|
||||
return function (t)
|
||||
local i = 0
|
||||
local v
|
||||
local ls = {}
|
||||
for i,v in ipairs(t) do
|
||||
if CONDITION then
|
||||
ls[#ls+1] = v
|
||||
end
|
||||
end
|
||||
table.sort(ls,function(v1,v2)
|
||||
return SORT_EXPR
|
||||
end)
|
||||
local n = #ls
|
||||
return function()
|
||||
i = i + 1
|
||||
v = ls[i]
|
||||
if i > n then return end
|
||||
return FIELDLIST
|
||||
end
|
||||
end
|
||||
]]
|
||||
|
||||
-- question: is this optimized case actually worth the extra code?
|
||||
local simple_query = [[
|
||||
return function (t)
|
||||
local n = #t
|
||||
local i = 0
|
||||
local v
|
||||
return function()
|
||||
repeat
|
||||
i = i + 1
|
||||
v = t[i]
|
||||
until i > n or CONDITION
|
||||
if i > n then return end
|
||||
return FIELDLIST
|
||||
end
|
||||
end
|
||||
]]
|
||||
|
||||
local function is_string (s)
|
||||
return type(s) == 'string'
|
||||
end
|
||||
|
||||
local field_error
|
||||
|
||||
local function fieldnames_as_string (data)
|
||||
return concat(data.fieldnames,',')
|
||||
end
|
||||
|
||||
local function massage_fields(data,f)
|
||||
local idx
|
||||
if f:find '^%d+$' then
|
||||
idx = tonumber(f)
|
||||
else
|
||||
idx = find(data.fieldnames,f)
|
||||
end
|
||||
if idx then
|
||||
return 'v['..idx..']'
|
||||
else
|
||||
field_error = f..' not found in '..fieldnames_as_string(data)
|
||||
return f
|
||||
end
|
||||
end
|
||||
|
||||
local List = require 'pl.List'
|
||||
|
||||
local function process_select (data,parms)
|
||||
--- preparing fields ----
|
||||
local res,ret
|
||||
field_error = nil
|
||||
local fields = parms.fields
|
||||
local numfields = fields:find '%$' or #data.fieldnames == 0
|
||||
if fields:find '^%s*%*%s*' then
|
||||
if not numfields then
|
||||
fields = fieldnames_as_string(data)
|
||||
else
|
||||
local ncol = #data[1]
|
||||
fields = {}
|
||||
for i = 1,ncol do append(fields,'$'..i) end
|
||||
fields = concat(fields,',')
|
||||
end
|
||||
end
|
||||
local idpat = patterns.IDEN
|
||||
if numfields then
|
||||
idpat = '%$(%d+)'
|
||||
else
|
||||
-- massage field names to replace non-identifier chars
|
||||
fields = rstrip(fields):gsub('[^,%w]','_')
|
||||
end
|
||||
local massage_fields = utils.bind1(massage_fields,data)
|
||||
ret = gsub(fields,idpat,massage_fields)
|
||||
if field_error then return nil,field_error end
|
||||
parms.fields = fields
|
||||
parms.proc_fields = ret
|
||||
parms.where = parms.where or 'true'
|
||||
if is_string(parms.where) then
|
||||
parms.where = gsub(parms.where,idpat,massage_fields)
|
||||
field_error = nil
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
parse_select = function(s,data)
|
||||
local endp
|
||||
local parms = {}
|
||||
local w1,w2 = s:find('where ')
|
||||
local s1,s2 = s:find('sort by ')
|
||||
if w1 then -- where clause!
|
||||
endp = (s1 or 0)-1
|
||||
parms.where = s:sub(w2+1,endp)
|
||||
end
|
||||
if s1 then -- sort by clause (must be last!)
|
||||
parms.sort_by = s:sub(s2+1)
|
||||
end
|
||||
endp = (w1 or s1 or 0)-1
|
||||
parms.fields = s:sub(1,endp)
|
||||
local status,err = process_select(data,parms)
|
||||
if not status then return nil,err
|
||||
else return parms end
|
||||
end
|
||||
|
||||
--- create a query iterator from a select string.
|
||||
-- Select string has this format: <br>
|
||||
-- FIELDLIST [ where LUA-CONDN [ sort by FIELD] ]<br>
|
||||
-- FIELDLIST is a comma-separated list of valid fields, or '*'. <br> <br>
|
||||
-- The condition can also be a table, with fields 'fields' (comma-sep string or
|
||||
-- table), 'sort_by' (string) and 'where' (Lua expression string or function)
|
||||
-- @param data table produced by read
|
||||
-- @param condn select string or table
|
||||
-- @param context a list of tables to be searched when resolving functions
|
||||
-- @param return_row if true, wrap the results in a row table
|
||||
-- @return an iterator over the specified fields, or nil
|
||||
-- @return an error message
|
||||
function data.query(data,condn,context,return_row)
|
||||
local err
|
||||
if is_string(condn) then
|
||||
condn,err = parse_select(condn,data)
|
||||
if not condn then return nil,err end
|
||||
elseif type(condn) == 'table' then
|
||||
if type(condn.fields) == 'table' then
|
||||
condn.fields = concat(condn.fields,',')
|
||||
end
|
||||
if not condn.proc_fields then
|
||||
local status,err = process_select(data,condn)
|
||||
if not status then return nil,err end
|
||||
end
|
||||
else
|
||||
return nil, "condition must be a string or a table"
|
||||
end
|
||||
local query, k
|
||||
if condn.sort_by then -- use sorted_query
|
||||
query = sorted_query
|
||||
else
|
||||
query = simple_query
|
||||
end
|
||||
local fields = condn.proc_fields or condn.fields
|
||||
if return_row then
|
||||
fields = '{'..fields..'}'
|
||||
end
|
||||
query,k = query:gsub('FIELDLIST',fields)
|
||||
if is_string(condn.where) then
|
||||
query = query:gsub('CONDITION',condn.where)
|
||||
condn.where = nil
|
||||
else
|
||||
query = query:gsub('CONDITION','_condn(v)')
|
||||
condn.where = function_arg(0,condn.where,'condition.where must be callable')
|
||||
end
|
||||
if condn.sort_by then
|
||||
local expr,sort_var,sort_dir
|
||||
local sort_by = condn.sort_by
|
||||
local i1,i2 = sort_by:find('%s+')
|
||||
if i1 then
|
||||
sort_var,sort_dir = sort_by:sub(1,i1-1),sort_by:sub(i2+1)
|
||||
else
|
||||
sort_var = sort_by
|
||||
sort_dir = 'asc'
|
||||
end
|
||||
if sort_var:match '^%$' then sort_var = sort_var:sub(2) end
|
||||
sort_var = massage_fields(data,sort_var)
|
||||
if field_error then return nil,field_error end
|
||||
if sort_dir == 'asc' then
|
||||
sort_dir = '<'
|
||||
else
|
||||
sort_dir = '>'
|
||||
end
|
||||
expr = ('%s %s %s'):format(sort_var:gsub('v','v1'),sort_dir,sort_var:gsub('v','v2'))
|
||||
query = query:gsub('SORT_EXPR',expr)
|
||||
end
|
||||
if condn.where then
|
||||
query = 'return function(_condn) '..query..' end'
|
||||
end
|
||||
if _DEBUG then print(query) end
|
||||
|
||||
local fn,err = loadstring(query,'tmp')
|
||||
if not fn then return nil,err end
|
||||
fn = fn() -- get the function
|
||||
if condn.where then
|
||||
fn = fn(condn.where)
|
||||
end
|
||||
local qfun = fn(data)
|
||||
if context then
|
||||
-- [specifying context for condition] @context is a list of tables which are
|
||||
-- 'injected'into the condition's custom context
|
||||
append(context,_G)
|
||||
local lookup = {}
|
||||
setfenv(qfun,lookup)
|
||||
setmetatable(lookup,{
|
||||
__index = function(tbl,key)
|
||||
-- _G.print(tbl,key)
|
||||
for k,t in ipairs(context) do
|
||||
if t[key] then return t[key] end
|
||||
end
|
||||
end
|
||||
})
|
||||
end
|
||||
return qfun
|
||||
end
|
||||
|
||||
|
||||
DataMT.select = data.query
|
||||
DataMT.select_row = function(d,condn,context)
|
||||
return data.query(d,condn,context,true)
|
||||
end
|
||||
|
||||
--- Filter input using a query.
|
||||
-- @param Q a query string
|
||||
-- @param infile filename or file-like object
|
||||
-- @param outfile filename or file-like object
|
||||
-- @param dont_fail true if you want to return an error, not just fail
|
||||
function data.filter (Q,infile,outfile,dont_fail)
|
||||
local err
|
||||
local d = data.read(infile or 'stdin')
|
||||
local out = open_file(outfile or 'stdout')
|
||||
local iter,err = d:select(Q)
|
||||
local delim = d.delim
|
||||
if not iter then
|
||||
err = 'error: '..err
|
||||
if dont_fail then
|
||||
return nil,err
|
||||
else
|
||||
utils.quit(1,err)
|
||||
end
|
||||
end
|
||||
while true do
|
||||
local res = {iter()}
|
||||
if #res == 0 then break end
|
||||
out:write(concat(res,delim),'\n')
|
||||
end
|
||||
end
|
||||
|
||||
return data
|
||||
|
||||
@@ -0,0 +1,478 @@
|
||||
--- Useful functions for getting directory contents and matching them against wildcards.
|
||||
-- @class module
|
||||
-- @name pl.dir
|
||||
|
||||
local utils = require 'pl.utils'
|
||||
local path = require 'pl.path'
|
||||
local is_windows = path.is_windows
|
||||
local tablex = require 'pl.tablex'
|
||||
local ldir = path.dir
|
||||
local chdir = path.chdir
|
||||
local mkdir = path.mkdir
|
||||
local rmdir = path.rmdir
|
||||
local sub = string.sub
|
||||
local os,pcall,ipairs,pairs,require,setmetatable,_G = os,pcall,ipairs,pairs,require,setmetatable,_G
|
||||
local remove = os.remove
|
||||
local append = table.insert
|
||||
local wrap = coroutine.wrap
|
||||
local yield = coroutine.yield
|
||||
local assert_arg,assert_string,raise = utils.assert_arg,utils.assert_string,utils.raise
|
||||
local List = utils.stdmt.List
|
||||
|
||||
--[[
|
||||
module ('pl.dir',utils._module)
|
||||
]]
|
||||
|
||||
local dir = {}
|
||||
|
||||
local function assert_dir (n,val)
|
||||
assert_arg(n,val,'string',path.isdir,'not a directory')
|
||||
end
|
||||
|
||||
local function assert_file (n,val)
|
||||
assert_arg(n,val,'string',path.isfile,'not a file')
|
||||
end
|
||||
|
||||
local function filemask(mask)
|
||||
mask = utils.escape(mask)
|
||||
return mask:gsub('%%%*','.+'):gsub('%%%?','.')..'$'
|
||||
end
|
||||
|
||||
--- does the filename match the shell pattern?.
|
||||
-- (cf. fnmatch.fnmatch in Python, 11.8)
|
||||
-- @param file A file name
|
||||
-- @param pattern A shell pattern
|
||||
-- @return true or false
|
||||
-- @raise file and pattern must be strings
|
||||
function dir.fnmatch(file,pattern)
|
||||
assert_string(1,file)
|
||||
assert_string(2,pattern)
|
||||
return path.normcase(file):find(filemask(pattern)) ~= nil
|
||||
end
|
||||
|
||||
--- return a list of all files which match the pattern.
|
||||
-- (cf. fnmatch.filter in Python, 11.8)
|
||||
-- @param files A table containing file names
|
||||
-- @param pattern A shell pattern.
|
||||
-- @return list of files
|
||||
-- @raise file and pattern must be strings
|
||||
function dir.filter(files,pattern)
|
||||
assert_arg(1,files,'table')
|
||||
assert_string(2,pattern)
|
||||
local res = {}
|
||||
local mask = filemask(pattern)
|
||||
for i,f in ipairs(files) do
|
||||
if f:find(mask) then append(res,f) end
|
||||
end
|
||||
return setmetatable(res,List)
|
||||
end
|
||||
|
||||
local function _listfiles(dir,filemode,match)
|
||||
local res = {}
|
||||
local check = utils.choose(filemode,path.isfile,path.isdir)
|
||||
if not dir then dir = '.' end
|
||||
for f in ldir(dir) do
|
||||
if f ~= '.' and f ~= '..' then
|
||||
local p = path.join(dir,f)
|
||||
if check(p) and (not match or match(p)) then
|
||||
append(res,p)
|
||||
end
|
||||
end
|
||||
end
|
||||
return setmetatable(res,List)
|
||||
end
|
||||
|
||||
--- return a list of all files in a directory which match the a shell pattern.
|
||||
-- @param dir A directory. If not given, all files in current directory are returned.
|
||||
-- @param mask A shell pattern. If not given, all files are returned.
|
||||
-- @return lsit of files
|
||||
-- @raise dir and mask must be strings
|
||||
function dir.getfiles(dir,mask)
|
||||
assert_dir(1,dir)
|
||||
assert_string(2,mask)
|
||||
local match
|
||||
if mask then
|
||||
mask = filemask(mask)
|
||||
match = function(f)
|
||||
return f:find(mask)
|
||||
end
|
||||
end
|
||||
return _listfiles(dir,true,match)
|
||||
end
|
||||
|
||||
--- return a list of all subdirectories of the directory.
|
||||
-- @param dir A directory
|
||||
-- @return a list of directories
|
||||
-- @raise dir must be a string
|
||||
function dir.getdirectories(dir)
|
||||
assert_dir(1,dir)
|
||||
return _listfiles(dir,false)
|
||||
end
|
||||
|
||||
local function quote_argument (f)
|
||||
f = path.normcase(f)
|
||||
if f:find '%s' then
|
||||
return '"'..f..'"'
|
||||
else
|
||||
return f
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local alien,ffi,ffi_checked,CopyFile,MoveFile,GetLastError,win32_errors,cmd_tmpfile
|
||||
|
||||
local function execute_command(cmd,parms)
|
||||
if not cmd_tmpfile then cmd_tmpfile = path.tmpname () end
|
||||
local err = path.is_windows and ' > ' or ' 2> '
|
||||
cmd = cmd..' '..parms..err..cmd_tmpfile
|
||||
local ret = utils.execute(cmd)
|
||||
if not ret then
|
||||
return false,(utils.readfile(cmd_tmpfile):gsub('\n(.*)',''))
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local function find_ffi_copyfile ()
|
||||
if not ffi_checked then
|
||||
ffi_checked = true
|
||||
local res
|
||||
res,alien = pcall(require,'alien')
|
||||
if not res then
|
||||
alien = nil
|
||||
res, ffi = pcall(require,'ffi')
|
||||
end
|
||||
if not res then
|
||||
ffi = nil
|
||||
return
|
||||
end
|
||||
else
|
||||
return
|
||||
end
|
||||
if alien then
|
||||
-- register the Win32 CopyFile and MoveFile functions
|
||||
local kernel = alien.load('kernel32.dll')
|
||||
CopyFile = kernel.CopyFileA
|
||||
CopyFile:types{'string','string','int',ret='int',abi='stdcall'}
|
||||
MoveFile = kernel.MoveFileA
|
||||
MoveFile:types{'string','string',ret='int',abi='stdcall'}
|
||||
GetLastError = kernel.GetLastError
|
||||
GetLastError:types{ret ='int', abi='stdcall'}
|
||||
elseif ffi then
|
||||
ffi.cdef [[
|
||||
int CopyFileA(const char *src, const char *dest, int iovr);
|
||||
int MoveFileA(const char *src, const char *dest);
|
||||
int GetLastError();
|
||||
]]
|
||||
CopyFile = ffi.C.CopyFileA
|
||||
MoveFile = ffi.C.MoveFileA
|
||||
GetLastError = ffi.C.GetLastError
|
||||
end
|
||||
win32_errors = {
|
||||
ERROR_FILE_NOT_FOUND = 2,
|
||||
ERROR_PATH_NOT_FOUND = 3,
|
||||
ERROR_ACCESS_DENIED = 5,
|
||||
ERROR_WRITE_PROTECT = 19,
|
||||
ERROR_BAD_UNIT = 20,
|
||||
ERROR_NOT_READY = 21,
|
||||
ERROR_WRITE_FAULT = 29,
|
||||
ERROR_READ_FAULT = 30,
|
||||
ERROR_SHARING_VIOLATION = 32,
|
||||
ERROR_LOCK_VIOLATION = 33,
|
||||
ERROR_HANDLE_DISK_FULL = 39,
|
||||
ERROR_BAD_NETPATH = 53,
|
||||
ERROR_NETWORK_BUSY = 54,
|
||||
ERROR_DEV_NOT_EXIST = 55,
|
||||
ERROR_FILE_EXISTS = 80,
|
||||
ERROR_OPEN_FAILED = 110,
|
||||
ERROR_INVALID_NAME = 123,
|
||||
ERROR_BAD_PATHNAME = 161,
|
||||
ERROR_ALREADY_EXISTS = 183,
|
||||
}
|
||||
end
|
||||
|
||||
local function two_arguments (f1,f2)
|
||||
return quote_argument(f1)..' '..quote_argument(f2)
|
||||
end
|
||||
|
||||
local function file_op (is_copy,src,dest,flag)
|
||||
if flag == 1 and path.exists(dest) then
|
||||
return false,"cannot overwrite destination"
|
||||
end
|
||||
if is_windows then
|
||||
-- if we haven't tried to load Alien/LuaJIT FFI before, then do so
|
||||
find_ffi_copyfile()
|
||||
-- fallback if there's no Alien, just use DOS commands *shudder*
|
||||
-- 'rename' involves a copy and then deleting the source.
|
||||
if not CopyFile then
|
||||
src = path.normcase(src)
|
||||
dest = path.normcase(dest)
|
||||
local cmd = is_copy and 'copy' or 'rename'
|
||||
local res, err = execute_command('copy',two_arguments(src,dest))
|
||||
if not res then return nil,err end
|
||||
if not is_copy then
|
||||
return execute_command('del',quote_argument(src))
|
||||
end
|
||||
else
|
||||
if path.isdir(dest) then
|
||||
dest = path.join(dest,path.basename(src))
|
||||
end
|
||||
local ret
|
||||
if is_copy then ret = CopyFile(src,dest,flag)
|
||||
else ret = MoveFile(src,dest) end
|
||||
if ret == 0 then
|
||||
local err = GetLastError()
|
||||
for name,value in pairs(win32_errors) do
|
||||
if value == err then return false,name end
|
||||
end
|
||||
return false,"Error #"..err
|
||||
else return true
|
||||
end
|
||||
end
|
||||
else -- for Unix, just use cp for now
|
||||
return execute_command(is_copy and 'cp' or 'mv',
|
||||
two_arguments(src,dest))
|
||||
end
|
||||
end
|
||||
|
||||
--- copy a file.
|
||||
-- @param src source file
|
||||
-- @param dest destination file or directory
|
||||
-- @param flag true if you want to force the copy (default)
|
||||
-- @return true if operation succeeded
|
||||
-- @raise src and dest must be strings
|
||||
function dir.copyfile (src,dest,flag)
|
||||
assert_string(1,src)
|
||||
assert_string(2,dest)
|
||||
flag = flag==nil or flag
|
||||
return file_op(true,src,dest,flag and 0 or 1)
|
||||
end
|
||||
|
||||
--- move a file.
|
||||
-- @param src source file
|
||||
-- @param dest destination file or directory
|
||||
-- @return true if operation succeeded
|
||||
-- @raise src and dest must be strings
|
||||
function dir.movefile (src,dest)
|
||||
assert_string(1,src)
|
||||
assert_string(2,dest)
|
||||
return file_op(false,src,dest,0)
|
||||
end
|
||||
|
||||
local function _dirfiles(dir,attrib)
|
||||
local dirs = {}
|
||||
local files = {}
|
||||
for f in ldir(dir) do
|
||||
if f ~= '.' and f ~= '..' then
|
||||
local p = path.join(dir,f)
|
||||
local mode = attrib(p,'mode')
|
||||
if mode=='directory' then
|
||||
append(dirs,f)
|
||||
else
|
||||
append(files,f)
|
||||
end
|
||||
end
|
||||
end
|
||||
return setmetatable(dirs,List),setmetatable(files,List)
|
||||
end
|
||||
|
||||
|
||||
local function _walker(root,bottom_up,attrib)
|
||||
local dirs,files = _dirfiles(root,attrib)
|
||||
if not bottom_up then yield(root,dirs,files) end
|
||||
for i,d in ipairs(dirs) do
|
||||
_walker(root..path.sep..d,bottom_up,attrib)
|
||||
end
|
||||
if bottom_up then yield(root,dirs,files) end
|
||||
end
|
||||
|
||||
--- return an iterator which walks through a directory tree starting at root.
|
||||
-- The iterator returns (root,dirs,files)
|
||||
-- Note that dirs and files are lists of names (i.e. you must say path.join(root,d)
|
||||
-- to get the actual full path)
|
||||
-- If bottom_up is false (or not present), then the entries at the current level are returned
|
||||
-- before we go deeper. This means that you can modify the returned list of directories before
|
||||
-- continuing.
|
||||
-- This is a clone of os.walk from the Python libraries.
|
||||
-- @param root A starting directory
|
||||
-- @param bottom_up False if we start listing entries immediately.
|
||||
-- @param follow_links follow symbolic links
|
||||
-- @return an iterator returning root,dirs,files
|
||||
-- @raise root must be a string
|
||||
function dir.walk(root,bottom_up,follow_links)
|
||||
assert_string(1,root)
|
||||
if not path.isdir(root) then return raise 'not a directory' end
|
||||
local attrib
|
||||
if path.is_windows or not follow_links then
|
||||
attrib = path.attrib
|
||||
else
|
||||
attrib = path.link_attrib
|
||||
end
|
||||
return wrap(function () _walker(root,bottom_up,attrib) end)
|
||||
end
|
||||
|
||||
--- remove a whole directory tree.
|
||||
-- @param fullpath A directory path
|
||||
-- @return true or nil
|
||||
-- @return error if failed
|
||||
-- @raise fullpath must be a string
|
||||
function dir.rmtree(fullpath)
|
||||
assert_string(1,fullpath)
|
||||
if not path.isdir(fullpath) then return raise 'not a directory' end
|
||||
if path.islink(fullpath) then return false,'will not follow symlink' end
|
||||
for root,dirs,files in dir.walk(fullpath,true) do
|
||||
for i,f in ipairs(files) do
|
||||
remove(path.join(root,f))
|
||||
end
|
||||
rmdir(root)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local dirpat
|
||||
if path.is_windows then
|
||||
dirpat = '(.+)\\[^\\]+$'
|
||||
else
|
||||
dirpat = '(.+)/[^/]+$'
|
||||
end
|
||||
|
||||
local _makepath
|
||||
function _makepath(p)
|
||||
-- windows root drive case
|
||||
if p:find '^%a:[\\]*$' then
|
||||
return true
|
||||
end
|
||||
if not path.isdir(p) then
|
||||
local subp = p:match(dirpat)
|
||||
if not _makepath(subp) then return raise ('cannot create '..subp) end
|
||||
return mkdir(p)
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
--- create a directory path.
|
||||
-- This will create subdirectories as necessary!
|
||||
-- @param p A directory path
|
||||
-- @return a valid created path
|
||||
-- @raise p must be a string
|
||||
function dir.makepath (p)
|
||||
assert_string(1,p)
|
||||
return _makepath(path.normcase(path.abspath(p)))
|
||||
end
|
||||
|
||||
|
||||
--- clone a directory tree. Will always try to create a new directory structure
|
||||
-- if necessary.
|
||||
-- @param path1 the base path of the source tree
|
||||
-- @param path2 the new base path for the destination
|
||||
-- @param file_fun an optional function to apply on all files
|
||||
-- @param verbose an optional boolean to control the verbosity of the output.
|
||||
-- It can also be a logging function that behaves like print()
|
||||
-- @return true, or nil
|
||||
-- @return error message, or list of failed directory creations
|
||||
-- @return list of failed file operations
|
||||
-- @raise path1 and path2 must be strings
|
||||
-- @usage clonetree('.','../backup',copyfile)
|
||||
function dir.clonetree (path1,path2,file_fun,verbose)
|
||||
assert_string(1,path1)
|
||||
assert_string(2,path2)
|
||||
if verbose == true then verbose = print end
|
||||
local abspath,normcase,isdir,join = path.abspath,path.normcase,path.isdir,path.join
|
||||
local faildirs,failfiles = {},{}
|
||||
if not isdir(path1) then return raise 'source is not a valid directory' end
|
||||
path1 = abspath(normcase(path1))
|
||||
path2 = abspath(normcase(path2))
|
||||
if verbose then verbose('normalized:',path1,path2) end
|
||||
-- particularly NB that the new path isn't fully contained in the old path
|
||||
if path1 == path2 then return raise "paths are the same" end
|
||||
local i1,i2 = path2:find(path1,1,true)
|
||||
if i2 == #path1 and path2:sub(i2+1,i2+1) == path.sep then
|
||||
return raise 'destination is a subdirectory of the source'
|
||||
end
|
||||
local cp = path.common_prefix (path1,path2)
|
||||
local idx = #cp
|
||||
if idx == 0 then -- no common path, but watch out for Windows paths!
|
||||
if path1:sub(2,2) == ':' then idx = 3 end
|
||||
end
|
||||
for root,dirs,files in dir.walk(path1) do
|
||||
local opath = path2..root:sub(idx)
|
||||
if verbose then verbose('paths:',opath,root) end
|
||||
if not isdir(opath) then
|
||||
local ret = dir.makepath(opath)
|
||||
if not ret then append(faildirs,opath) end
|
||||
if verbose then verbose('creating:',opath,ret) end
|
||||
end
|
||||
if file_fun then
|
||||
for i,f in ipairs(files) do
|
||||
local p1 = join(root,f)
|
||||
local p2 = join(opath,f)
|
||||
local ret = file_fun(p1,p2)
|
||||
if not ret then append(failfiles,p2) end
|
||||
if verbose then
|
||||
verbose('files:',p1,p2,ret)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return true,faildirs,failfiles
|
||||
end
|
||||
|
||||
--- return an iterator over all entries in a directory tree
|
||||
-- @param d a directory
|
||||
-- @return an iterator giving pathname and mode (true for dir, false otherwise)
|
||||
-- @raise d must be a non-empty string
|
||||
function dir.dirtree( d )
|
||||
assert( d and d ~= "", "directory parameter is missing or empty" )
|
||||
local exists, isdir = path.exists, path.isdir
|
||||
local sep = path.sep
|
||||
|
||||
local last = sub ( d, -1 )
|
||||
if last == sep or last == '/' then
|
||||
d = sub( d, 1, -2 )
|
||||
end
|
||||
|
||||
local function yieldtree( dir )
|
||||
for entry in ldir( dir ) do
|
||||
if entry ~= "." and entry ~= ".." then
|
||||
entry = dir .. sep .. entry
|
||||
if exists(entry) then -- Just in case a symlink is broken.
|
||||
local is_dir = isdir(entry)
|
||||
yield( entry, is_dir )
|
||||
if is_dir then
|
||||
yieldtree( entry )
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return wrap( function() yieldtree( d ) end )
|
||||
end
|
||||
|
||||
|
||||
--- Recursively returns all the file starting at <i>path</i>. It can optionally take a shell pattern and
|
||||
-- only returns files that match <i>pattern</i>. If a pattern is given it will do a case insensitive search.
|
||||
-- @param start_path {string} A directory. If not given, all files in current directory are returned.
|
||||
-- @param pattern {string} A shell pattern. If not given, all files are returned.
|
||||
-- @return Table containing all the files found recursively starting at <i>path</i> and filtered by <i>pattern</i>.
|
||||
-- @raise start_path must be a string
|
||||
function dir.getallfiles( start_path, pattern )
|
||||
assert( type( start_path ) == "string", "bad argument #1 to 'GetAllFiles' (Expected string but recieved " .. type( start_path ) .. ")" )
|
||||
pattern = pattern or ""
|
||||
|
||||
local files = {}
|
||||
local normcase = path.normcase
|
||||
for filename, mode in dir.dirtree( start_path ) do
|
||||
if not mode then
|
||||
local mask = filemask( pattern )
|
||||
if normcase(filename):find( mask ) then
|
||||
files[#files + 1] = filename
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return files
|
||||
end
|
||||
|
||||
return dir
|
||||
@@ -0,0 +1,69 @@
|
||||
--- File manipulation functions: reading, writing, moving and copying.
|
||||
-- @class module
|
||||
-- @name pl.file
|
||||
local os = os
|
||||
local utils = require 'pl.utils'
|
||||
local dir = require 'pl.dir'
|
||||
local path = require 'pl.path'
|
||||
|
||||
--[[
|
||||
module ('pl.file',utils._module)
|
||||
]]
|
||||
local file = {}
|
||||
|
||||
--- return the contents of a file as a string
|
||||
-- @class function
|
||||
-- @name file.read
|
||||
-- @param filename The file path
|
||||
-- @return file contents
|
||||
file.read = utils.readfile
|
||||
|
||||
--- write a string to a file
|
||||
-- @class function
|
||||
-- @name file.write
|
||||
-- @param filename The file path
|
||||
-- @param str The string
|
||||
file.write = utils.writefile
|
||||
|
||||
--- copy a file.
|
||||
-- @class function
|
||||
-- @name file.copy
|
||||
-- @param src source file
|
||||
-- @param dest destination file
|
||||
-- @param flag true if you want to force the copy (default)
|
||||
-- @return true if operation succeeded
|
||||
file.copy = dir.copyfile
|
||||
|
||||
--- move a file.
|
||||
-- @class function
|
||||
-- @name file.move
|
||||
-- @param src source file
|
||||
-- @param dest destination file
|
||||
-- @return true if operation succeeded, else false and the reason for the error.
|
||||
file.move = dir.movefile
|
||||
|
||||
--- Return the time of last access as the number of seconds since the epoch.
|
||||
-- @class function
|
||||
-- @name file.access_time
|
||||
-- @param path A file path
|
||||
file.access_time = path.getatime
|
||||
|
||||
---Return when the file was created.
|
||||
-- @class function
|
||||
-- @name file.creation_time
|
||||
-- @param path A file path
|
||||
file.creation_time = path.getctime
|
||||
|
||||
--- Return the time of last modification
|
||||
-- @class function
|
||||
-- @name file.modified_time
|
||||
-- @param path A file path
|
||||
file.modified_time = path.getmtime
|
||||
|
||||
--- Delete a file
|
||||
-- @class function
|
||||
-- @name file.delete
|
||||
-- @param path A file path
|
||||
file.delete = os.remove
|
||||
|
||||
return file
|
||||
@@ -0,0 +1,379 @@
|
||||
--- Functional helpers like composition, binding and placeholder expressions.
|
||||
-- Placeholder expressions are useful for short anonymous functions, and were
|
||||
-- inspired by the Boost Lambda library.
|
||||
-- <pre class=example>
|
||||
-- utils.import 'pl.func'
|
||||
-- ls = List{10,20,30}
|
||||
-- = ls:map(_1+1)
|
||||
-- {11,21,31}
|
||||
-- </pre>
|
||||
-- They can also be used to <em>bind</em> particular arguments of a function.
|
||||
-- <pre class = example>
|
||||
-- p = bind(print,'start>',_0)
|
||||
-- p(10,20,30)
|
||||
-- start> 10 20 30
|
||||
-- </pre>
|
||||
-- See <a href="../../index.html#func">the Guide</a>
|
||||
-- @class module
|
||||
-- @name pl.func
|
||||
local type,select,setmetatable,getmetatable,rawset = type,select,setmetatable,getmetatable,rawset
|
||||
local concat,append = table.concat,table.insert
|
||||
local max = math.max
|
||||
local print,tostring = print,tostring
|
||||
local pairs,ipairs,loadstring,rawget,unpack = pairs,ipairs,loadstring,rawget,unpack
|
||||
local _G = _G
|
||||
local utils = require 'pl.utils'
|
||||
local tablex = require 'pl.tablex'
|
||||
local map = tablex.map
|
||||
local _DEBUG = rawget(_G,'_DEBUG')
|
||||
local assert_arg = utils.assert_arg
|
||||
|
||||
--[[
|
||||
module ('pl.func',utils._module)
|
||||
]]
|
||||
|
||||
local func = {}
|
||||
|
||||
-- metatable for Placeholder Expressions (PE)
|
||||
local _PEMT = {}
|
||||
|
||||
local function P (t)
|
||||
setmetatable(t,_PEMT)
|
||||
return t
|
||||
end
|
||||
|
||||
func.PE = P
|
||||
|
||||
local function isPE (obj)
|
||||
return getmetatable(obj) == _PEMT
|
||||
end
|
||||
|
||||
func.isPE = isPE
|
||||
|
||||
-- construct a placeholder variable (e.g _1 and _2)
|
||||
local function PH (idx)
|
||||
return P {op='X',repr='_'..idx, index=idx}
|
||||
end
|
||||
|
||||
-- construct a constant placeholder variable (e.g _C1 and _C2)
|
||||
local function CPH (idx)
|
||||
return P {op='X',repr='_C'..idx, index=idx}
|
||||
end
|
||||
|
||||
func._1,func._2,func._3,func._4,func._5 = PH(1),PH(2),PH(3),PH(4),PH(5)
|
||||
func._0 = P{op='X',repr='...',index=0}
|
||||
|
||||
function func.Var (name)
|
||||
local ls = utils.split(name,'[%s,]+')
|
||||
local res = {}
|
||||
for _,n in ipairs(ls) do
|
||||
append(res,P{op='X',repr=n,index=0})
|
||||
end
|
||||
return unpack(res)
|
||||
end
|
||||
|
||||
function func._ (value)
|
||||
return P{op='X',repr=value,index='wrap'}
|
||||
end
|
||||
|
||||
local repr
|
||||
|
||||
func.Nil = func.Var 'nil'
|
||||
|
||||
function _PEMT.__index(obj,key)
|
||||
return P{op='[]',obj,key}
|
||||
end
|
||||
|
||||
function _PEMT.__call(fun,...)
|
||||
return P{op='()',fun,...}
|
||||
end
|
||||
|
||||
function _PEMT.__tostring (e)
|
||||
return repr(e)
|
||||
end
|
||||
|
||||
function _PEMT.__unm(arg)
|
||||
return P{op='-',arg}
|
||||
end
|
||||
|
||||
function func.Not (arg)
|
||||
return P{op='not',arg}
|
||||
end
|
||||
|
||||
function func.Len (arg)
|
||||
return P{op='#',arg}
|
||||
end
|
||||
|
||||
|
||||
local function binreg(context,t)
|
||||
for name,op in pairs(t) do
|
||||
rawset(context,name,function(x,y)
|
||||
return P{op=op,x,y}
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
local function import_name (name,fun,context)
|
||||
rawset(context,name,function(...)
|
||||
return P{op='()',fun,...}
|
||||
end)
|
||||
end
|
||||
|
||||
local imported_functions = {}
|
||||
|
||||
local function is_global_table (n)
|
||||
return type(_G[n]) == 'table'
|
||||
end
|
||||
|
||||
--- wrap a table of functions. This makes them available for use in
|
||||
-- placeholder expressions.
|
||||
-- @param tname a table name
|
||||
-- @param context context to put results, defaults to environment of caller
|
||||
function func.import(tname,context)
|
||||
assert_arg(1,tname,'string',is_global_table,'arg# 1: not a name of a global table')
|
||||
local t = _G[tname]
|
||||
context = context or _G
|
||||
for name,fun in pairs(t) do
|
||||
import_name(name,fun,context)
|
||||
imported_functions[fun] = name
|
||||
end
|
||||
end
|
||||
|
||||
--- register a function for use in placeholder expressions.
|
||||
-- @param fun a function
|
||||
-- @param name an optional name
|
||||
-- @return a placeholder functiond
|
||||
function func.register (fun,name)
|
||||
assert_arg(1,fun,'function')
|
||||
if name then
|
||||
assert_arg(2,name,'string')
|
||||
imported_functions[fun] = name
|
||||
end
|
||||
return function(...)
|
||||
return P{op='()',fun,...}
|
||||
end
|
||||
end
|
||||
|
||||
function func.lookup_imported_name (fun)
|
||||
return imported_functions[fun]
|
||||
end
|
||||
|
||||
local function _arg(...) return ... end
|
||||
|
||||
function func.Args (...)
|
||||
return P{op='()',_arg,...}
|
||||
end
|
||||
|
||||
-- binary and unary operators, with their precedences (see 2.5.6)
|
||||
local operators = {
|
||||
['or'] = 0,
|
||||
['and'] = 1,
|
||||
['=='] = 2, ['~='] = 2, ['<'] = 2, ['>'] = 2, ['<='] = 2, ['>='] = 2,
|
||||
['..'] = 3,
|
||||
['+'] = 4, ['-'] = 4,
|
||||
['*'] = 5, ['/'] = 5, ['%'] = 5,
|
||||
['not'] = 6, ['#'] = 6, ['-'] = 6,
|
||||
['^'] = 7
|
||||
}
|
||||
|
||||
-- comparisons (as prefix functions)
|
||||
binreg (func,{And='and',Or='or',Eq='==',Lt='<',Gt='>',Le='<=',Ge='>='})
|
||||
|
||||
-- standard binary operators (as metamethods)
|
||||
binreg (_PEMT,{__add='+',__sub='-',__mul='*',__div='/',__mod='%',__pow='^',__concat='..'})
|
||||
|
||||
binreg (_PEMT,{__eq='=='})
|
||||
|
||||
--- all elements of a table except the first.
|
||||
-- @param ls a list-like table.
|
||||
function func.tail (ls)
|
||||
assert_arg(1,ls,'table')
|
||||
local res = {}
|
||||
for i = 2,#ls do
|
||||
append(res,ls[i])
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
--- create a string representation of a placeholder expression.
|
||||
-- @param e a placeholder expression
|
||||
-- @param lastpred not used
|
||||
function repr (e,lastpred)
|
||||
local tail = func.tail
|
||||
if isPE(e) then
|
||||
local pred = operators[e.op]
|
||||
local ls = map(repr,e,pred)
|
||||
if pred then --unary or binary operator
|
||||
if #ls ~= 1 then
|
||||
local s = concat(ls,' '..e.op..' ')
|
||||
if lastpred and lastpred > pred then
|
||||
s = '('..s..')'
|
||||
end
|
||||
return s
|
||||
else
|
||||
return e.op..' '..ls[1]
|
||||
end
|
||||
else -- either postfix, or a placeholder
|
||||
if e.op == '[]' then
|
||||
return ls[1]..'['..ls[2]..']'
|
||||
elseif e.op == '()' then
|
||||
local fn
|
||||
if ls[1] ~= nil then -- was _args, undeclared!
|
||||
fn = ls[1]
|
||||
else
|
||||
fn = ''
|
||||
end
|
||||
return fn..'('..concat(tail(ls),',')..')'
|
||||
else
|
||||
return e.repr
|
||||
end
|
||||
end
|
||||
elseif type(e) == 'string' then
|
||||
return '"'..e..'"'
|
||||
elseif type(e) == 'function' then
|
||||
local name = func.lookup_imported_name(e)
|
||||
if name then return name else return tostring(e) end
|
||||
else
|
||||
return tostring(e) --should not really get here!
|
||||
end
|
||||
end
|
||||
func.repr = repr
|
||||
|
||||
-- collect all the non-PE values in this PE into vlist, and replace each occurence
|
||||
-- with a constant PH (_C1, etc). Return the maximum placeholder index found.
|
||||
local collect_values
|
||||
function collect_values (e,vlist)
|
||||
if isPE(e) then
|
||||
if e.op ~= 'X' then
|
||||
local m = 0
|
||||
for i,subx in ipairs(e) do
|
||||
local pe = isPE(subx)
|
||||
if pe then
|
||||
if subx.op == 'X' and subx.index == 'wrap' then
|
||||
subx = subx.repr
|
||||
pe = false
|
||||
else
|
||||
m = max(m,collect_values(subx,vlist))
|
||||
end
|
||||
end
|
||||
if not pe then
|
||||
append(vlist,subx)
|
||||
e[i] = CPH(#vlist)
|
||||
end
|
||||
end
|
||||
return m
|
||||
else -- was a placeholder, it has an index...
|
||||
return e.index
|
||||
end
|
||||
else -- plain value has no placeholder dependence
|
||||
return 0
|
||||
end
|
||||
end
|
||||
func.collect_values = collect_values
|
||||
|
||||
--- instantiate a PE into an actual function. First we find the largest placeholder used,
|
||||
-- e.g. _2; from this a list of the formal parameters can be build. Then we collect and replace
|
||||
-- any non-PE values from the PE, and build up a constant binding list.
|
||||
-- Finally, the expression can be compiled, and e.__PE_function is set.
|
||||
-- @param e a placeholder expression
|
||||
-- @return a function
|
||||
function func.instantiate (e)
|
||||
local consts,values,parms = {},{},{}
|
||||
local rep, err, fun
|
||||
local n = func.collect_values(e,values)
|
||||
for i = 1,#values do
|
||||
append(consts,'_C'..i)
|
||||
if _DEBUG then print(i,values[i]) end
|
||||
end
|
||||
for i =1,n do
|
||||
append(parms,'_'..i)
|
||||
end
|
||||
consts = concat(consts,',')
|
||||
parms = concat(parms,',')
|
||||
rep = repr(e)
|
||||
local fstr = ('return function(%s) return function(%s) return %s end end'):format(consts,parms,rep)
|
||||
if _DEBUG then print(fstr) end
|
||||
fun,err = loadstring(fstr,'fun')
|
||||
if not fun then return nil,err end
|
||||
fun = fun() -- get wrapper
|
||||
fun = fun(unpack(values)) -- call wrapper (values could be empty)
|
||||
e.__PE_function = fun
|
||||
return fun
|
||||
end
|
||||
|
||||
--- instantiate a PE unless it has already been done.
|
||||
-- @param e a placeholder expression
|
||||
-- @return the function
|
||||
function func.I(e)
|
||||
if rawget(e,'__PE_function') then
|
||||
return e.__PE_function
|
||||
else return func.instantiate(e)
|
||||
end
|
||||
end
|
||||
|
||||
utils.add_function_factory(_PEMT,func.I)
|
||||
|
||||
--- bind the first parameter of the function to a value.
|
||||
-- @class function
|
||||
-- @name func.curry
|
||||
-- @param fn a function of one or more arguments
|
||||
-- @param p a value
|
||||
-- @return a function of one less argument
|
||||
-- @usage (curry(math.max,10))(20) == math.max(10,20)
|
||||
func.curry = utils.bind1
|
||||
|
||||
--- create a function which chains two functions.
|
||||
-- @param f a function of at least one argument
|
||||
-- @param g a function of at least one argument
|
||||
-- @return a function
|
||||
-- @usage printf = compose(io.write,string.format)
|
||||
function func.compose (f,g)
|
||||
return function(...) return f(g(...)) end
|
||||
end
|
||||
|
||||
--- bind the arguments of a function to given values.
|
||||
-- bind(fn,v,_2) is equivalent to curry(fn,v).
|
||||
-- @param fn a function of at least one argument
|
||||
-- @param ... values or placeholder variables
|
||||
-- @return a function
|
||||
-- @usage (bind(f,_1,a))(b) == f(a,b)
|
||||
-- @usage (bind(f,_2,_1))(a,b) == f(b,a)
|
||||
function func.bind(fn,...)
|
||||
local args = table.pack(...)
|
||||
local holders,parms,bvalues,values = {},{},{'fn'},{}
|
||||
local nv,maxplace,varargs = 1,0,false
|
||||
for i = 1,args.n do
|
||||
local a = args[i]
|
||||
if isPE(a) and a.op == 'X' then
|
||||
append(holders,a.repr)
|
||||
maxplace = max(maxplace,a.index)
|
||||
if a.index == 0 then varargs = true end
|
||||
else
|
||||
local v = '_v'..nv
|
||||
append(bvalues,v)
|
||||
append(holders,v)
|
||||
append(values,a)
|
||||
nv = nv + 1
|
||||
end
|
||||
end
|
||||
for np = 1,maxplace do
|
||||
append(parms,'_'..np)
|
||||
end
|
||||
if varargs then append(parms,'...') end
|
||||
bvalues = concat(bvalues,',')
|
||||
parms = concat(parms,',')
|
||||
holders = concat(holders,',')
|
||||
local fstr = ([[
|
||||
return function (%s)
|
||||
return function(%s) return fn(%s) end
|
||||
end
|
||||
]]):format(bvalues,parms,holders)
|
||||
if _DEBUG then print(fstr) end
|
||||
local res,err = loadstring(fstr)
|
||||
res = res()
|
||||
return res(fn,unpack(values))
|
||||
end
|
||||
|
||||
return func
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
--------------
|
||||
-- entry point for loading all PL libraries only on demand.
|
||||
-- Requiring 'pl' means that whenever a module is accesssed (e.g. utils.split)
|
||||
-- then that module is dynamically loaded. The submodules are all brought into
|
||||
-- the global space.
|
||||
-- @class module
|
||||
-- @name pl
|
||||
|
||||
local modules = {
|
||||
utils = true,path=true,dir=true,tablex=true,stringio=true,sip=true,
|
||||
input=true,seq=true,lexer=true,stringx=true,
|
||||
config=true,pretty=true,data=true,func=true,text=true,
|
||||
operator=true,lapp=true,array2d=true,
|
||||
comprehension=true,xml=true,
|
||||
test = true, app = true, file = true, class = true, List = true,
|
||||
Map = true, Set = true, OrderedMap = true, MultiMap = true,
|
||||
Date = true,
|
||||
-- classes --
|
||||
}
|
||||
_G.utils = require 'pl.utils'
|
||||
|
||||
for name,klass in pairs(_G.utils.stdmt) do
|
||||
klass.__index = function(t,key)
|
||||
return require ('pl.'..name)[key]
|
||||
end;
|
||||
end
|
||||
|
||||
local _hook
|
||||
setmetatable(_G,{
|
||||
hook = function(handler)
|
||||
_hook = handler
|
||||
end,
|
||||
__index = function(t,name)
|
||||
local found = modules[name]
|
||||
-- either true, or the name of the module containing this class.
|
||||
-- either way, we load the required module and make it globally available.
|
||||
if found then
|
||||
-- e..g pretty.dump causes pl.pretty to become available as 'pretty'
|
||||
rawset(_G,name,require('pl.'..name))
|
||||
return _G[name]
|
||||
elseif _hook then
|
||||
return _hook(t,name)
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
if _G.PENLIGHT_STRICT then require 'pl.strict' end
|
||||
@@ -0,0 +1,172 @@
|
||||
--- Iterators for extracting words or numbers from an input source.
|
||||
-- <pre class=example>
|
||||
-- require 'pl'
|
||||
-- local total,n = <a href="pl.seq.html#sum">seq.sum</a>(input.numbers())
|
||||
-- print('average',total/n)
|
||||
-- </pre>
|
||||
-- <p> See <a href="../../index.html#lexer">here</a>
|
||||
-- @class module
|
||||
-- @name pl.input
|
||||
local strfind = string.find
|
||||
local strsub = string.sub
|
||||
local strmatch = string.match
|
||||
local pairs,type,unpack,tonumber = pairs,type,unpack,tonumber
|
||||
local utils = require 'pl.utils'
|
||||
local patterns = utils.patterns
|
||||
local io = io
|
||||
local assert_arg = utils.assert_arg
|
||||
|
||||
--[[
|
||||
module ('pl.input',utils._module)
|
||||
]]
|
||||
|
||||
local input = {}
|
||||
|
||||
--- create an iterator over all tokens.
|
||||
-- based on allwords from PiL, 7.1
|
||||
-- @param getter any function that returns a line of text
|
||||
-- @param pattern
|
||||
-- @param fn Optionally can pass a function to process each token as it/s found.
|
||||
-- @return an iterator
|
||||
function input.alltokens (getter,pattern,fn)
|
||||
local line = getter() -- current line
|
||||
local pos = 1 -- current position in the line
|
||||
return function () -- iterator function
|
||||
while line do -- repeat while there are lines
|
||||
local s, e = strfind(line, pattern, pos)
|
||||
if s then -- found a word?
|
||||
pos = e + 1 -- next position is after this token
|
||||
local res = strsub(line, s, e) -- return the token
|
||||
if fn then res = fn(res) end
|
||||
return res
|
||||
else
|
||||
line = getter() -- token not found; try next line
|
||||
pos = 1 -- restart from first position
|
||||
end
|
||||
end
|
||||
return nil -- no more lines: end of traversal
|
||||
end
|
||||
end
|
||||
local alltokens = input.alltokens
|
||||
|
||||
-- question: shd this _split_ a string containing line feeds?
|
||||
|
||||
--- create a function which grabs the next value from a source. If the source is a string, then the getter
|
||||
-- will return the string and thereafter return nil. If not specified then the source is assumed to be stdin.
|
||||
-- @param f a string or a file-like object (i.e. has a read() method which returns the next line)
|
||||
-- @return a getter function
|
||||
function input.create_getter(f)
|
||||
if f then
|
||||
if type(f) == 'string' then
|
||||
local ls = utils.split(f,'\n')
|
||||
local i,n = 0,#ls
|
||||
return function()
|
||||
i = i + 1
|
||||
if i > n then return nil end
|
||||
return ls[i]
|
||||
end
|
||||
else
|
||||
-- anything that supports the read() method!
|
||||
if not f.read then error('not a file-like object') end
|
||||
return function() return f:read() end
|
||||
end
|
||||
else
|
||||
return io.read -- i.e. just read from stdin
|
||||
end
|
||||
end
|
||||
|
||||
--- generate a sequence of numbers from a source.
|
||||
-- @param f A source
|
||||
-- @return An iterator
|
||||
function input.numbers(f)
|
||||
return alltokens(input.create_getter(f),
|
||||
'('..patterns.FLOAT..')',tonumber)
|
||||
end
|
||||
|
||||
--- generate a sequence of words from a source.
|
||||
-- @param f A source
|
||||
-- @return An iterator
|
||||
function input.words(f)
|
||||
return alltokens(input.create_getter(f),"%w+")
|
||||
end
|
||||
|
||||
local function apply_tonumber (no_fail,...)
|
||||
local args = {...}
|
||||
for i = 1,#args do
|
||||
local n = tonumber(args[i])
|
||||
if n == nil then
|
||||
if not no_fail then return nil,args[i] end
|
||||
else
|
||||
args[i] = n
|
||||
end
|
||||
end
|
||||
return args
|
||||
end
|
||||
|
||||
--- parse an input source into fields.
|
||||
-- By default, will fail if it cannot convert a field to a number.
|
||||
-- @param ids a list of field indices, or a maximum field index
|
||||
-- @param delim delimiter to parse fields (default space)
|
||||
-- @param f a source @see create_getter
|
||||
-- @param opts option table, {no_fail=true}
|
||||
-- @return an iterator with the field values
|
||||
-- @usage for x,y in fields {2,3} do print(x,y) end -- 2nd and 3rd fields from stdin
|
||||
function input.fields (ids,delim,f,opts)
|
||||
local sep
|
||||
local s
|
||||
local getter = input.create_getter(f)
|
||||
local no_fail = opts and opts.no_fail
|
||||
local no_convert = opts and opts.no_convert
|
||||
if not delim or delim == ' ' then
|
||||
delim = '%s'
|
||||
sep = '%s+'
|
||||
s = '%s*'
|
||||
else
|
||||
sep = delim
|
||||
s = ''
|
||||
end
|
||||
local max_id = 0
|
||||
if type(ids) == 'table' then
|
||||
for i,id in pairs(ids) do
|
||||
if id > max_id then max_id = id end
|
||||
end
|
||||
else
|
||||
max_id = ids
|
||||
ids = {}
|
||||
for i = 1,max_id do ids[#ids+1] = i end
|
||||
end
|
||||
local pat = '[^'..delim..']*'
|
||||
local k = 1
|
||||
for i = 1,max_id do
|
||||
if ids[k] == i then
|
||||
k = k + 1
|
||||
s = s..'('..pat..')'
|
||||
else
|
||||
s = s..pat
|
||||
end
|
||||
if i < max_id then
|
||||
s = s..sep
|
||||
end
|
||||
end
|
||||
local linecount = 1
|
||||
return function()
|
||||
local line,results,err
|
||||
repeat
|
||||
line = getter()
|
||||
linecount = linecount + 1
|
||||
if not line then return nil end
|
||||
if no_convert then
|
||||
results = {strmatch(line,s)}
|
||||
else
|
||||
results,err = apply_tonumber(no_fail,strmatch(line,s))
|
||||
if not results then
|
||||
utils.quit("line "..(linecount-1)..": cannot convert '"..err.."' to number")
|
||||
end
|
||||
end
|
||||
until #results > 0
|
||||
return unpack(results)
|
||||
end
|
||||
end
|
||||
|
||||
return input
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
--- Simple command-line parsing using human-readable specification.
|
||||
-- Supports GNU-style parameters.
|
||||
-- <pre class=example>
|
||||
-- lapp = require 'pl.lapp'
|
||||
-- local args = lapp [[
|
||||
-- Does some calculations
|
||||
-- -o,--offset (default 0.0) Offset to add to scaled number
|
||||
-- -s,--scale (number) Scaling factor
|
||||
-- <number> (number ) Number to be scaled
|
||||
-- ]]
|
||||
--
|
||||
-- print(args.offset + args.scale * args.number)
|
||||
-- </pre>
|
||||
-- Lines begining with '-' are flags; there may be a short and a long name;
|
||||
-- lines begining wih '<var>' are arguments. Anything in parens after
|
||||
-- the flag/argument is either a default, a type name or a range constraint.
|
||||
-- <p>See <a href="../../index.html#lapp">the Guide</a>
|
||||
-- @class module
|
||||
-- @name pl.lapp
|
||||
|
||||
local status,sip = pcall(require,'pl.sip')
|
||||
if not status then
|
||||
sip = require 'sip'
|
||||
end
|
||||
local match = sip.match_at_start
|
||||
local append,tinsert = table.insert,table.insert
|
||||
|
||||
--[[
|
||||
module('pl.lapp')
|
||||
]]
|
||||
|
||||
local function lines(s) return s:gmatch('([^\n]*)\n') end
|
||||
local function lstrip(str) return str:gsub('^%s+','') end
|
||||
local function strip(str) return lstrip(str):gsub('%s+$','') end
|
||||
local function at(s,k) return s:sub(k,k) end
|
||||
local function isdigit(s) return s:find('^%d+$') == 1 end
|
||||
|
||||
local lapp = {}
|
||||
|
||||
local open_files,parms,aliases,parmlist,usage,windows,script
|
||||
|
||||
lapp.callback = false -- keep Strict happy
|
||||
|
||||
local filetypes = {
|
||||
stdin = {io.stdin,'file-in'}, stdout = {io.stdout,'file-out'},
|
||||
stderr = {io.stderr,'file-out'}
|
||||
}
|
||||
|
||||
--- controls whether to dump usage on error.
|
||||
-- Defaults to true
|
||||
lapp.show_usage_error = true
|
||||
|
||||
--- quit this script immediately.
|
||||
-- @param msg optional message
|
||||
-- @param no_usage suppress 'usage' display
|
||||
function lapp.quit(msg,no_usage)
|
||||
if msg then
|
||||
io.stderr:write(msg..'\n\n')
|
||||
end
|
||||
if not no_usage then
|
||||
io.stderr:write(usage)
|
||||
end
|
||||
os.exit(1);
|
||||
end
|
||||
|
||||
--- print an error to stderr and quit.
|
||||
-- @param msg a message
|
||||
-- @param no_usage suppress 'usage' display
|
||||
function lapp.error(msg,no_usage)
|
||||
if not lapp.show_usage_error then
|
||||
no_usage = true
|
||||
end
|
||||
lapp.quit(script..':'..msg,no_usage)
|
||||
end
|
||||
|
||||
--- open a file.
|
||||
-- This will quit on error, and keep a list of file objects for later cleanup.
|
||||
-- @param file filename
|
||||
-- @param opt same as second parameter of <code>io.open</code>
|
||||
function lapp.open (file,opt)
|
||||
local val,err = io.open(file,opt)
|
||||
if not val then lapp.error(err,true) end
|
||||
append(open_files,val)
|
||||
return val
|
||||
end
|
||||
|
||||
--- quit if the condition is false.
|
||||
-- @param condn a condition
|
||||
-- @param msg an optional message
|
||||
function lapp.assert(condn,msg)
|
||||
if not condn then
|
||||
lapp.error(msg)
|
||||
end
|
||||
end
|
||||
|
||||
local function range_check(x,min,max,parm)
|
||||
lapp.assert(min <= x and max >= x,parm..' out of range')
|
||||
end
|
||||
|
||||
local function xtonumber(s)
|
||||
local val = tonumber(s)
|
||||
if not val then lapp.error("unable to convert to number: "..s) end
|
||||
return val
|
||||
end
|
||||
|
||||
local function is_filetype(type)
|
||||
return type == 'file-in' or type == 'file-out'
|
||||
end
|
||||
|
||||
local types
|
||||
|
||||
local function convert_parameter(ps,val)
|
||||
if ps.converter then
|
||||
val = ps.converter(val)
|
||||
end
|
||||
if ps.type == 'number' then
|
||||
val = xtonumber(val)
|
||||
elseif is_filetype(ps.type) then
|
||||
val = lapp.open(val,(ps.type == 'file-in' and 'r') or 'w' )
|
||||
elseif ps.type == 'boolean' then
|
||||
val = true
|
||||
end
|
||||
if ps.constraint then
|
||||
ps.constraint(val)
|
||||
end
|
||||
return val
|
||||
end
|
||||
|
||||
--- add a new type to Lapp. These appear in parens after the value like
|
||||
-- a range constraint, e.g. '<ival> (integer) Process PID'
|
||||
-- @param name name of type
|
||||
-- @param converter either a function to convert values, or a Lua type name.
|
||||
-- @param constraint optional function to verify values, should use lapp.error
|
||||
-- if failed.
|
||||
function lapp.add_type (name,converter,constraint)
|
||||
types[name] = {converter=converter,constraint=constraint}
|
||||
end
|
||||
|
||||
local function force_short(short)
|
||||
lapp.assert(#short==1,short..": short parameters should be one character")
|
||||
end
|
||||
|
||||
local function process_default (sval)
|
||||
local val = tonumber(sval)
|
||||
if val then -- we have a number!
|
||||
return val,'number'
|
||||
elseif filetypes[sval] then
|
||||
local ft = filetypes[sval]
|
||||
return ft[1],ft[2]
|
||||
else
|
||||
if sval:match '^["\']' then sval = sval:sub(2,-2) end
|
||||
return sval,'string'
|
||||
end
|
||||
end
|
||||
|
||||
--- process a Lapp options string.
|
||||
-- Usually called as lapp().
|
||||
-- @param str the options text
|
||||
-- @return a table with parameter-value pairs
|
||||
function lapp.process_options_string(str)
|
||||
local results = {}
|
||||
local opts = {at_start=true}
|
||||
local varargs
|
||||
open_files = {}
|
||||
parms = {}
|
||||
aliases = {}
|
||||
parmlist = {}
|
||||
types = {}
|
||||
|
||||
local function check_varargs(s)
|
||||
local res,cnt = s:gsub('^%.%.%.%s*','')
|
||||
return res, (cnt > 0)
|
||||
end
|
||||
|
||||
local function set_result(ps,parm,val)
|
||||
if not ps.varargs then
|
||||
results[parm] = val
|
||||
else
|
||||
if not results[parm] then
|
||||
results[parm] = { val }
|
||||
else
|
||||
append(results[parm],val)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
usage = str
|
||||
|
||||
for line in lines(str) do
|
||||
local res = {}
|
||||
local optspec,optparm,i1,i2,defval,vtype,constraint,rest
|
||||
line = lstrip(line)
|
||||
local function check(str)
|
||||
return match(str,line,res)
|
||||
end
|
||||
|
||||
-- flags: either '-<short>', '-<short>,--<long>' or '--<long>'
|
||||
if check '-$v{short}, --$v{long} $' or check '-$v{short} $' or check '--$v{long} $' then
|
||||
if res.long then
|
||||
optparm = res.long
|
||||
if res.short then aliases[res.short] = optparm end
|
||||
else
|
||||
optparm = res.short
|
||||
end
|
||||
if res.short then force_short(res.short) end
|
||||
res.rest, varargs = check_varargs(res.rest)
|
||||
elseif check '$<{name} $' then -- is it <parameter_name>?
|
||||
-- so <input file...> becomes input_file ...
|
||||
optparm,rest = res.name:match '([^%.]+)(.*)'
|
||||
optparm = optparm:gsub('%A','_')
|
||||
varargs = rest == '...'
|
||||
append(parmlist,optparm)
|
||||
end
|
||||
if res.rest then -- this is not a pure doc line
|
||||
line = res.rest
|
||||
res = {}
|
||||
-- do we have (default <val>) or (<type>)?
|
||||
if match('$({def} $',line,res) or match('$({def}',line,res) then
|
||||
local typespec = strip(res.def)
|
||||
if match('default $',typespec,res) then
|
||||
defval,vtype = process_default(res[1])
|
||||
elseif match('$f{min}..$f{max}',typespec,res) then
|
||||
local min,max = res.min,res.max
|
||||
vtype = 'number'
|
||||
constraint = function(x)
|
||||
range_check(x,min,max,optparm)
|
||||
end
|
||||
else -- () just contains type of required parameter
|
||||
vtype = typespec
|
||||
end
|
||||
else -- must be a plain flag, no extra parameter required
|
||||
defval = false
|
||||
vtype = 'boolean'
|
||||
end
|
||||
local ps = {
|
||||
type = vtype,
|
||||
defval = defval,
|
||||
required = defval == nil,
|
||||
comment = res.rest or optparm,
|
||||
constraint = constraint,
|
||||
varargs = varargs
|
||||
}
|
||||
varargs = nil
|
||||
if types[vtype] then
|
||||
local converter = types[vtype].converter
|
||||
if type(converter) == 'string' then
|
||||
ps.type = converter
|
||||
else
|
||||
ps.converter = converter
|
||||
end
|
||||
ps.constraint = types[vtype].constraint
|
||||
end
|
||||
parms[optparm] = ps
|
||||
end
|
||||
end
|
||||
-- cool, we have our parms, let's parse the command line args
|
||||
local iparm = 1
|
||||
local iextra = 1
|
||||
local i = 1
|
||||
local parm,ps,val
|
||||
|
||||
while i <= #arg do
|
||||
local theArg = arg[i]
|
||||
local res = {}
|
||||
-- look for a flag, -<short flags> or --<long flag>
|
||||
if match('--$v{long}',theArg,res) or match('-$v{short}',theArg,res) then
|
||||
if res.long then -- long option
|
||||
parm = res.long
|
||||
elseif #res.short == 1 then
|
||||
parm = res.short
|
||||
else
|
||||
local parmstr = res.short
|
||||
parm = at(parmstr,1)
|
||||
if isdigit(at(parmstr,2)) then
|
||||
-- a short option followed by a digit is an exception (for AW;))
|
||||
-- push ahead into the arg array
|
||||
tinsert(arg,i+1,parmstr:sub(2))
|
||||
else
|
||||
-- push multiple flags into the arg array!
|
||||
for k = 2,#parmstr do
|
||||
tinsert(arg,i+k-1,'-'..at(parmstr,k))
|
||||
end
|
||||
end
|
||||
end
|
||||
if parm == 'h' or parm == 'help' then
|
||||
lapp.quit()
|
||||
end
|
||||
if aliases[parm] then parm = aliases[parm] end
|
||||
else -- a parameter
|
||||
parm = parmlist[iparm]
|
||||
if not parm then
|
||||
-- extra unnamed parameters are indexed starting at 1
|
||||
parm = iextra
|
||||
ps = { type = 'string' }
|
||||
parms[parm] = ps
|
||||
iextra = iextra + 1
|
||||
else
|
||||
ps = parms[parm]
|
||||
end
|
||||
if not ps.varargs then
|
||||
iparm = iparm + 1
|
||||
end
|
||||
val = theArg
|
||||
end
|
||||
ps = parms[parm]
|
||||
if not ps then lapp.error("unrecognized parameter: "..parm) end
|
||||
if ps.type ~= 'boolean' then -- we need a value! This should follow
|
||||
if not val then
|
||||
i = i + 1
|
||||
val = arg[i]
|
||||
end
|
||||
lapp.assert(val,parm.." was expecting a value")
|
||||
end
|
||||
ps.used = true
|
||||
val = convert_parameter(ps,val)
|
||||
set_result(ps,parm,val)
|
||||
if is_filetype(ps.type) then
|
||||
set_result(ps,parm..'_name',theArg)
|
||||
end
|
||||
if lapp.callback then
|
||||
lapp.callback(parm,theArg,res)
|
||||
end
|
||||
i = i + 1
|
||||
val = nil
|
||||
end
|
||||
-- check unused parms, set defaults and check if any required parameters were missed
|
||||
for parm,ps in pairs(parms) do
|
||||
if not ps.used then
|
||||
if ps.required then lapp.error("missing required parameter: "..parm) end
|
||||
set_result(ps,parm,ps.defval)
|
||||
end
|
||||
end
|
||||
return results
|
||||
end
|
||||
|
||||
if arg then
|
||||
script = arg[0]:gsub('.+[\\/]',''):gsub('%.%a+$','')
|
||||
else
|
||||
script = "inter"
|
||||
end
|
||||
|
||||
|
||||
setmetatable(lapp, {
|
||||
__call = function(tbl,str) return lapp.process_options_string(str) end,
|
||||
})
|
||||
|
||||
|
||||
return lapp
|
||||
|
||||
|
||||
@@ -0,0 +1,461 @@
|
||||
--- Lexical scanner for creating a sequence of tokens from text. <br>
|
||||
-- <p><code>lexer.scan(s)</code> returns an iterator over all tokens found in the
|
||||
-- string <code>s</code>. This iterator returns two values, a token type string
|
||||
-- (such as 'string' for quoted string, 'iden' for identifier) and the value of the
|
||||
-- token.
|
||||
-- <p>
|
||||
-- Versions specialized for Lua and C are available; these also handle block comments
|
||||
-- and classify keywords as 'keyword' tokens. For example:
|
||||
-- <pre class=example>
|
||||
-- > s = 'for i=1,n do'
|
||||
-- > for t,v in lexer.lua(s) do print(t,v) end
|
||||
-- keyword for
|
||||
-- iden i
|
||||
-- = =
|
||||
-- number 1
|
||||
-- , ,
|
||||
-- iden n
|
||||
-- keyword do
|
||||
-- </pre>
|
||||
-- See the Guide for further <a href="../../index.html#lexer">discussion</a> <br>
|
||||
-- @class module
|
||||
-- @name pl.lexer
|
||||
|
||||
local yield,wrap = coroutine.yield,coroutine.wrap
|
||||
local strfind = string.find
|
||||
local strsub = string.sub
|
||||
local append = table.insert
|
||||
--[[
|
||||
module ('pl.lexer',utils._module)
|
||||
]]
|
||||
|
||||
local function assert_arg(idx,val,tp)
|
||||
if type(val) ~= tp then
|
||||
error("argument "..idx.." must be "..tp, 2)
|
||||
end
|
||||
end
|
||||
|
||||
local lexer = {}
|
||||
|
||||
local NUMBER1 = '^[%+%-]?%d+%.?%d*[eE][%+%-]?%d+'
|
||||
local NUMBER2 = '^[%+%-]?%d+%.?%d*'
|
||||
local NUMBER3 = '^0x[%da-fA-F]+'
|
||||
local NUMBER4 = '^%d+%.?%d*[eE][%+%-]?%d+'
|
||||
local NUMBER5 = '^%d+%.?%d*'
|
||||
local IDEN = '^[%a_][%w_]*'
|
||||
local WSPACE = '^%s+'
|
||||
local STRING1 = [[^'.-[^\\]']]
|
||||
local STRING2 = [[^".-[^\\]"]]
|
||||
local STRING3 = "^((['\"])%2)" -- empty string
|
||||
local PREPRO = '^#.-[^\\]\n'
|
||||
|
||||
local plain_matches,lua_matches,cpp_matches,lua_keyword,cpp_keyword
|
||||
|
||||
local function tdump(tok)
|
||||
return yield(tok,tok)
|
||||
end
|
||||
|
||||
local function ndump(tok,options)
|
||||
if options and options.number then
|
||||
tok = tonumber(tok)
|
||||
end
|
||||
return yield("number",tok)
|
||||
end
|
||||
|
||||
-- regular strings, single or double quotes; usually we want them
|
||||
-- without the quotes
|
||||
local function sdump(tok,options)
|
||||
if options and options.string then
|
||||
tok = tok:sub(2,-2)
|
||||
end
|
||||
return yield("string",tok)
|
||||
end
|
||||
|
||||
-- long Lua strings need extra work to get rid of the quotes
|
||||
local function sdump_l(tok,options)
|
||||
if options and options.string then
|
||||
tok = tok:sub(3,-3)
|
||||
end
|
||||
return yield("string",tok)
|
||||
end
|
||||
|
||||
local function chdump(tok,options)
|
||||
if options and options.string then
|
||||
tok = tok:sub(2,-2)
|
||||
end
|
||||
return yield("char",tok)
|
||||
end
|
||||
|
||||
local function cdump(tok)
|
||||
return yield('comment',tok)
|
||||
end
|
||||
|
||||
local function wsdump (tok)
|
||||
return yield("space",tok)
|
||||
end
|
||||
|
||||
local function pdump (tok)
|
||||
return yield('prepro',tok)
|
||||
end
|
||||
|
||||
local function plain_vdump(tok)
|
||||
return yield("iden",tok)
|
||||
end
|
||||
|
||||
local function lua_vdump(tok)
|
||||
if lua_keyword[tok] then
|
||||
return yield("keyword",tok)
|
||||
else
|
||||
return yield("iden",tok)
|
||||
end
|
||||
end
|
||||
|
||||
local function cpp_vdump(tok)
|
||||
if cpp_keyword[tok] then
|
||||
return yield("keyword",tok)
|
||||
else
|
||||
return yield("iden",tok)
|
||||
end
|
||||
end
|
||||
|
||||
--- create a plain token iterator from a string or file-like object.
|
||||
-- @param s the string
|
||||
-- @param matches an optional match table (set of pattern-action pairs)
|
||||
-- @param filter a table of token types to exclude, by default {space=true}
|
||||
-- @param options a table of options; by default, {number=true,string=true},
|
||||
-- which means convert numbers and strip string quotes.
|
||||
function lexer.scan (s,matches,filter,options)
|
||||
--assert_arg(1,s,'string')
|
||||
local file = type(s) ~= 'string' and s
|
||||
filter = filter or {space=true}
|
||||
options = options or {number=true,string=true}
|
||||
if filter then
|
||||
if filter.space then filter[wsdump] = true end
|
||||
if filter.comments then
|
||||
filter[cdump] = true
|
||||
end
|
||||
end
|
||||
if not matches then
|
||||
if not plain_matches then
|
||||
plain_matches = {
|
||||
{WSPACE,wsdump},
|
||||
{NUMBER3,ndump},
|
||||
{IDEN,plain_vdump},
|
||||
{NUMBER1,ndump},
|
||||
{NUMBER2,ndump},
|
||||
{STRING3,sdump},
|
||||
{STRING1,sdump},
|
||||
{STRING2,sdump},
|
||||
{'^.',tdump}
|
||||
}
|
||||
end
|
||||
matches = plain_matches
|
||||
end
|
||||
local function lex ()
|
||||
local i1,i2,idx,res1,res2,tok,pat,fun,capt
|
||||
local line = 1
|
||||
if file then s = file:read()..'\n' end
|
||||
local sz = #s
|
||||
local idx = 1
|
||||
--print('sz',sz)
|
||||
while true do
|
||||
for _,m in ipairs(matches) do
|
||||
pat = m[1]
|
||||
fun = m[2]
|
||||
i1,i2 = strfind(s,pat,idx)
|
||||
if i1 then
|
||||
tok = strsub(s,i1,i2)
|
||||
idx = i2 + 1
|
||||
if not (filter and filter[fun]) then
|
||||
lexer.finished = idx > sz
|
||||
res1,res2 = fun(tok,options)
|
||||
end
|
||||
if res1 then
|
||||
local tp = type(res1)
|
||||
-- insert a token list
|
||||
if tp=='table' then
|
||||
yield('','')
|
||||
for _,t in ipairs(res1) do
|
||||
yield(t[1],t[2])
|
||||
end
|
||||
elseif tp == 'string' then -- or search up to some special pattern
|
||||
i1,i2 = strfind(s,res1,idx)
|
||||
if i1 then
|
||||
tok = strsub(s,i1,i2)
|
||||
idx = i2 + 1
|
||||
yield('',tok)
|
||||
else
|
||||
yield('','')
|
||||
idx = sz + 1
|
||||
end
|
||||
--if idx > sz then return end
|
||||
else
|
||||
yield(line,idx)
|
||||
end
|
||||
end
|
||||
if idx > sz then
|
||||
if file then
|
||||
--repeat -- next non-empty line
|
||||
line = line + 1
|
||||
s = file:read()
|
||||
if not s then return end
|
||||
--until not s:match '^%s*$'
|
||||
s = s .. '\n'
|
||||
idx ,sz = 1,#s
|
||||
break
|
||||
else
|
||||
return
|
||||
end
|
||||
else break end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return wrap(lex)
|
||||
end
|
||||
|
||||
local function isstring (s)
|
||||
return type(s) == 'string'
|
||||
end
|
||||
|
||||
--- insert tokens into a stream.
|
||||
-- @param tok a token stream
|
||||
-- @param a1 a string is the type, a table is a token list and
|
||||
-- a function is assumed to be a token-like iterator (returns type & value)
|
||||
-- @param a2 a string is the value
|
||||
function lexer.insert (tok,a1,a2)
|
||||
if not a1 then return end
|
||||
local ts
|
||||
if isstring(a1) and isstring(a2) then
|
||||
ts = {{a1,a2}}
|
||||
elseif type(a1) == 'function' then
|
||||
ts = {}
|
||||
for t,v in a1() do
|
||||
append(ts,{t,v})
|
||||
end
|
||||
else
|
||||
ts = a1
|
||||
end
|
||||
tok(ts)
|
||||
end
|
||||
|
||||
--- get everything in a stream upto a newline.
|
||||
-- @param tok a token stream
|
||||
-- @return a string
|
||||
function lexer.getline (tok)
|
||||
local t,v = tok('.-\n')
|
||||
return v
|
||||
end
|
||||
|
||||
--- get current line number. <br>
|
||||
-- Only available if the input source is a file-like object.
|
||||
-- @param tok a token stream
|
||||
-- @return the line number and current column
|
||||
function lexer.lineno (tok)
|
||||
return tok(0)
|
||||
end
|
||||
|
||||
--- get the rest of the stream.
|
||||
-- @param tok a token stream
|
||||
-- @return a string
|
||||
function lexer.getrest (tok)
|
||||
local t,v = tok('.+')
|
||||
return v
|
||||
end
|
||||
|
||||
--- get the Lua keywords as a set-like table.
|
||||
-- So <code>res["and"]</code> etc would be <code>true</code>.
|
||||
-- @return a table
|
||||
function lexer.get_keywords ()
|
||||
if not lua_keyword then
|
||||
lua_keyword = {
|
||||
["and"] = true, ["break"] = true, ["do"] = true,
|
||||
["else"] = true, ["elseif"] = true, ["end"] = true,
|
||||
["false"] = true, ["for"] = true, ["function"] = true,
|
||||
["if"] = true, ["in"] = true, ["local"] = true, ["nil"] = true,
|
||||
["not"] = true, ["or"] = true, ["repeat"] = true,
|
||||
["return"] = true, ["then"] = true, ["true"] = true,
|
||||
["until"] = true, ["while"] = true
|
||||
}
|
||||
end
|
||||
return lua_keyword
|
||||
end
|
||||
|
||||
|
||||
--- create a Lua token iterator from a string or file-like object.
|
||||
-- Will return the token type and value.
|
||||
-- @param s the string
|
||||
-- @param filter a table of token types to exclude, by default {space=true,comments=true}
|
||||
-- @param options a table of options; by default, {number=true,string=true},
|
||||
-- which means convert numbers and strip string quotes.
|
||||
function lexer.lua(s,filter,options)
|
||||
filter = filter or {space=true,comments=true}
|
||||
lexer.get_keywords()
|
||||
if not lua_matches then
|
||||
lua_matches = {
|
||||
{WSPACE,wsdump},
|
||||
{NUMBER3,ndump},
|
||||
{IDEN,lua_vdump},
|
||||
{NUMBER4,ndump},
|
||||
{NUMBER5,ndump},
|
||||
{STRING3,sdump},
|
||||
{STRING1,sdump},
|
||||
{STRING2,sdump},
|
||||
{'^%-%-%[%[.-%]%]',cdump},
|
||||
{'^%-%-.-\n',cdump},
|
||||
{'^%[%[.-%]%]',sdump_l},
|
||||
{'^==',tdump},
|
||||
{'^~=',tdump},
|
||||
{'^<=',tdump},
|
||||
{'^>=',tdump},
|
||||
{'^%.%.%.',tdump},
|
||||
{'^%.%.',tdump},
|
||||
{'^.',tdump}
|
||||
}
|
||||
end
|
||||
return lexer.scan(s,lua_matches,filter,options)
|
||||
end
|
||||
|
||||
--- create a C/C++ token iterator from a string or file-like object.
|
||||
-- Will return the token type type and value.
|
||||
-- @param s the string
|
||||
-- @param filter a table of token types to exclude, by default {space=true,comments=true}
|
||||
-- @param options a table of options; by default, {number=true,string=true},
|
||||
-- which means convert numbers and strip string quotes.
|
||||
function lexer.cpp(s,filter,options)
|
||||
filter = filter or {comments=true}
|
||||
if not cpp_keyword then
|
||||
cpp_keyword = {
|
||||
["class"] = true, ["break"] = true, ["do"] = true, ["sizeof"] = true,
|
||||
["else"] = true, ["continue"] = true, ["struct"] = true,
|
||||
["false"] = true, ["for"] = true, ["public"] = true, ["void"] = true,
|
||||
["private"] = true, ["protected"] = true, ["goto"] = true,
|
||||
["if"] = true, ["static"] = true, ["const"] = true, ["typedef"] = true,
|
||||
["enum"] = true, ["char"] = true, ["int"] = true, ["bool"] = true,
|
||||
["long"] = true, ["float"] = true, ["true"] = true, ["delete"] = true,
|
||||
["double"] = true, ["while"] = true, ["new"] = true,
|
||||
["namespace"] = true, ["try"] = true, ["catch"] = true,
|
||||
["switch"] = true, ["case"] = true, ["extern"] = true,
|
||||
["return"] = true,["default"] = true,['unsigned'] = true,['signed'] = true,
|
||||
["union"] = true, ["volatile"] = true, ["register"] = true,["short"] = true,
|
||||
}
|
||||
end
|
||||
if not cpp_matches then
|
||||
cpp_matches = {
|
||||
{WSPACE,wsdump},
|
||||
{PREPRO,pdump},
|
||||
{NUMBER3,ndump},
|
||||
{IDEN,cpp_vdump},
|
||||
{NUMBER4,ndump},
|
||||
{NUMBER5,ndump},
|
||||
{STRING3,sdump},
|
||||
{STRING1,chdump},
|
||||
{STRING2,sdump},
|
||||
{'^//.-\n',cdump},
|
||||
{'^/%*.-%*/',cdump},
|
||||
{'^==',tdump},
|
||||
{'^!=',tdump},
|
||||
{'^<=',tdump},
|
||||
{'^>=',tdump},
|
||||
{'^->',tdump},
|
||||
{'^&&',tdump},
|
||||
{'^||',tdump},
|
||||
{'^%+%+',tdump},
|
||||
{'^%-%-',tdump},
|
||||
{'^%+=',tdump},
|
||||
{'^%-=',tdump},
|
||||
{'^%*=',tdump},
|
||||
{'^/=',tdump},
|
||||
{'^|=',tdump},
|
||||
{'^%^=',tdump},
|
||||
{'^::',tdump},
|
||||
{'^.',tdump}
|
||||
}
|
||||
end
|
||||
return lexer.scan(s,cpp_matches,filter,options)
|
||||
end
|
||||
|
||||
--- get a list of parameters separated by a delimiter from a stream.
|
||||
-- @param tok the token stream
|
||||
-- @param endtoken end of list (default ')'). Can be '\n'
|
||||
-- @param delim separator (default ',')
|
||||
-- @return a list of token lists.
|
||||
function lexer.get_separated_list(tok,endtoken,delim)
|
||||
endtoken = endtoken or ')'
|
||||
delim = delim or ','
|
||||
local parm_values = {}
|
||||
local level = 1 -- used to count ( and )
|
||||
local tl = {}
|
||||
local function tappend (tl,t,val)
|
||||
val = val or t
|
||||
append(tl,{t,val})
|
||||
end
|
||||
local is_end
|
||||
if endtoken == '\n' then
|
||||
is_end = function(t,val)
|
||||
return t == 'space' and val:find '\n'
|
||||
end
|
||||
else
|
||||
is_end = function (t)
|
||||
return t == endtoken
|
||||
end
|
||||
end
|
||||
local token,value
|
||||
while true do
|
||||
token,value=tok()
|
||||
if not token then return nil,'EOS' end -- end of stream is an error!
|
||||
if is_end(token,value) and level == 1 then
|
||||
append(parm_values,tl)
|
||||
break
|
||||
elseif token == '(' then
|
||||
level = level + 1
|
||||
tappend(tl,'(')
|
||||
elseif token == ')' then
|
||||
level = level - 1
|
||||
if level == 0 then -- finished with parm list
|
||||
append(parm_values,tl)
|
||||
break
|
||||
else
|
||||
tappend(tl,')')
|
||||
end
|
||||
elseif token == delim and level == 1 then
|
||||
append(parm_values,tl) -- a new parm
|
||||
tl = {}
|
||||
else
|
||||
tappend(tl,token,value)
|
||||
end
|
||||
end
|
||||
return parm_values,{token,value}
|
||||
end
|
||||
|
||||
--- get the next non-space token from the stream.
|
||||
-- @param tok the token stream.
|
||||
function lexer.skipws (tok)
|
||||
local t,v = tok()
|
||||
while t == 'space' do
|
||||
t,v = tok()
|
||||
end
|
||||
return t,v
|
||||
end
|
||||
|
||||
local skipws = lexer.skipws
|
||||
|
||||
--- get the next token, which must be of the expected type.
|
||||
-- Throws an error if this type does not match!
|
||||
-- @param tok the token stream
|
||||
-- @param expected_type the token type
|
||||
-- @param no_skip_ws whether we should skip whitespace
|
||||
function lexer.expecting (tok,expected_type,no_skip_ws)
|
||||
assert_arg(1,tok,'function')
|
||||
assert_arg(2,expected_type,'string')
|
||||
local t,v
|
||||
if no_skip_ws then
|
||||
t,v = tok()
|
||||
else
|
||||
t,v = skipws(tok)
|
||||
end
|
||||
if t ~= expected_type then error ("expecting "..expected_type,2) end
|
||||
return v
|
||||
end
|
||||
|
||||
return lexer
|
||||
@@ -0,0 +1,264 @@
|
||||
--- Extract delimited Lua sequences from strings.
|
||||
-- Inspired by Damian Conway's Text::Balanced in Perl. <br/>
|
||||
-- <ul>
|
||||
-- <li>[1] <a href="http://lua-users.org/wiki/LuaBalanced">Lua Wiki Page</a></li>
|
||||
-- <li>[2] http://search.cpan.org/dist/Text-Balanced/lib/Text/Balanced.pm</li>
|
||||
-- </ul> <br/>
|
||||
-- <pre class=example>
|
||||
-- local lb = require "pl.luabalanced"
|
||||
-- --Extract Lua expression starting at position 4.
|
||||
-- print(lb.match_expression("if x^2 + x > 5 then print(x) end", 4))
|
||||
-- --> x^2 + x > 5 16
|
||||
-- --Extract Lua string starting at (default) position 1.
|
||||
-- print(lb.match_string([["test\"123" .. "more"]]))
|
||||
-- --> "test\"123" 12
|
||||
-- </pre>
|
||||
-- (c) 2008, David Manura, Licensed under the same terms as Lua (MIT license).
|
||||
-- @class module
|
||||
-- @name pl.luabalanced
|
||||
|
||||
local M = {}
|
||||
|
||||
local assert = assert
|
||||
local table_concat = table.concat
|
||||
|
||||
-- map opening brace <-> closing brace.
|
||||
local ends = { ['('] = ')', ['{'] = '}', ['['] = ']' }
|
||||
local begins = {}; for k,v in pairs(ends) do begins[v] = k end
|
||||
|
||||
|
||||
-- Match Lua string in string <s> starting at position <pos>.
|
||||
-- Returns <string>, <posnew>, where <string> is the matched
|
||||
-- string (or nil on no match) and <posnew> is the character
|
||||
-- following the match (or <pos> on no match).
|
||||
-- Supports all Lua string syntax: "...", '...', [[...]], [=[...]=], etc.
|
||||
local function match_string(s, pos)
|
||||
pos = pos or 1
|
||||
local posa = pos
|
||||
local c = s:sub(pos,pos)
|
||||
if c == '"' or c == "'" then
|
||||
pos = pos + 1
|
||||
while 1 do
|
||||
pos = assert(s:find("[" .. c .. "\\]", pos), 'syntax error')
|
||||
if s:sub(pos,pos) == c then
|
||||
local part = s:sub(posa, pos)
|
||||
return part, pos + 1
|
||||
else
|
||||
pos = pos + 2
|
||||
end
|
||||
end
|
||||
else
|
||||
local sc = s:match("^%[(=*)%[", pos)
|
||||
if sc then
|
||||
local _; _, pos = s:find("%]" .. sc .. "%]", pos)
|
||||
assert(pos)
|
||||
local part = s:sub(posa, pos)
|
||||
return part, pos + 1
|
||||
else
|
||||
return nil, pos
|
||||
end
|
||||
end
|
||||
end
|
||||
M.match_string = match_string
|
||||
|
||||
|
||||
-- Match bracketed Lua expression, e.g. "(...)", "{...}", "[...]", "[[...]]",
|
||||
-- [=[...]=], etc.
|
||||
-- Function interface is similar to match_string.
|
||||
local function match_bracketed(s, pos)
|
||||
pos = pos or 1
|
||||
local posa = pos
|
||||
local ca = s:sub(pos,pos)
|
||||
if not ends[ca] then
|
||||
return nil, pos
|
||||
end
|
||||
local stack = {}
|
||||
while 1 do
|
||||
pos = s:find('[%(%{%[%)%}%]\"\']', pos)
|
||||
assert(pos, 'syntax error: unbalanced')
|
||||
local c = s:sub(pos,pos)
|
||||
if c == '"' or c == "'" then
|
||||
local part; part, pos = match_string(s, pos)
|
||||
assert(part)
|
||||
elseif ends[c] then -- open
|
||||
local mid, posb
|
||||
if c == '[' then mid, posb = s:match('^%[(=*)%[()', pos) end
|
||||
if mid then
|
||||
pos = s:match('%]' .. mid .. '%]()', posb)
|
||||
assert(pos, 'syntax error: long string not terminated')
|
||||
if #stack == 0 then
|
||||
local part = s:sub(posa, pos-1)
|
||||
return part, pos
|
||||
end
|
||||
else
|
||||
stack[#stack+1] = c
|
||||
pos = pos + 1
|
||||
end
|
||||
else -- close
|
||||
assert(stack[#stack] == assert(begins[c]), 'syntax error: unbalanced')
|
||||
stack[#stack] = nil
|
||||
if #stack == 0 then
|
||||
local part = s:sub(posa, pos)
|
||||
return part, pos+1
|
||||
end
|
||||
pos = pos + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
M.match_bracketed = match_bracketed
|
||||
|
||||
|
||||
-- Match Lua comment, e.g. "--...\n", "--[[...]]", "--[=[...]=]", etc.
|
||||
-- Function interface is similar to match_string.
|
||||
local function match_comment(s, pos)
|
||||
pos = pos or 1
|
||||
if s:sub(pos, pos+1) ~= '--' then
|
||||
return nil, pos
|
||||
end
|
||||
pos = pos + 2
|
||||
local partt, post = match_string(s, pos)
|
||||
if partt then
|
||||
return '--' .. partt, post
|
||||
end
|
||||
local part; part, pos = s:match('^([^\n]*\n?)()', pos)
|
||||
return '--' .. part, pos
|
||||
end
|
||||
|
||||
|
||||
-- Match Lua expression, e.g. "a + b * c[e]".
|
||||
-- Function interface is similar to match_string.
|
||||
local wordop = {['and']=true, ['or']=true, ['not']=true}
|
||||
local is_compare = {['>']=true, ['<']=true, ['~']=true}
|
||||
local function match_expression(s, pos)
|
||||
pos = pos or 1
|
||||
local posa = pos
|
||||
local lastident
|
||||
local poscs, posce
|
||||
while pos do
|
||||
local c = s:sub(pos,pos)
|
||||
if c == '"' or c == "'" or c == '[' and s:find('^[=%[]', pos+1) then
|
||||
local part; part, pos = match_string(s, pos)
|
||||
assert(part, 'syntax error')
|
||||
elseif c == '-' and s:sub(pos+1,pos+1) == '-' then
|
||||
-- note: handle adjacent comments in loop to properly support
|
||||
-- backtracing (poscs/posce).
|
||||
poscs = pos
|
||||
while s:sub(pos,pos+1) == '--' do
|
||||
local part; part, pos = match_comment(s, pos)
|
||||
assert(part)
|
||||
pos = s:match('^%s*()', pos)
|
||||
posce = pos
|
||||
end
|
||||
elseif c == '(' or c == '{' or c == '[' then
|
||||
local part; part, pos = match_bracketed(s, pos)
|
||||
elseif c == '=' and s:sub(pos+1,pos+1) == '=' then
|
||||
pos = pos + 2 -- skip over two-char op containing '='
|
||||
elseif c == '=' and is_compare[s:sub(pos-1,pos-1)] then
|
||||
pos = pos + 1 -- skip over two-char op containing '='
|
||||
elseif c:match'^[%)%}%];,=]' then
|
||||
local part = s:sub(posa, pos-1)
|
||||
return part, pos
|
||||
elseif c:match'^[%w_]' then
|
||||
local newident,newpos = s:match('^([%w_]+)()', pos)
|
||||
if pos ~= posa and not wordop[newident] then -- non-first ident
|
||||
local pose = ((posce == pos) and poscs or pos) - 1
|
||||
while s:match('^%s', pose) do pose = pose - 1 end
|
||||
local ce = s:sub(pose,pose)
|
||||
if ce:match'[%)%}\'\"%]]' or
|
||||
ce:match'[%w_]' and not wordop[lastident]
|
||||
then
|
||||
local part = s:sub(posa, pos-1)
|
||||
return part, pos
|
||||
end
|
||||
end
|
||||
lastident, pos = newident, newpos
|
||||
else
|
||||
pos = pos + 1
|
||||
end
|
||||
pos = s:find('[%(%{%[%)%}%]\"\';,=%w_%-]', pos)
|
||||
end
|
||||
local part = s:sub(posa, #s)
|
||||
return part, #s+1
|
||||
end
|
||||
M.match_expression = match_expression
|
||||
|
||||
|
||||
-- Match name list (zero or more names). E.g. "a,b,c"
|
||||
-- Function interface is similar to match_string,
|
||||
-- but returns array as match.
|
||||
local function match_namelist(s, pos)
|
||||
pos = pos or 1
|
||||
local list = {}
|
||||
while 1 do
|
||||
local c = #list == 0 and '^' or '^%s*,%s*'
|
||||
local item, post = s:match(c .. '([%a_][%w_]*)%s*()', pos)
|
||||
if item then pos = post else break end
|
||||
list[#list+1] = item
|
||||
end
|
||||
return list, pos
|
||||
end
|
||||
M.match_namelist = match_namelist
|
||||
|
||||
|
||||
-- Match expression list (zero or more expressions). E.g. "a+b,b*c".
|
||||
-- Function interface is similar to match_string,
|
||||
-- but returns array as match.
|
||||
local function match_explist(s, pos)
|
||||
pos = pos or 1
|
||||
local list = {}
|
||||
while 1 do
|
||||
if #list ~= 0 then
|
||||
local post = s:match('^%s*,%s*()', pos)
|
||||
if post then pos = post else break end
|
||||
end
|
||||
local item; item, pos = match_expression(s, pos)
|
||||
assert(item, 'syntax error')
|
||||
list[#list+1] = item
|
||||
end
|
||||
return list, pos
|
||||
end
|
||||
M.match_explist = match_explist
|
||||
|
||||
|
||||
-- Replace snippets of code in Lua code string <s>
|
||||
-- using replacement function f(u,sin) --> sout.
|
||||
-- <u> is the type of snippet ('c' = comment, 's' = string,
|
||||
-- 'e' = any other code).
|
||||
-- Snippet is replaced with <sout> (unless <sout> is nil or false, in
|
||||
-- which case the original snippet is kept)
|
||||
-- This is somewhat analogous to string.gsub .
|
||||
local function gsub(s, f)
|
||||
local pos = 1
|
||||
local posa = 1
|
||||
local sret = ''
|
||||
while 1 do
|
||||
pos = s:find('[%-\'\"%[]', pos)
|
||||
if not pos then break end
|
||||
if s:match('^%-%-', pos) then
|
||||
local exp = s:sub(posa, pos-1)
|
||||
if #exp > 0 then sret = sret .. (f('e', exp) or exp) end
|
||||
local comment; comment, pos = match_comment(s, pos)
|
||||
sret = sret .. (f('c', assert(comment)) or comment)
|
||||
posa = pos
|
||||
else
|
||||
local posb = s:find('^[\'\"%[]', pos)
|
||||
local str
|
||||
if posb then str, pos = match_string(s, posb) end
|
||||
if str then
|
||||
local exp = s:sub(posa, posb-1)
|
||||
if #exp > 0 then sret = sret .. (f('e', exp) or exp) end
|
||||
sret = sret .. (f('s', str) or str)
|
||||
posa = pos
|
||||
else
|
||||
pos = pos + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
local exp = s:sub(posa)
|
||||
if #exp > 0 then sret = sret .. (f('e', exp) or exp) end
|
||||
return sret
|
||||
end
|
||||
M.gsub = gsub
|
||||
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,197 @@
|
||||
--- Lua operators available as functions.
|
||||
-- (similar to the Python module of the same name)<br>
|
||||
-- There is a module field <code>optable</code> which maps the operator strings
|
||||
-- onto these functions, e.g. <pre class=example>operator.optable['()']==operator.call</pre>
|
||||
-- <p>Operator strings like '>' and '{}' can be passed to most Penlight functions
|
||||
-- expecting a function argument.</p>
|
||||
-- @class module
|
||||
-- @name pl.operator
|
||||
|
||||
local strfind = string.find
|
||||
local utils = require 'pl.utils'
|
||||
|
||||
local operator = {}
|
||||
|
||||
--- apply function to some arguments ()
|
||||
-- @param fn a function or callable object
|
||||
-- @param ... arguments
|
||||
function operator.call(fn,...)
|
||||
return fn(...)
|
||||
end
|
||||
|
||||
--- get the indexed value from a table []
|
||||
-- @param t a table or any indexable object
|
||||
-- @param k the key
|
||||
function operator.index(t,k)
|
||||
return t[k]
|
||||
end
|
||||
|
||||
--- returns true if arguments are equal ==
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.eq(a,b)
|
||||
return a==b
|
||||
end
|
||||
|
||||
--- returns true if arguments are not equal ~=
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.neq(a,b)
|
||||
return a~=b
|
||||
end
|
||||
|
||||
--- returns true if a is less than b <
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.lt(a,b)
|
||||
return a < b
|
||||
end
|
||||
|
||||
--- returns true if a is less or equal to b <=
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.le(a,b)
|
||||
return a <= b
|
||||
end
|
||||
|
||||
--- returns true if a is greater than b >
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.gt(a,b)
|
||||
return a > b
|
||||
end
|
||||
|
||||
--- returns true if a is greater or equal to b >=
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.ge(a,b)
|
||||
return a >= b
|
||||
end
|
||||
|
||||
--- returns length of string or table #
|
||||
-- @param a a string or a table
|
||||
function operator.len(a)
|
||||
return #a
|
||||
end
|
||||
|
||||
--- add two values +
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.add(a,b)
|
||||
return a+b
|
||||
end
|
||||
|
||||
--- subtract b from a -
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.sub(a,b)
|
||||
return a-b
|
||||
end
|
||||
|
||||
--- multiply two values *
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.mul(a,b)
|
||||
return a*b
|
||||
end
|
||||
|
||||
--- divide first value by second /
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.div(a,b)
|
||||
return a/b
|
||||
end
|
||||
|
||||
--- raise first to the power of second ^
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.pow(a,b)
|
||||
return a^b
|
||||
end
|
||||
|
||||
--- modulo; remainder of a divided by b %
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.mod(a,b)
|
||||
return a%b
|
||||
end
|
||||
|
||||
--- concatenate two values (either strings or __concat defined) ..
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.concat(a,b)
|
||||
return a..b
|
||||
end
|
||||
|
||||
--- return the negative of a value -
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.unm(a)
|
||||
return -a
|
||||
end
|
||||
|
||||
--- false if value evaluates as true not
|
||||
-- @param a value
|
||||
function operator.lnot(a)
|
||||
return not a
|
||||
end
|
||||
|
||||
--- true if both values evaluate as true and
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.land(a,b)
|
||||
return a and b
|
||||
end
|
||||
|
||||
--- true if either value evaluate as true or
|
||||
-- @param a value
|
||||
-- @param b value
|
||||
function operator.lor(a,b)
|
||||
return a or b
|
||||
end
|
||||
|
||||
--- make a table from the arguments {}
|
||||
-- @param ... non-nil arguments
|
||||
-- @return a table
|
||||
function operator.table (...)
|
||||
return {...}
|
||||
end
|
||||
|
||||
--- match two strings ~
|
||||
-- uses @{string.find}
|
||||
function operator.match (a,b)
|
||||
return strfind(a,b)~=nil
|
||||
end
|
||||
|
||||
--- the null operation.
|
||||
-- @param ... arguments
|
||||
-- @return the arguments
|
||||
function operator.nop (...)
|
||||
return ...
|
||||
end
|
||||
|
||||
operator.optable = {
|
||||
['+']=operator.add,
|
||||
['-']=operator.sub,
|
||||
['*']=operator.mul,
|
||||
['/']=operator.div,
|
||||
['%']=operator.mod,
|
||||
['^']=operator.pow,
|
||||
['..']=operator.concat,
|
||||
['()']=operator.call,
|
||||
['[]']=operator.index,
|
||||
['<']=operator.lt,
|
||||
['<=']=operator.le,
|
||||
['>']=operator.gt,
|
||||
['>=']=operator.ge,
|
||||
['==']=operator.eq,
|
||||
['~=']=operator.neq,
|
||||
['#']=operator.len,
|
||||
['and']=operator.land,
|
||||
['or']=operator.lor,
|
||||
['{}']=operator.table,
|
||||
['~']=operator.match,
|
||||
['']=operator.nop,
|
||||
}
|
||||
|
||||
return operator
|
||||
@@ -0,0 +1,335 @@
|
||||
--- Path manipulation and file queries. <br>
|
||||
-- This is modelled after Python's os.path library (11.1)
|
||||
-- @class module
|
||||
-- @name pl.path
|
||||
|
||||
-- imports and locals
|
||||
local _G = _G
|
||||
local sub = string.sub
|
||||
local getenv = os.getenv
|
||||
local tmpnam = os.tmpname
|
||||
local attributes, currentdir, link_attrib
|
||||
local package = package
|
||||
local io = io
|
||||
local append = table.insert
|
||||
local ipairs = ipairs
|
||||
local utils = require 'pl.utils'
|
||||
local assert_arg,assert_string,raise = utils.assert_arg,utils.assert_string,utils.raise
|
||||
|
||||
--[[
|
||||
module ('pl.path',utils._module)
|
||||
]]
|
||||
|
||||
local path, attrib
|
||||
|
||||
if _G.luajava then
|
||||
path = require 'pl.platf.luajava'
|
||||
else
|
||||
path = {}
|
||||
|
||||
local res,lfs = _G.pcall(_G.require,'lfs')
|
||||
if res then
|
||||
attributes = lfs.attributes
|
||||
currentdir = lfs.currentdir
|
||||
link_attrib = lfs.symlinkattributes
|
||||
else
|
||||
error("pl.path requires LuaFileSystem")
|
||||
end
|
||||
|
||||
attrib = attributes
|
||||
path.attrib = attrib
|
||||
path.link_attrib = link_attrib
|
||||
path.dir = lfs.dir
|
||||
path.mkdir = lfs.mkdir
|
||||
path.rmdir = lfs.rmdir
|
||||
path.chdir = lfs.chdir
|
||||
|
||||
--- is this a directory?
|
||||
-- @param P A file path
|
||||
function path.isdir(P)
|
||||
if P:match("\\$") then
|
||||
P = P:sub(1,-2)
|
||||
end
|
||||
return attrib(P,'mode') == 'directory'
|
||||
end
|
||||
|
||||
--- is this a file?.
|
||||
-- @param P A file path
|
||||
function path.isfile(P)
|
||||
return attrib(P,'mode') == 'file'
|
||||
end
|
||||
|
||||
-- is this a symbolic link?
|
||||
-- @param P A file path
|
||||
function path.islink(P)
|
||||
if link_attrib then
|
||||
return link_attrib(P,'mode')=='link'
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
--- return size of a file.
|
||||
-- @param P A file path
|
||||
function path.getsize(P)
|
||||
return attrib(P,'size')
|
||||
end
|
||||
|
||||
--- does a path exist?.
|
||||
-- @param P A file path
|
||||
-- @return the file path if it exists, nil otherwise
|
||||
function path.exists(P)
|
||||
return attrib(P,'mode') ~= nil and P
|
||||
end
|
||||
|
||||
--- Return the time of last access as the number of seconds since the epoch.
|
||||
-- @param P A file path
|
||||
function path.getatime(P)
|
||||
return attrib(P,'access')
|
||||
end
|
||||
|
||||
--- Return the time of last modification
|
||||
-- @param P A file path
|
||||
function path.getmtime(P)
|
||||
return attrib(P,'modification')
|
||||
end
|
||||
|
||||
---Return the system's ctime.
|
||||
-- @param P A file path
|
||||
function path.getctime(P)
|
||||
return path.attrib(P,'change')
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function at(s,i)
|
||||
return sub(s,i,i)
|
||||
end
|
||||
|
||||
path.is_windows = utils.dir_separator == '\\'
|
||||
|
||||
local other_sep
|
||||
-- !constant sep is the directory separator for this platform.
|
||||
if path.is_windows then
|
||||
path.sep = '\\'; other_sep = '/'
|
||||
path.dirsep = ';'
|
||||
else
|
||||
path.sep = '/'
|
||||
path.dirsep = ':'
|
||||
end
|
||||
local sep,dirsep = path.sep,path.dirsep
|
||||
|
||||
--- are we running Windows?
|
||||
-- @class field
|
||||
-- @name path.is_windows
|
||||
|
||||
--- path separator for this platform.
|
||||
-- @class field
|
||||
-- @name path.sep
|
||||
|
||||
--- separator for PATH for this platform
|
||||
-- @class field
|
||||
-- @name path.dirsep
|
||||
|
||||
--- given a path, return the directory part and a file part.
|
||||
-- if there's no directory part, the first value will be empty
|
||||
-- @param P A file path
|
||||
function path.splitpath(P)
|
||||
assert_string(1,P)
|
||||
local i = #P
|
||||
local ch = at(P,i)
|
||||
while i > 0 and ch ~= sep and ch ~= other_sep do
|
||||
i = i - 1
|
||||
ch = at(P,i)
|
||||
end
|
||||
if i == 0 then
|
||||
return '',P
|
||||
else
|
||||
return sub(P,1,i-1), sub(P,i+1)
|
||||
end
|
||||
end
|
||||
|
||||
--- return an absolute path.
|
||||
-- @param P A file path
|
||||
function path.abspath(P)
|
||||
assert_string(1,P)
|
||||
if not currentdir then return P end
|
||||
P = P:gsub('[\\/]$','')
|
||||
local pwd = currentdir()
|
||||
if not path.isabs(P) then
|
||||
return path.join(pwd,P)
|
||||
elseif path.is_windows and at(P,2) ~= ':' and at(P,2) ~= '\\' then
|
||||
return pwd:sub(1,2)..P
|
||||
else
|
||||
return P
|
||||
end
|
||||
end
|
||||
|
||||
--- given a path, return the root part and the extension part.
|
||||
-- if there's no extension part, the second value will be empty
|
||||
-- @param P A file path
|
||||
function path.splitext(P)
|
||||
assert_string(1,P)
|
||||
local i = #P
|
||||
local ch = at(P,i)
|
||||
while i > 0 and ch ~= '.' do
|
||||
if ch == sep or ch == other_sep then
|
||||
return P,''
|
||||
end
|
||||
i = i - 1
|
||||
ch = at(P,i)
|
||||
end
|
||||
if i == 0 then
|
||||
return P,''
|
||||
else
|
||||
return sub(P,1,i-1),sub(P,i)
|
||||
end
|
||||
end
|
||||
|
||||
--- return the directory part of a path
|
||||
-- @param P A file path
|
||||
function path.dirname(P)
|
||||
assert_string(1,P)
|
||||
local p1,p2 = path.splitpath(P)
|
||||
return p1
|
||||
end
|
||||
|
||||
--- return the file part of a path
|
||||
-- @param P A file path
|
||||
function path.basename(P)
|
||||
assert_string(1,P)
|
||||
local p1,p2 = path.splitpath(P)
|
||||
return p2
|
||||
end
|
||||
|
||||
--- get the extension part of a path.
|
||||
-- @param P A file path
|
||||
function path.extension(P)
|
||||
assert_string(1,P)
|
||||
local p1,p2 = path.splitext(P)
|
||||
return p2
|
||||
end
|
||||
|
||||
--- is this an absolute path?.
|
||||
-- @param P A file path
|
||||
function path.isabs(P)
|
||||
assert_string(1,P)
|
||||
if path.is_windows then
|
||||
return at(P,1) == '/' or at(P,1)=='\\' or at(P,2)==':'
|
||||
else
|
||||
return at(P,1) == '/'
|
||||
end
|
||||
end
|
||||
|
||||
--- return the P resulting from combining the two paths.
|
||||
-- if the second is already an absolute path, then it returns it.
|
||||
-- @param p1 A file path
|
||||
-- @param p2 A file path
|
||||
function path.join(p1,p2)
|
||||
assert_string(1,p1)
|
||||
assert_string(2,p2)
|
||||
if path.isabs(p2) then return p2 end
|
||||
local endc = at(p1,#p1)
|
||||
if endc ~= path.sep and endc ~= other_sep then
|
||||
p1 = p1..path.sep
|
||||
end
|
||||
return p1..p2
|
||||
end
|
||||
|
||||
--- normalize the case of a pathname. On Unix, this returns the path unchanged;
|
||||
-- for Windows, it converts the path to lowercase, and it also converts forward slashes
|
||||
-- to backward slashes.
|
||||
-- @param P A file path
|
||||
function path.normcase(P)
|
||||
assert_string(1,P)
|
||||
if path.is_windows then
|
||||
return (P:lower():gsub('/','\\'))
|
||||
else
|
||||
return P
|
||||
end
|
||||
end
|
||||
|
||||
--- normalize a path name.
|
||||
-- A//B, A/./B and A/foo/../B all become A/B.
|
||||
-- @param P a file path
|
||||
function path.normpath (P)
|
||||
assert_string(1,P)
|
||||
if path.is_windows then
|
||||
P = P:gsub('/','\\')
|
||||
return (P:gsub('[^\\]+\\%.%.\\',''):gsub('\\%.?\\','\\'))
|
||||
else
|
||||
return (P:gsub('[^/]+/%.%./',''):gsub('/%.?/','/'))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--- Replace a starting '~' with the user's home directory.
|
||||
-- In windows, if HOME isn't set, then USERPROFILE is used in preference to
|
||||
-- HOMEDRIVE HOMEPATH. This is guaranteed to be writeable on all versions of Windows.
|
||||
-- @param P A file path
|
||||
function path.expanduser(P)
|
||||
assert_string(1,P)
|
||||
if at(P,1) == '~' then
|
||||
local home = getenv('HOME')
|
||||
if not home then -- has to be Windows
|
||||
home = getenv 'USERPROFILE' or (getenv 'HOMEDRIVE' .. getenv 'HOMEPATH')
|
||||
end
|
||||
return home..sub(P,2)
|
||||
else
|
||||
return P
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
---Return a suitable full path to a new temporary file name.
|
||||
-- unlike os.tmpnam(), it always gives you a writeable path (uses %TMP% on Windows)
|
||||
function path.tmpname ()
|
||||
local res = tmpnam()
|
||||
if path.is_windows then res = getenv('TMP')..res end
|
||||
return res
|
||||
end
|
||||
|
||||
--- return the largest common prefix path of two paths.
|
||||
-- @param path1 a file path
|
||||
-- @param path2 a file path
|
||||
function path.common_prefix (path1,path2)
|
||||
assert_string(1,path1)
|
||||
assert_string(2,path2)
|
||||
-- get them in order!
|
||||
if #path1 > #path2 then path2,path1 = path1,path2 end
|
||||
for i = 1,#path1 do
|
||||
local c1 = at(path1,i)
|
||||
if c1 ~= at(path2,i) then
|
||||
local cp = path1:sub(1,i-1)
|
||||
if at(path1,i-1) ~= sep then
|
||||
cp = path.dirname(cp)
|
||||
end
|
||||
return cp
|
||||
end
|
||||
end
|
||||
if at(path2,#path1+1) ~= sep then path1 = path.dirname(path1) end
|
||||
return path1
|
||||
--return ''
|
||||
end
|
||||
|
||||
|
||||
--- return the full path where a particular Lua module would be found.
|
||||
-- Both package.path and package.cpath is searched, so the result may
|
||||
-- either be a Lua file or a shared libarary.
|
||||
-- @param mod name of the module
|
||||
-- @return on success: path of module, lua or binary
|
||||
-- @return on error: nil,error string
|
||||
function path.package_path(mod)
|
||||
assert_string(1,mod)
|
||||
local res
|
||||
mod = mod:gsub('%.',sep)
|
||||
res = package.searchpath(mod,package.path)
|
||||
if res then return res,true end
|
||||
res = package.searchpath(mod,package.cpath)
|
||||
if res then return res,false end
|
||||
return raise 'cannot find module on path'
|
||||
end
|
||||
|
||||
|
||||
---- finis -----
|
||||
return path
|
||||
@@ -0,0 +1,65 @@
|
||||
--- Permutation operations.
|
||||
-- @class module
|
||||
-- @name pl.permute
|
||||
local tablex = require 'pl.tablex'
|
||||
local utils = require 'pl.utils'
|
||||
local copy = tablex.deepcopy
|
||||
local append = table.insert
|
||||
local coroutine = coroutine
|
||||
local resume = coroutine.resume
|
||||
local assert_arg = utils.assert_arg
|
||||
|
||||
--[[
|
||||
module ('pl.permute',utils._module)
|
||||
]]
|
||||
|
||||
local permute = {}
|
||||
|
||||
-- PiL, 9.3
|
||||
|
||||
local permgen
|
||||
permgen = function (a, n, fn)
|
||||
if n == 0 then
|
||||
fn(a)
|
||||
else
|
||||
for i=1,n do
|
||||
-- put i-th element as the last one
|
||||
a[n], a[i] = a[i], a[n]
|
||||
|
||||
-- generate all permutations of the other elements
|
||||
permgen(a, n - 1, fn)
|
||||
|
||||
-- restore i-th element
|
||||
a[n], a[i] = a[i], a[n]
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- an iterator over all permutations of the elements of a list.
|
||||
-- Please note that the same list is returned each time, so do not keep references!
|
||||
-- @param a list-like table
|
||||
-- @return an iterator which provides the next permutation as a list
|
||||
function permute.iter (a)
|
||||
assert_arg(1,a,'table')
|
||||
local n = #a
|
||||
local co = coroutine.create(function () permgen(a, n, coroutine.yield) end)
|
||||
return function () -- iterator
|
||||
local code, res = resume(co)
|
||||
return res
|
||||
end
|
||||
end
|
||||
|
||||
--- construct a table containing all the permutations of a list.
|
||||
-- @param a list-like table
|
||||
-- @return a table of tables
|
||||
-- @usage permute.table {1,2,3} --> {{2,3,1},{3,2,1},{3,1,2},{1,3,2},{2,1,3},{1,2,3}}
|
||||
function permute.table (a)
|
||||
assert_arg(1,a,'table')
|
||||
local res = {}
|
||||
local n = #a
|
||||
permgen(a,n,function(t) append(res,copy(t)) end)
|
||||
return res
|
||||
end
|
||||
|
||||
return permute
|
||||
@@ -0,0 +1,101 @@
|
||||
-- experimental support for LuaJava
|
||||
--
|
||||
local path = {}
|
||||
|
||||
|
||||
path.link_attrib = nil
|
||||
|
||||
local File = luajava.bindClass("java.io.File")
|
||||
local Array = luajava.bindClass('java.lang.reflect.Array')
|
||||
|
||||
local function file(s)
|
||||
return luajava.new(File,s)
|
||||
end
|
||||
|
||||
function path.dir(P)
|
||||
local ls = file(P):list()
|
||||
print(ls)
|
||||
local idx,n = -1,Array:getLength(ls)
|
||||
return function ()
|
||||
idx = idx + 1
|
||||
if idx == n then return nil
|
||||
else
|
||||
return Array:get(ls,idx)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function path.mkdir(P)
|
||||
return file(P):mkdir()
|
||||
end
|
||||
|
||||
function path.rmdir(P)
|
||||
return file(P):delete()
|
||||
end
|
||||
|
||||
--- is this a directory?
|
||||
-- @param P A file path
|
||||
function path.isdir(P)
|
||||
if P:match("\\$") then
|
||||
P = P:sub(1,-2)
|
||||
end
|
||||
return file(P):isDirectory()
|
||||
end
|
||||
|
||||
--- is this a file?.
|
||||
-- @param P A file path
|
||||
function path.isfile(P)
|
||||
return file(P):isFile()
|
||||
end
|
||||
|
||||
-- is this a symbolic link?
|
||||
-- Direct support for symbolic links is not provided.
|
||||
-- see http://stackoverflow.com/questions/813710/java-1-6-determine-symbolic-links
|
||||
-- and the caveats therein.
|
||||
-- @param P A file path
|
||||
function path.islink(P)
|
||||
local f = file(P)
|
||||
local canon
|
||||
local parent = f:getParent()
|
||||
if not parent then
|
||||
canon = f
|
||||
else
|
||||
parent = f.getParentFile():getCanonicalFile()
|
||||
canon = luajava.new(File,parent,f:getName())
|
||||
end
|
||||
return canon:getCanonicalFile() ~= canon:getAbsoluteFile()
|
||||
end
|
||||
|
||||
--- return size of a file.
|
||||
-- @param P A file path
|
||||
function path.getsize(P)
|
||||
return file(P):length()
|
||||
end
|
||||
|
||||
--- does a path exist?.
|
||||
-- @param P A file path
|
||||
-- @return the file path if it exists, nil otherwise
|
||||
function path.exists(P)
|
||||
return file(P):exists() and P
|
||||
end
|
||||
|
||||
--- Return the time of last access as the number of seconds since the epoch.
|
||||
-- @param P A file path
|
||||
function path.getatime(P)
|
||||
return path.getmtime(P)
|
||||
end
|
||||
|
||||
--- Return the time of last modification
|
||||
-- @param P A file path
|
||||
function path.getmtime(P)
|
||||
-- Java time is no. of millisec since the epoch
|
||||
return file(P):lastModified()/1000
|
||||
end
|
||||
|
||||
---Return the system's ctime.
|
||||
-- @param P A file path
|
||||
function path.getctime(P)
|
||||
return path.getmtime(P)
|
||||
end
|
||||
|
||||
return path
|
||||
@@ -0,0 +1,224 @@
|
||||
--- Pretty-printing Lua tables.
|
||||
-- Also provides a sandboxed Lua table reader and
|
||||
-- a function to present large numbers in human-friendly format.
|
||||
-- @class module
|
||||
-- @name pl.pretty
|
||||
|
||||
local append = table.insert
|
||||
local concat = table.concat
|
||||
local utils = require 'pl.utils'
|
||||
local lexer = require 'pl.lexer'
|
||||
local assert_arg = utils.assert_arg
|
||||
|
||||
local pretty = {}
|
||||
|
||||
--- read a string representation of a Lua table.
|
||||
-- Uses load(), but tries to be cautious about loading arbitrary code!
|
||||
-- It is expecting a string of the form '{...}', with perhaps some whitespace
|
||||
-- before or after the curly braces. An empty environment is used, and
|
||||
-- any occurance of the keyword 'function' will be considered a problem.
|
||||
-- @param s {string} string of the form '{...}', with perhaps some whitespace
|
||||
-- before or after the curly braces.
|
||||
function pretty.read(s)
|
||||
assert_arg(1,s,'string')
|
||||
if not s:find '^%s*%b{}%s*$' then return nil,"not a Lua table" end
|
||||
if s:find '[^\'"%w_]function[^\'"%w_]' then
|
||||
local tok = lexer.lua(s)
|
||||
for t,v in tok do
|
||||
if t == 'keyword' then
|
||||
return nil,"cannot have Lua keywords in table definition"
|
||||
end
|
||||
end
|
||||
end
|
||||
local chunk,err = utils.load('return '..s,'tbl','t',{})
|
||||
if not chunk then return nil,err end
|
||||
return chunk()
|
||||
end
|
||||
|
||||
local function quote_if_necessary (v)
|
||||
if not v then return ''
|
||||
else
|
||||
if v:find ' ' then v = '"'..v..'"' end
|
||||
end
|
||||
return v
|
||||
end
|
||||
|
||||
local keywords
|
||||
|
||||
|
||||
--- Create a string representation of a Lua table.
|
||||
-- This function never fails, but may complain by returning an
|
||||
-- extra value. Normally puts out one item per line, using
|
||||
-- the provided indent; set the second parameter to '' if
|
||||
-- you want output on one line.
|
||||
-- @param tbl {table} Table to serialize to a string.
|
||||
-- @param space {string} (optional) The indent to use.
|
||||
-- Defaults to two spaces.
|
||||
-- @param not_clever {bool} (optional) Use for plain output, e.g {['key']=1}.
|
||||
-- Defaults to false.
|
||||
-- @return a string
|
||||
-- @return a possible error message
|
||||
function pretty.write (tbl,space,not_clever)
|
||||
if type(tbl) ~= 'table' then
|
||||
local res = tostring(tbl)
|
||||
if type(tbl) == 'string' then res = '"'..res..'"' end
|
||||
return res, 'not a table'
|
||||
end
|
||||
if not keywords then
|
||||
keywords = lexer.get_keywords()
|
||||
end
|
||||
local set = ' = '
|
||||
if space == '' then set = '=' end
|
||||
space = space or ' '
|
||||
local lines = {}
|
||||
local line = ''
|
||||
local tables = {}
|
||||
|
||||
local function is_identifier (s)
|
||||
return (s:find('^[%a_][%w_]*$')) and not keywords[s]
|
||||
end
|
||||
|
||||
local function put(s)
|
||||
if #s > 0 then
|
||||
line = line..s
|
||||
end
|
||||
end
|
||||
|
||||
local function putln (s)
|
||||
if #line > 0 then
|
||||
line = line..s
|
||||
append(lines,line)
|
||||
line = ''
|
||||
else
|
||||
append(lines,s)
|
||||
end
|
||||
end
|
||||
|
||||
local function eat_last_comma ()
|
||||
local n,lastch = #lines
|
||||
local lastch = lines[n]:sub(-1,-1)
|
||||
if lastch == ',' then
|
||||
lines[n] = lines[n]:sub(1,-2)
|
||||
end
|
||||
end
|
||||
|
||||
local function quote (s)
|
||||
return ('%q'):format(tostring(s))
|
||||
end
|
||||
|
||||
local function index (numkey,key)
|
||||
if not numkey then key = quote(key) end
|
||||
return '['..key..']'
|
||||
end
|
||||
|
||||
local writeit
|
||||
writeit = function (t,oldindent,indent)
|
||||
local tp = type(t)
|
||||
if tp ~= 'string' and tp ~= 'table' then
|
||||
putln(quote_if_necessary(tostring(t))..',')
|
||||
elseif tp == 'string' then
|
||||
if t:find('\n') then
|
||||
putln('[[\n'..t..']],')
|
||||
else
|
||||
putln(quote(t)..',')
|
||||
end
|
||||
elseif tp == 'table' then
|
||||
if tables[t] then
|
||||
putln('<cycle>,')
|
||||
return
|
||||
end
|
||||
tables[t] = true
|
||||
local newindent = indent..space
|
||||
putln('{')
|
||||
local used = {}
|
||||
if not not_clever then
|
||||
for i,val in ipairs(t) do
|
||||
put(indent)
|
||||
writeit(val,indent,newindent)
|
||||
used[i] = true
|
||||
end
|
||||
end
|
||||
for key,val in pairs(t) do
|
||||
local numkey = type(key) == 'number'
|
||||
if not_clever then
|
||||
key = tostring(key)
|
||||
put(indent..index(numkey,key)..set)
|
||||
writeit(val,indent,newindent)
|
||||
else
|
||||
if not numkey or not used[key] then -- non-array indices
|
||||
if numkey or not is_identifier(key) then
|
||||
key = index(numkey,key)
|
||||
end
|
||||
put(indent..key..set)
|
||||
writeit(val,indent,newindent)
|
||||
end
|
||||
end
|
||||
end
|
||||
eat_last_comma()
|
||||
putln(oldindent..'},')
|
||||
else
|
||||
putln(tostring(t)..',')
|
||||
end
|
||||
end
|
||||
writeit(tbl,'',space)
|
||||
eat_last_comma()
|
||||
return concat(lines,#space > 0 and '\n' or '')
|
||||
end
|
||||
|
||||
--- Dump a Lua table out to a file or stdout.
|
||||
-- @param t {table} The table to write to a file or stdout.
|
||||
-- @param ... {string} (optional) File name to write too. Defaults to writing
|
||||
-- to stdout.
|
||||
function pretty.dump (t,...)
|
||||
if select('#',...)==0 then
|
||||
print(pretty.write(t))
|
||||
return true
|
||||
else
|
||||
return utils.writefile(...,pretty.write(t))
|
||||
end
|
||||
end
|
||||
|
||||
local memp,nump = {'B','KiB','MiB','GiB'},{'','K','M','B'}
|
||||
|
||||
local comma
|
||||
function comma (val)
|
||||
local thou = math.floor(val/1000)
|
||||
if thou > 0 then return comma(thou)..','..(val % 1000)
|
||||
else return tostring(val) end
|
||||
end
|
||||
|
||||
--- format large numbers nicely for human consumption.
|
||||
-- @param num a number
|
||||
-- @param kind one of 'M' (memory in KiB etc), 'N' (postfixes are 'K','M' and 'B')
|
||||
-- and 'T' (use commas as thousands separator)
|
||||
-- @param prec number of digits to use for 'M' and 'N' (default 1)
|
||||
function pretty.number (num,kind,prec)
|
||||
local fmt = '%.'..(prec or 1)..'f%s'
|
||||
if kind == 'T' then
|
||||
return comma(num)
|
||||
else
|
||||
local postfixes, fact
|
||||
if kind == 'M' then
|
||||
fact = 1024
|
||||
postfixes = memp
|
||||
else
|
||||
fact = 1000
|
||||
postfixes = nump
|
||||
end
|
||||
local div = fact
|
||||
local k = 1
|
||||
while num >= div and k <= #postfixes do
|
||||
div = div * fact
|
||||
k = k + 1
|
||||
end
|
||||
div = div / fact
|
||||
if k > #postfixes then k = k - 1; div = div/fact end
|
||||
if k > 1 then
|
||||
return fmt:format(num/div,postfixes[k] or 'duh')
|
||||
else
|
||||
return num..postfixes[1]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return pretty
|
||||
@@ -0,0 +1,527 @@
|
||||
--- Manipulating sequences as iterators.
|
||||
-- @class module
|
||||
-- @name pl.seq
|
||||
|
||||
local next,assert,type,pairs,tonumber,type,setmetatable,getmetatable,_G = next,assert,type,pairs,tonumber,type,setmetatable,getmetatable,_G
|
||||
local strfind = string.find
|
||||
local strmatch = string.match
|
||||
local format = string.format
|
||||
local mrandom = math.random
|
||||
local remove,tsort,tappend = table.remove,table.sort,table.insert
|
||||
local io = io
|
||||
local utils = require 'pl.utils'
|
||||
local function_arg = utils.function_arg
|
||||
local _List = utils.stdmt.List
|
||||
local _Map = utils.stdmt.Map
|
||||
local assert_arg = utils.assert_arg
|
||||
require 'debug'
|
||||
|
||||
--[[
|
||||
module("pl.seq",utils._module)
|
||||
]]
|
||||
|
||||
local seq = {}
|
||||
|
||||
-- given a number, return a function(y) which returns true if y > x
|
||||
-- @param x a number
|
||||
function seq.greater_than(x)
|
||||
return function(v)
|
||||
return tonumber(v) > x
|
||||
end
|
||||
end
|
||||
|
||||
-- given a number, returns a function(y) which returns true if y < x
|
||||
-- @param x a number
|
||||
function seq.less_than(x)
|
||||
return function(v)
|
||||
return tonumber(v) < x
|
||||
end
|
||||
end
|
||||
|
||||
-- given any value, return a function(y) which returns true if y == x
|
||||
-- @param x a value
|
||||
function seq.equal_to(x)
|
||||
if type(x) == "number" then
|
||||
return function(v)
|
||||
return tonumber(v) == x
|
||||
end
|
||||
else
|
||||
return function(v)
|
||||
return v == x
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- given a string, return a function(y) which matches y against the string.
|
||||
-- @param s a string
|
||||
function seq.matching(s)
|
||||
return function(v)
|
||||
return strfind(v,s)
|
||||
end
|
||||
end
|
||||
|
||||
--- sequence adaptor for a table. Note that if any generic function is
|
||||
-- passed a table, it will automatically use seq.list()
|
||||
-- @param t a list-like table
|
||||
-- @usage sum(list(t)) is the sum of all elements of t
|
||||
-- @usage for x in list(t) do...end
|
||||
function seq.list(t)
|
||||
assert_arg(1,t,'table')
|
||||
local key,value
|
||||
return function()
|
||||
key,value = next(t,key)
|
||||
return value
|
||||
end
|
||||
end
|
||||
|
||||
--- return the keys of the table.
|
||||
-- @param t a list-like table
|
||||
-- @return iterator over keys
|
||||
function seq.keys(t)
|
||||
assert_arg(1,t,'table')
|
||||
local key,value
|
||||
return function()
|
||||
key,value = next(t,key)
|
||||
return key
|
||||
end
|
||||
end
|
||||
|
||||
local list = seq.list
|
||||
local function default_iter(iter)
|
||||
if type(iter) == 'table' then return list(iter)
|
||||
else return iter end
|
||||
end
|
||||
|
||||
seq.iter = default_iter
|
||||
|
||||
--- create an iterator over a numerical range. Like the standard Python function xrange.
|
||||
-- @param start a number
|
||||
-- @param finish a number greater than start
|
||||
function seq.range(start,finish)
|
||||
local i = start - 1
|
||||
return function()
|
||||
i = i + 1
|
||||
if i > finish then return nil
|
||||
else return i end
|
||||
end
|
||||
end
|
||||
|
||||
-- count the number of elements in the sequence which satisfy the predicate
|
||||
-- @param iter a sequence
|
||||
-- @param condn a predicate function (must return either true or false)
|
||||
-- @param optional argument to be passed to predicate as second argument.
|
||||
function seq.count(iter,condn,arg)
|
||||
local i = 0
|
||||
seq.foreach(iter,function(val)
|
||||
if condn(val,arg) then i = i + 1 end
|
||||
end)
|
||||
return i
|
||||
end
|
||||
|
||||
--- return the minimum and the maximum value of the sequence.
|
||||
-- @param iter a sequence
|
||||
function seq.minmax(iter)
|
||||
local vmin,vmax = 1e70,-1e70
|
||||
for v in default_iter(iter) do
|
||||
v = tonumber(v)
|
||||
if v < vmin then vmin = v end
|
||||
if v > vmax then vmax = v end
|
||||
end
|
||||
return vmin,vmax
|
||||
end
|
||||
|
||||
--- return the sum and element count of the sequence.
|
||||
-- @param iter a sequence
|
||||
-- @param fn an optional function to apply to the values
|
||||
function seq.sum(iter,fn)
|
||||
local s = 0
|
||||
local i = 0
|
||||
for v in default_iter(iter) do
|
||||
if fn then v = fn(v) end
|
||||
s = s + v
|
||||
i = i + 1
|
||||
end
|
||||
return s,i
|
||||
end
|
||||
|
||||
--- create a table from the sequence. (This will make the result a List.)
|
||||
-- @param iter a sequence
|
||||
-- @return a List
|
||||
-- @usage copy(list(ls)) is equal to ls
|
||||
-- @usage copy(list {1,2,3}) == List{1,2,3}
|
||||
function seq.copy(iter)
|
||||
local res = {}
|
||||
for v in default_iter(iter) do
|
||||
tappend(res,v)
|
||||
end
|
||||
setmetatable(res,_List)
|
||||
return res
|
||||
end
|
||||
|
||||
--- create a table of pairs from the double-valued sequence.
|
||||
-- @param iter a double-valued sequence
|
||||
-- @param i1 used to capture extra iterator values
|
||||
-- @param i2 as with pairs & ipairs
|
||||
-- @usage copy2(ipairs{10,20,30}) == {{1,10},{2,20},{3,30}}
|
||||
-- @return a list-like table
|
||||
function seq.copy2 (iter,i1,i2)
|
||||
local res = {}
|
||||
for v1,v2 in iter,i1,i2 do
|
||||
tappend(res,{v1,v2})
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
--- create a table of 'tuples' from a multi-valued sequence.
|
||||
-- A generalization of copy2 above
|
||||
-- @param iter a multiple-valued sequence
|
||||
-- @return a list-like table
|
||||
function seq.copy_tuples (iter)
|
||||
iter = default_iter(iter)
|
||||
local res = {}
|
||||
local row = {iter()}
|
||||
while #row > 0 do
|
||||
tappend(res,row)
|
||||
row = {iter()}
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
--- return an iterator of random numbers.
|
||||
-- @param n the length of the sequence
|
||||
-- @param l same as the first optional argument to math.random
|
||||
-- @param u same as the second optional argument to math.random
|
||||
-- @return a sequnce
|
||||
function seq.random(n,l,u)
|
||||
local rand
|
||||
assert(type(n) == 'number')
|
||||
if u then
|
||||
rand = function() return mrandom(l,u) end
|
||||
elseif l then
|
||||
rand = function() return mrandom(l) end
|
||||
else
|
||||
rand = mrandom
|
||||
end
|
||||
|
||||
return function()
|
||||
if n == 0 then return nil
|
||||
else
|
||||
n = n - 1
|
||||
return rand()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- return an iterator to the sorted elements of a sequence.
|
||||
-- @param iter a sequence
|
||||
-- @param comp an optional comparison function (comp(x,y) is true if x < y)
|
||||
function seq.sort(iter,comp)
|
||||
local t = seq.copy(iter)
|
||||
tsort(t,comp)
|
||||
return list(t)
|
||||
end
|
||||
|
||||
--- return an iterator which returns elements of two sequences.
|
||||
-- @param iter1 a sequence
|
||||
-- @param iter2 a sequence
|
||||
-- @usage for x,y in seq.zip(ls1,ls2) do....end
|
||||
function seq.zip(iter1,iter2)
|
||||
iter1 = default_iter(iter1)
|
||||
iter2 = default_iter(iter2)
|
||||
return function()
|
||||
return iter1(),iter2()
|
||||
end
|
||||
end
|
||||
|
||||
--- A table where the key/values are the values and value counts of the sequence.
|
||||
-- This version works with 'hashable' values like strings and numbers. <br>
|
||||
-- pl.tablex.count_map is more general.
|
||||
-- @param iter a sequence
|
||||
-- @return a map-like table
|
||||
-- @return a table
|
||||
-- @see pl.tablex.count_map
|
||||
function seq.count_map(iter)
|
||||
local t = {}
|
||||
local v
|
||||
for s in default_iter(iter) do
|
||||
v = t[s]
|
||||
if v then t[s] = v + 1
|
||||
else t[s] = 1 end
|
||||
end
|
||||
return setmetatable(t,_Map)
|
||||
end
|
||||
|
||||
-- given a sequence, return all the unique values in that sequence.
|
||||
-- @param iter a sequence
|
||||
-- @param returns_table true if we return a table, not a sequence
|
||||
-- @return a sequence or a table; defaults to a sequence.
|
||||
function seq.unique(iter,returns_table)
|
||||
local t = seq.count_map(iter)
|
||||
local res = {}
|
||||
for k in pairs(t) do tappend(res,k) end
|
||||
table.sort(res)
|
||||
if returns_table then
|
||||
return res
|
||||
else
|
||||
return list(res)
|
||||
end
|
||||
end
|
||||
|
||||
-- print out a sequence @iter, with a separator @sep (default space)
|
||||
-- and maximum number of values per line @nfields (default 7)
|
||||
-- @fmt is an optional format function to create a representation of each value.
|
||||
function seq.printall(iter,sep,nfields,fmt)
|
||||
local write = io.write
|
||||
if not sep then sep = ' ' end
|
||||
if not nfields then
|
||||
if sep == '\n' then nfields = 1e30
|
||||
else nfields = 7 end
|
||||
end
|
||||
if fmt then
|
||||
local fstr = fmt
|
||||
fmt = function(v) return format(fstr,v) end
|
||||
end
|
||||
local k = 1
|
||||
for v in default_iter(iter) do
|
||||
if fmt then v = fmt(v) end
|
||||
if k < nfields then
|
||||
write(v,sep)
|
||||
k = k + 1
|
||||
else
|
||||
write(v,'\n')
|
||||
k = 1
|
||||
end
|
||||
end
|
||||
write '\n'
|
||||
end
|
||||
|
||||
-- return an iterator running over every element of two sequences (concatenation).
|
||||
-- @param iter1 a sequence
|
||||
-- @param iter2 a sequence
|
||||
function seq.splice(iter1,iter2)
|
||||
iter1 = default_iter(iter1)
|
||||
iter2 = default_iter(iter2)
|
||||
local iter = iter1
|
||||
return function()
|
||||
local ret = iter()
|
||||
if ret == nil then
|
||||
if iter == iter1 then
|
||||
iter = iter2
|
||||
return iter()
|
||||
else return nil end
|
||||
else
|
||||
return ret
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- return a sequence where every element of a sequence has been transformed
|
||||
-- by a function. If you don't supply an argument, then the function will
|
||||
-- receive both values of a double-valued sequence, otherwise behaves rather like
|
||||
-- tablex.map.
|
||||
-- @param fn a function to apply to elements; may take two arguments
|
||||
-- @param iter a sequence of one or two values
|
||||
-- @param arg optional argument to pass to function.
|
||||
function seq.map(fn,iter,arg)
|
||||
fn = function_arg(1,fn)
|
||||
iter = default_iter(iter)
|
||||
return function()
|
||||
local v1,v2 = iter()
|
||||
if v1 == nil then return nil end
|
||||
if arg then return fn(v1,arg) or false
|
||||
else return fn(v1,v2) or false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- filter a sequence using a predicate function
|
||||
-- @param iter a sequence of one or two values
|
||||
-- @param pred a boolean function; may take two arguments
|
||||
-- @param arg optional argument to pass to function.
|
||||
function seq.filter (iter,pred,arg)
|
||||
pred = function_arg(2,pred)
|
||||
return function ()
|
||||
local v1,v2
|
||||
while true do
|
||||
v1,v2 = iter()
|
||||
if v1 == nil then return nil end
|
||||
if arg then
|
||||
if pred(v1,arg) then return v1,v2 end
|
||||
else
|
||||
if pred(v1,v2) then return v1,v2 end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- 'reduce' a sequence using a binary function.
|
||||
-- @param fun a function of two arguments
|
||||
-- @param iter a sequence
|
||||
-- @param oldval optional initial value
|
||||
-- @usage seq.reduce(operator.add,seq.list{1,2,3,4}) == 10
|
||||
-- @usage seq.reduce('-',{1,2,3,4,5}) == -13
|
||||
function seq.reduce (fun,iter,oldval)
|
||||
fun = function_arg(1,fun)
|
||||
iter = default_iter(iter)
|
||||
if not oldval then
|
||||
oldval = iter()
|
||||
end
|
||||
local val = oldval
|
||||
for v in iter do
|
||||
val = fun(val,v)
|
||||
end
|
||||
return val
|
||||
end
|
||||
|
||||
--- take the first n values from the sequence.
|
||||
-- @param iter a sequence of one or two values
|
||||
-- @param n number of items to take
|
||||
-- @return a sequence of at most n items
|
||||
function seq.take (iter,n)
|
||||
local i = 1
|
||||
iter = default_iter(iter)
|
||||
return function()
|
||||
if i > n then return end
|
||||
local val1,val2 = iter()
|
||||
if not val1 then return end
|
||||
i = i + 1
|
||||
return val1,val2
|
||||
end
|
||||
end
|
||||
|
||||
--- skip the first n values of a sequence
|
||||
-- @param iter a sequence of one or more values
|
||||
-- @param n number of items to skip
|
||||
function seq.skip (iter,n)
|
||||
n = n or 1
|
||||
for i = 1,n do iter() end
|
||||
return iter
|
||||
end
|
||||
|
||||
--- a sequence with a sequence count and the original value. <br>
|
||||
-- enum(copy(ls)) is a roundabout way of saying ipairs(ls).
|
||||
-- @param iter a single or double valued sequence
|
||||
-- @return sequence of (i,v), i = 1..n and v is from iter.
|
||||
function seq.enum (iter)
|
||||
local i = 0
|
||||
iter = default_iter(iter)
|
||||
return function ()
|
||||
local val1,val2 = iter()
|
||||
if not val1 then return end
|
||||
i = i + 1
|
||||
return i,val1,val2
|
||||
end
|
||||
end
|
||||
|
||||
--- map using a named method over a sequence.
|
||||
-- @param iter a sequence
|
||||
-- @param name the method name
|
||||
-- @param arg1 optional first extra argument
|
||||
-- @param arg2 optional second extra argument
|
||||
function seq.mapmethod (iter,name,arg1,arg2)
|
||||
iter = default_iter(iter)
|
||||
return function()
|
||||
local val = iter()
|
||||
if not val then return end
|
||||
local fn = val[name]
|
||||
if not fn then error(type(val).." does not have method "..name) end
|
||||
return fn(val,arg1,arg2)
|
||||
end
|
||||
end
|
||||
|
||||
--- a sequence of (last,current) values from another sequence.
|
||||
-- This will return S(i-1),S(i) if given S(i)
|
||||
-- @param iter a sequence
|
||||
function seq.last (iter)
|
||||
iter = default_iter(iter)
|
||||
local l = iter()
|
||||
if l == nil then return nil end
|
||||
return function ()
|
||||
local val,ll
|
||||
val = iter()
|
||||
if val == nil then return nil end
|
||||
ll = l
|
||||
l = val
|
||||
return val,ll
|
||||
end
|
||||
end
|
||||
|
||||
--- call the function on each element of the sequence.
|
||||
-- @param iter a sequence with up to 3 values
|
||||
-- @param fn a function
|
||||
function seq.foreach(iter,fn)
|
||||
fn = function_arg(2,fn)
|
||||
for i1,i2,i3 in default_iter(iter) do fn(i1,i2,i3) end
|
||||
end
|
||||
|
||||
---------------------- Sequence Adapters ---------------------
|
||||
|
||||
local SMT
|
||||
local callable = utils.is_callable
|
||||
|
||||
local function SW (iter,...)
|
||||
if callable(iter) then
|
||||
return setmetatable({iter=iter},SMT)
|
||||
else
|
||||
return iter,...
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- can't directly look these up in seq because of the wrong argument order...
|
||||
local map,reduce,mapmethod = seq.map, seq.reduce, seq.mapmethod
|
||||
local overrides = {
|
||||
map = function(self,fun,arg)
|
||||
return map(fun,self,arg)
|
||||
end,
|
||||
reduce = function(self,fun)
|
||||
return reduce(fun,self)
|
||||
end
|
||||
}
|
||||
|
||||
SMT = {
|
||||
__index = function (tbl,key)
|
||||
local s = overrides[key] or seq[key]
|
||||
if s then
|
||||
return function(sw,...) return SW(s(sw.iter,...)) end
|
||||
else
|
||||
return function(sw,...) return SW(mapmethod(sw.iter,key,...)) end
|
||||
end
|
||||
end,
|
||||
__call = function (sw)
|
||||
return sw.iter()
|
||||
end,
|
||||
}
|
||||
|
||||
setmetatable(seq,{
|
||||
__call = function(tbl,iter)
|
||||
if not callable(iter) then
|
||||
if type(iter) == 'table' then iter = seq.list(iter)
|
||||
else return iter
|
||||
end
|
||||
end
|
||||
return setmetatable({iter=iter},SMT)
|
||||
end
|
||||
})
|
||||
|
||||
--- create a wrapped iterator over all lines in the file.
|
||||
-- @param f either a filename or nil (for standard input)
|
||||
-- @return a sequence wrapper
|
||||
function seq.lines (f)
|
||||
local iter = f and io.lines(f) or io.lines()
|
||||
return SW(iter)
|
||||
end
|
||||
|
||||
function seq.import ()
|
||||
_G.debug.setmetatable(function() end,{
|
||||
__index = function(tbl,key)
|
||||
local s = overrides[key] or seq[key]
|
||||
if s then return s
|
||||
else
|
||||
return function(s,...) return seq.mapmethod(s,key,...) end
|
||||
end
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
return seq
|
||||
@@ -0,0 +1,335 @@
|
||||
--- Simple Input Patterns (SIP). <p>
|
||||
-- SIP patterns start with '$', then a
|
||||
-- one-letter type, and then an optional variable in curly braces. <p>
|
||||
-- Example:
|
||||
-- <pre class=example>
|
||||
-- sip.match('$v=$q','name="dolly"',res)
|
||||
-- ==> res=={'name','dolly'}
|
||||
-- sip.match('($q{first},$q{second})','("john","smith")',res)
|
||||
-- ==> res=={second='smith',first='john'}
|
||||
-- </pre>
|
||||
-- <pre>
|
||||
-- <b>Type names</b>
|
||||
-- v identifier
|
||||
-- i integer
|
||||
-- f floating-point
|
||||
-- q quoted string
|
||||
-- ([{< match up to closing bracket
|
||||
-- </pre>
|
||||
-- <p>
|
||||
-- See <a href="../../index.html#sip">the Guide</a>
|
||||
-- @class module
|
||||
-- @name pl.sip
|
||||
|
||||
local append,concat = table.insert,table.concat
|
||||
local concat = table.concat
|
||||
local ipairs,loadstring,type,unpack = ipairs,loadstring,type,unpack
|
||||
local io,_G = io,_G
|
||||
local print,rawget = print,rawget
|
||||
|
||||
local patterns = {
|
||||
FLOAT = '[%+%-%d]%d*%.?%d*[eE]?[%+%-]?%d*',
|
||||
INTEGER = '[+%-%d]%d*',
|
||||
IDEN = '[%a_][%w_]*',
|
||||
FILE = '[%a%.\\][:%][%w%._%-\\]*'
|
||||
}
|
||||
|
||||
local function assert_arg(idx,val,tp)
|
||||
if type(val) ~= tp then
|
||||
error("argument "..idx.." must be "..tp, 2)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--[[
|
||||
module ('pl.sip',utils._module)
|
||||
]]
|
||||
|
||||
local sip = {}
|
||||
|
||||
local brackets = {['<'] = '>', ['('] = ')', ['{'] = '}', ['['] = ']' }
|
||||
local stdclasses = {a=1,c=0,d=1,l=1,p=0,u=1,w=1,x=1,s=0}
|
||||
|
||||
local _patterns = {}
|
||||
|
||||
|
||||
local function group(s)
|
||||
return '('..s..')'
|
||||
end
|
||||
|
||||
-- escape all magic characters except $, which has special meaning
|
||||
-- Also, un-escape any characters after $, so $( passes through as is.
|
||||
local function escape (spec)
|
||||
--_G.print('spec',spec)
|
||||
local res = spec:gsub('[%-%.%+%[%]%(%)%^%%%?%*]','%%%1'):gsub('%$%%(%S)','$%1')
|
||||
--_G.print('res',res)
|
||||
return res
|
||||
end
|
||||
|
||||
local function imcompressible (s)
|
||||
return s:gsub('%s+','\001')
|
||||
end
|
||||
|
||||
-- [handling of spaces in patterns]
|
||||
-- spaces may be 'compressed' (i.e will match zero or more spaces)
|
||||
-- unless this occurs within a number or an identifier. So we mark
|
||||
-- the four possible imcompressible patterns first and then replace.
|
||||
-- The possible alnum patterns are v,f,a,d,x,l and u.
|
||||
local function compress_spaces (s)
|
||||
s = s:gsub('%$[vifadxlu]%s+%$[vfadxlu]',imcompressible)
|
||||
s = s:gsub('[%w_]%s+[%w_]',imcompressible)
|
||||
s = s:gsub('[%w_]%s+%$[vfadxlu]',imcompressible)
|
||||
s = s:gsub('%$[vfadxlu]%s+[%w_]',imcompressible)
|
||||
s = s:gsub('%s+','%%s*')
|
||||
s = s:gsub('\001',' ')
|
||||
return s
|
||||
end
|
||||
|
||||
--- convert a SIP pattern into the equivalent Lua string pattern.
|
||||
-- @param spec a SIP pattern
|
||||
-- @param options a table; only the <code>at_start</code> field is
|
||||
-- currently meaningful and esures that the pattern is anchored
|
||||
-- at the start of the string.
|
||||
-- @return a Lua string pattern.
|
||||
function sip.create_pattern (spec,options)
|
||||
assert_arg(1,spec,'string')
|
||||
local fieldnames,fieldtypes = {},{}
|
||||
|
||||
if type(spec) == 'string' then
|
||||
spec = escape(spec)
|
||||
else
|
||||
local res = {}
|
||||
for i,s in ipairs(spec) do
|
||||
res[i] = escape(s)
|
||||
end
|
||||
spec = concat(res,'.-')
|
||||
end
|
||||
|
||||
local kount = 1
|
||||
|
||||
local function addfield (name,type)
|
||||
if not name then name = kount end
|
||||
if fieldnames then append(fieldnames,name) end
|
||||
if fieldtypes then fieldtypes[name] = type end
|
||||
kount = kount + 1
|
||||
end
|
||||
|
||||
local named_vars, pattern
|
||||
named_vars = spec:find('{%a+}')
|
||||
pattern = '%$%S'
|
||||
|
||||
if options and options.at_start then
|
||||
spec = '^'..spec
|
||||
end
|
||||
if spec:sub(-1,-1) == '$' then
|
||||
spec = spec:sub(1,-2)..'$r'
|
||||
if named_vars then spec = spec..'{rest}' end
|
||||
end
|
||||
|
||||
|
||||
local names
|
||||
|
||||
if named_vars then
|
||||
names = {}
|
||||
spec = spec:gsub('{(%a+)}',function(name)
|
||||
append(names,name)
|
||||
return ''
|
||||
end)
|
||||
end
|
||||
spec = compress_spaces(spec)
|
||||
|
||||
local k = 1
|
||||
local err
|
||||
local r = (spec:gsub(pattern,function(s)
|
||||
local type,name
|
||||
type = s:sub(2,2)
|
||||
if names then name = names[k]; k=k+1 end
|
||||
-- this kludge is necessary because %q generates two matches, and
|
||||
-- we want to ignore the first. Not a problem for named captures.
|
||||
if not names and type == 'q' then
|
||||
addfield(nil,'Q')
|
||||
else
|
||||
addfield(name,type)
|
||||
end
|
||||
local res
|
||||
if type == 'v' then
|
||||
res = group(patterns.IDEN)
|
||||
elseif type == 'i' then
|
||||
res = group(patterns.INTEGER)
|
||||
elseif type == 'f' then
|
||||
res = group(patterns.FLOAT)
|
||||
elseif type == 'r' then
|
||||
res = '(%S.*)'
|
||||
elseif type == 'q' then
|
||||
-- some Lua pattern matching voodoo; we want to match '...' as
|
||||
-- well as "...", and can use the fact that %n will match a
|
||||
-- previous capture. Adding the extra field above comes from needing
|
||||
-- to accomodate the extra spurious match (which is either ' or ")
|
||||
addfield(name,type)
|
||||
res = '(["\'])(.-)%'..(kount-2)
|
||||
elseif type == 'p' then
|
||||
res = '([%a]?[:]?[\\/%.%w_]+)'
|
||||
else
|
||||
local endbracket = brackets[type]
|
||||
if endbracket then
|
||||
res = '(%b'..type..endbracket..')'
|
||||
elseif stdclasses[type] or stdclasses[type:lower()] then
|
||||
res = '(%'..type..'+)'
|
||||
else
|
||||
err = "unknown format type or character class"
|
||||
end
|
||||
end
|
||||
return res
|
||||
end))
|
||||
--print(r,err)
|
||||
if err then
|
||||
return nil,err
|
||||
else
|
||||
return r,fieldnames,fieldtypes
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function tnumber (s)
|
||||
return s == 'd' or s == 'i' or s == 'f'
|
||||
end
|
||||
|
||||
function sip.create_spec_fun(spec,options)
|
||||
local fieldtypes,fieldnames
|
||||
local ls = {}
|
||||
spec,fieldnames,fieldtypes = sip.create_pattern(spec,options)
|
||||
if not spec then return spec,fieldnames end
|
||||
local named_vars = type(fieldnames[1]) == 'string'
|
||||
for i = 1,#fieldnames do
|
||||
append(ls,'mm'..i)
|
||||
end
|
||||
local fun = ('return (function(s,res)\n\tlocal %s = s:match(%q)\n'):format(concat(ls,','),spec)
|
||||
fun = fun..'\tif not mm1 then return false end\n'
|
||||
local k=1
|
||||
for i,f in ipairs(fieldnames) do
|
||||
if f ~= '_' then
|
||||
local var = 'mm'..i
|
||||
if tnumber(fieldtypes[f]) then
|
||||
var = 'tonumber('..var..')'
|
||||
elseif brackets[fieldtypes[f]] then
|
||||
var = var..':sub(2,-2)'
|
||||
end
|
||||
if named_vars then
|
||||
fun = ('%s\tres.%s = %s\n'):format(fun,f,var)
|
||||
else
|
||||
if fieldtypes[f] ~= 'Q' then -- we skip the string-delim capture
|
||||
fun = ('%s\tres[%d] = %s\n'):format(fun,k,var)
|
||||
k = k + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return fun..'\treturn true\nend)\n', named_vars
|
||||
end
|
||||
|
||||
--- convert a SIP pattern into a matching function.
|
||||
-- The returned function takes two arguments, the line and an empty table.
|
||||
-- If the line matched the pattern, then this function return true
|
||||
-- and the table is filled with field-value pairs.
|
||||
-- @param spec a SIP pattern
|
||||
-- @param options optional table; {anywhere=true} will stop pattern anchoring at start
|
||||
-- @return a function if successful, or nil,<error>
|
||||
function sip.compile(spec,options)
|
||||
assert_arg(1,spec,'string')
|
||||
local fun,names = sip.create_spec_fun(spec,options)
|
||||
if not fun then return nil,names end
|
||||
if rawget(_G,'_DEBUG') then print(fun) end
|
||||
local chunk,err = loadstring(fun,'tmp')
|
||||
if err then return nil,err end
|
||||
return chunk(),names
|
||||
end
|
||||
|
||||
local cache = {}
|
||||
|
||||
--- match a SIP pattern against a string.
|
||||
-- @param spec a SIP pattern
|
||||
-- @param line a string
|
||||
-- @param res a table to receive values
|
||||
-- @param options (optional) option table
|
||||
-- @return true or false
|
||||
function sip.match (spec,line,res,options)
|
||||
assert_arg(1,spec,'string')
|
||||
assert_arg(2,line,'string')
|
||||
assert_arg(3,res,'table')
|
||||
if not cache[spec] then
|
||||
cache[spec] = sip.compile(spec,options)
|
||||
end
|
||||
return cache[spec](line,res)
|
||||
end
|
||||
|
||||
--- match a SIP pattern against the start of a string.
|
||||
-- @param spec a SIP pattern
|
||||
-- @param line a string
|
||||
-- @param res a table to receive values
|
||||
-- @return true or false
|
||||
function sip.match_at_start (spec,line,res)
|
||||
return sip.match(spec,line,res,{at_start=true})
|
||||
end
|
||||
|
||||
--- given a pattern and a file object, return an iterator over the results
|
||||
-- @param spec a SIP pattern
|
||||
-- @param f a file - use standard input if not specified.
|
||||
function sip.fields (spec,f)
|
||||
assert_arg(1,spec,'string')
|
||||
f = f or io.stdin
|
||||
local fun,err = sip.compile(spec)
|
||||
if not fun then return nil,err end
|
||||
local res = {}
|
||||
return function()
|
||||
while true do
|
||||
local line = f:read()
|
||||
if not line then return end
|
||||
if fun(line,res) then
|
||||
local values = res
|
||||
res = {}
|
||||
return unpack(values)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- register a match which will be used in the read function.
|
||||
-- @param spec a SIP pattern
|
||||
-- @param fun a function to be called with the results of the match
|
||||
-- @see read
|
||||
function sip.pattern (spec,fun)
|
||||
assert_arg(1,spec,'string')
|
||||
local pat,named = sip.compile(spec)
|
||||
append(_patterns,{pat=pat,named=named,callback=fun or false})
|
||||
end
|
||||
|
||||
--- enter a loop which applies all registered matches to the input file.
|
||||
-- @param f a file object; if nil, then io.stdin is assumed.
|
||||
function sip.read (f)
|
||||
local owned,err
|
||||
f = f or io.stdin
|
||||
if type(f) == 'string' then
|
||||
f,err = io.open(f)
|
||||
if not f then return nil,err end
|
||||
owned = true
|
||||
end
|
||||
local res = {}
|
||||
for line in f:lines() do
|
||||
for _,item in ipairs(_patterns) do
|
||||
if item.pat(line,res) then
|
||||
if item.callback then
|
||||
if item.named then
|
||||
item.callback(res)
|
||||
else
|
||||
item.callback(unpack(res))
|
||||
end
|
||||
end
|
||||
res = {}
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if owned then f:close() end
|
||||
end
|
||||
|
||||
return sip
|
||||
@@ -0,0 +1,71 @@
|
||||
--- Checks uses of undeclared global variables.
|
||||
-- All global variables must be 'declared' through a regular assignment
|
||||
-- (even assigning nil will do) in a main chunk before being used
|
||||
-- anywhere or assigned to inside a function.
|
||||
-- @class module
|
||||
-- @name pl.strict
|
||||
|
||||
require 'debug'
|
||||
local getinfo, error, rawset, rawget = debug.getinfo, error, rawset, rawget
|
||||
local handler,hooked
|
||||
|
||||
local mt = getmetatable(_G)
|
||||
if mt == nil then
|
||||
mt = {}
|
||||
setmetatable(_G, mt)
|
||||
elseif mt.hook then
|
||||
hooked = true
|
||||
end
|
||||
|
||||
-- predeclaring _PROMPT keeps the Lua Interpreter happy
|
||||
mt.__declared = {_PROMPT=true}
|
||||
|
||||
local function what ()
|
||||
local d = getinfo(3, "S")
|
||||
return d and d.what or "C"
|
||||
end
|
||||
|
||||
mt.__newindex = function (t, n, v)
|
||||
if not mt.__declared[n] then
|
||||
local w = what()
|
||||
if w ~= "main" and w ~= "C" then
|
||||
error("assign to undeclared variable '"..n.."'", 2)
|
||||
end
|
||||
mt.__declared[n] = true
|
||||
end
|
||||
rawset(t, n, v)
|
||||
end
|
||||
|
||||
handler = function(t,n)
|
||||
if not mt.__declared[n] and what() ~= "C" then
|
||||
error("variable '"..n.."' is not declared", 2)
|
||||
end
|
||||
return rawget(t, n)
|
||||
end
|
||||
|
||||
function package.strict (mod)
|
||||
local mt = getmetatable(mod)
|
||||
if mt == nil then
|
||||
mt = {}
|
||||
setmetatable(mod, mt)
|
||||
end
|
||||
mt.__declared = {}
|
||||
mt.__newindex = function(t, n, v)
|
||||
mt.__declared[n] = true
|
||||
rawset(t, n, v)
|
||||
end
|
||||
mt.__index = function(t,n)
|
||||
if not mt.__declared[n] then
|
||||
error("variable '"..n.."' is not declared", 2)
|
||||
end
|
||||
return rawget(t, n)
|
||||
end
|
||||
end
|
||||
|
||||
if not hooked then
|
||||
mt.__index = handler
|
||||
else
|
||||
mt.hook(handler)
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
--- Reading and writing strings using file-like objects. <br>
|
||||
-- <pre class=example>
|
||||
-- f = stringio.open(text)
|
||||
-- l1 = f:read() -- read first line
|
||||
-- n,m = f:read ('*n','*n') -- read two numbers
|
||||
-- for line in f:lines() do print(line) end -- iterate over all lines
|
||||
-- f = stringio.create()
|
||||
-- f:write('hello')
|
||||
-- f:write('dolly')
|
||||
-- assert(f:value(),'hellodolly')
|
||||
-- </pre>
|
||||
-- See <a href="../../index.html#stringio">the Guide</a>.
|
||||
-- @class module
|
||||
-- @name pl.stringio
|
||||
|
||||
local getmetatable,tostring,unpack,tonumber = getmetatable,tostring,unpack,tonumber
|
||||
local concat,append = table.concat,table.insert
|
||||
|
||||
local stringio = {}
|
||||
|
||||
--- Writer class
|
||||
local SW = {}
|
||||
SW.__index = SW
|
||||
|
||||
local function xwrite(self,...)
|
||||
local args = {...} --arguments may not be nil!
|
||||
for i = 1, #args do
|
||||
append(self.tbl,args[i])
|
||||
end
|
||||
end
|
||||
|
||||
function SW:write(arg1,arg2,...)
|
||||
if arg2 then
|
||||
xwrite(self,arg1,arg2,...)
|
||||
else
|
||||
append(self.tbl,arg1)
|
||||
end
|
||||
end
|
||||
|
||||
function SW:writef(fmt,...)
|
||||
self:write(fmt:format(...))
|
||||
end
|
||||
|
||||
function SW:value()
|
||||
return concat(self.tbl)
|
||||
end
|
||||
|
||||
function SW:close() -- for compatibility only
|
||||
end
|
||||
|
||||
function SW:seek()
|
||||
end
|
||||
|
||||
--- Reader class
|
||||
local SR = {}
|
||||
SR.__index = SR
|
||||
|
||||
function SR:_read(fmt)
|
||||
local i,str = self.i,self.str
|
||||
local sz = #str
|
||||
if i >= sz then return nil end
|
||||
local res
|
||||
if fmt == nil or fmt == '*l' then
|
||||
local idx = str:find('\n',i) or (sz+1)
|
||||
res = str:sub(i,idx-1)
|
||||
self.i = idx+1
|
||||
elseif fmt == '*a' then
|
||||
res = str:sub(i)
|
||||
self.i = sz
|
||||
elseif fmt == '*n' then
|
||||
local _,i2,i2,idx
|
||||
_,idx = str:find ('%s*%d+',i)
|
||||
_,i2 = str:find ('%.%d+',idx+1)
|
||||
if i2 then idx = i2 end
|
||||
_,i2 = str:find ('[eE][%+%-]*%d+',idx+1)
|
||||
if i2 then idx = i2 end
|
||||
local val = str:sub(i,idx)
|
||||
res = tonumber(val)
|
||||
self.i = idx+1
|
||||
elseif type(fmt) == 'number' then
|
||||
res = str:sub(i,i+fmt-1)
|
||||
self.i = i + fmt
|
||||
else
|
||||
error("bad read format",2)
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
function SR:read(...)
|
||||
local fmts = {...}
|
||||
if #fmts <= 1 then
|
||||
return self:_read(fmts[1])
|
||||
else
|
||||
local res = {}
|
||||
for i = 1, #fmts do
|
||||
res[i] = self:_read(fmts[i])
|
||||
end
|
||||
return unpack(res)
|
||||
end
|
||||
end
|
||||
|
||||
function SR:seek(whence,offset)
|
||||
local base
|
||||
whence = whence or 'cur'
|
||||
offset = offset or 0
|
||||
if whence == 'set' then
|
||||
base = 1
|
||||
elseif whence == 'cur' then
|
||||
base = self.i
|
||||
elseif whence == 'end' then
|
||||
base = #self.str
|
||||
end
|
||||
self.i = base + offset
|
||||
return self.i
|
||||
end
|
||||
|
||||
function SR:lines()
|
||||
return function()
|
||||
return self:read()
|
||||
end
|
||||
end
|
||||
|
||||
function SR:close() -- for compatibility only
|
||||
end
|
||||
|
||||
--- create a file-like object which can be used to construct a string.
|
||||
-- The resulting object has an extra <code>value()</code> method for
|
||||
-- retrieving the string value.
|
||||
-- @usage f = create(); f:write('hello, dolly\n'); print(f:value())
|
||||
function stringio.create()
|
||||
return setmetatable({tbl={}},SW)
|
||||
end
|
||||
|
||||
--- create a file-like object for reading from a given string.
|
||||
-- @param s The input string.
|
||||
function stringio.open(s)
|
||||
return setmetatable({str=s,i=1},SR)
|
||||
end
|
||||
|
||||
function stringio.lines(s)
|
||||
return stringio.open(s):lines()
|
||||
end
|
||||
|
||||
return stringio
|
||||
@@ -0,0 +1,441 @@
|
||||
--- Python-style string library. <p>
|
||||
-- see 3.6.1 of the Python reference. <p>
|
||||
-- If you want to make these available as string methods, then say
|
||||
-- <code>stringx.import()</code> to bring them into the standard <code>string</code>
|
||||
-- table.
|
||||
-- @class module
|
||||
-- @name pl.stringx
|
||||
local string = string
|
||||
local find = string.find
|
||||
local type,setmetatable,getmetatable,ipairs,unpack = type,setmetatable,getmetatable,ipairs,unpack
|
||||
local error,tostring = error,tostring
|
||||
local gsub = string.gsub
|
||||
local rep = string.rep
|
||||
local sub = string.sub
|
||||
local concat = table.concat
|
||||
local utils = require 'pl.utils'
|
||||
local escape = utils.escape
|
||||
local ceil = math.ceil
|
||||
local _G = _G
|
||||
local assert_arg,usplit,list_MT = utils.assert_arg,utils.split,utils.stdmt.List
|
||||
local lstrip
|
||||
|
||||
local function assert_string (n,s)
|
||||
assert_arg(n,s,'string')
|
||||
end
|
||||
|
||||
local function non_empty(s)
|
||||
return #s > 0
|
||||
end
|
||||
|
||||
local function assert_nonempty_string(n,s)
|
||||
assert_arg(n,s,'string',non_empty,'must be a non-empty string')
|
||||
end
|
||||
|
||||
--[[
|
||||
module ('pl.stringx',utils._module)
|
||||
]]
|
||||
|
||||
local stringx = {}
|
||||
|
||||
--- does s only contain alphabetic characters?.
|
||||
-- @param s a string
|
||||
function stringx.isalpha(s)
|
||||
assert_string(1,s)
|
||||
return find(s,'^%a+$') == 1
|
||||
end
|
||||
|
||||
--- does s only contain digits?.
|
||||
-- @param s a string
|
||||
function stringx.isdigit(s)
|
||||
assert_string(1,s)
|
||||
return find(s,'^%d+$') == 1
|
||||
end
|
||||
|
||||
--- does s only contain alphanumeric characters?.
|
||||
-- @param s a string
|
||||
function stringx.isalnum(s)
|
||||
assert_string(1,s)
|
||||
return find(s,'^%w+$') == 1
|
||||
end
|
||||
|
||||
--- does s only contain spaces?.
|
||||
-- @param s a string
|
||||
function stringx.isspace(s)
|
||||
assert_string(1,s)
|
||||
return find(s,'^%s+$') == 1
|
||||
end
|
||||
|
||||
--- does s only contain lower case characters?.
|
||||
-- @param s a string
|
||||
function stringx.islower(s)
|
||||
assert_string(1,s)
|
||||
return find(s,'^[%l%s]+$') == 1
|
||||
end
|
||||
|
||||
--- does s only contain upper case characters?.
|
||||
-- @param s a string
|
||||
function stringx.isupper(s)
|
||||
assert_string(1,s)
|
||||
return find(s,'^[%u%s]+$') == 1
|
||||
end
|
||||
|
||||
--- concatenate the strings using this string as a delimiter.
|
||||
-- @param self the string
|
||||
-- @param seq a table of strings or numbers
|
||||
-- @usage (' '):join {1,2,3} == '1 2 3'
|
||||
function stringx.join (self,seq)
|
||||
assert_string(1,self)
|
||||
return concat(seq,self)
|
||||
end
|
||||
|
||||
--- does string start with the substring?.
|
||||
-- @param self the string
|
||||
-- @param s2 a string
|
||||
function stringx.startswith(self,s2)
|
||||
assert_string(1,self)
|
||||
assert_string(2,s2)
|
||||
return find(self,s2,1,true) == 1
|
||||
end
|
||||
|
||||
local function _find_all(s,sub,first,last)
|
||||
if sub == '' then return #s+1,#s end
|
||||
local i1,i2 = find(s,sub,first,true)
|
||||
local res
|
||||
local k = 0
|
||||
while i1 do
|
||||
res = i1
|
||||
k = k + 1
|
||||
i1,i2 = find(s,sub,i2+1,true)
|
||||
if last and i1 > last then break end
|
||||
end
|
||||
return res,k
|
||||
end
|
||||
|
||||
--- does string end with the given substring?.
|
||||
-- @param s a string
|
||||
-- @param send a substring or a table of suffixes
|
||||
function stringx.endswith(s,send)
|
||||
assert_string(1,s)
|
||||
if type(send) == 'string' then
|
||||
return #s >= #send and s:find(send, #s-#send+1, true) and true or false
|
||||
elseif type(send) == 'table' then
|
||||
local endswith = stringx.endswith
|
||||
for _,suffix in ipairs(send) do
|
||||
if endswith(s,suffix) then return true end
|
||||
end
|
||||
return false
|
||||
else
|
||||
error('argument #2: either a substring or a table of suffixes expected')
|
||||
end
|
||||
end
|
||||
|
||||
-- break string into a list of lines
|
||||
-- @param self the string
|
||||
-- @param keepends (currently not used)
|
||||
function stringx.splitlines (self,keepends)
|
||||
assert_string(1,self)
|
||||
local res = usplit(self,'[\r\n]')
|
||||
-- we are currently hacking around a problem with utils.split (see stringx.split)
|
||||
if #res == 0 then res = {''} end
|
||||
return setmetatable(res,list_MT)
|
||||
end
|
||||
|
||||
local function tab_expand (self,n)
|
||||
return (gsub(self,'([^\t]*)\t', function(s)
|
||||
return s..(' '):rep(n - #s % n)
|
||||
end))
|
||||
end
|
||||
|
||||
--- replace all tabs in s with n spaces. If not specified, n defaults to 8.
|
||||
-- with 0.9.5 this now correctly expands to the next tab stop (if you really
|
||||
-- want to just replace tabs, use :gsub('\t',' ') etc)
|
||||
-- @param self the string
|
||||
-- @param n number of spaces to expand each tab, (default 8)
|
||||
function stringx.expandtabs(self,n)
|
||||
assert_string(1,self)
|
||||
n = n or 8
|
||||
if not self:find '\n' then return tab_expand(self,n) end
|
||||
local res,i = {},1
|
||||
for line in stringx.lines(self) do
|
||||
res[i] = tab_expand(line,n)
|
||||
i = i + 1
|
||||
end
|
||||
return table.concat(res,'\n')
|
||||
end
|
||||
|
||||
--- find index of first instance of sub in s from the left.
|
||||
-- @param self the string
|
||||
-- @param sub substring
|
||||
-- @param i1 start index
|
||||
function stringx.lfind(self,sub,i1)
|
||||
assert_string(1,self)
|
||||
assert_string(2,sub)
|
||||
local idx = find(self,sub,i1,true)
|
||||
if idx then return idx else return nil end
|
||||
end
|
||||
|
||||
--- find index of first instance of sub in s from the right.
|
||||
-- @param self the string
|
||||
-- @param sub substring
|
||||
-- @param first first index
|
||||
-- @param last last index
|
||||
function stringx.rfind(self,sub,first,last)
|
||||
assert_string(1,self)
|
||||
assert_string(2,sub)
|
||||
local idx = _find_all(self,sub,first,last)
|
||||
if idx then return idx else return nil end
|
||||
end
|
||||
|
||||
--- replace up to n instances of old by new in the string s.
|
||||
-- if n is not present, replace all instances.
|
||||
-- @param s the string
|
||||
-- @param old the target substring
|
||||
-- @param new the substitution
|
||||
-- @param n optional maximum number of substitutions
|
||||
-- @return result string
|
||||
-- @return the number of substitutions
|
||||
function stringx.replace(s,old,new,n)
|
||||
assert_string(1,s)
|
||||
assert_string(1,old)
|
||||
return (gsub(s,escape(old),new:gsub('%%','%%%%'),n))
|
||||
end
|
||||
|
||||
--- split a string into a list of strings using a delimiter.
|
||||
-- @class function
|
||||
-- @name split
|
||||
-- @param self the string
|
||||
-- @param re a delimiter (defaults to whitespace)
|
||||
-- @param n maximum number of results
|
||||
-- @usage #(('one two'):split()) == 2
|
||||
-- @usage ('one,two,three'):split(',') == List{'one','two','three'}
|
||||
-- @usage ('one,two,three'):split(',',2) == List{'one','two,three'}
|
||||
function stringx.split(self,re,n)
|
||||
local s = self
|
||||
local plain = true
|
||||
if not re then -- default spaces
|
||||
s = lstrip(s)
|
||||
plain = false
|
||||
end
|
||||
local res = usplit(s,re,plain,n)
|
||||
if re and re ~= '' and find(s,re,-#re,true) then
|
||||
res[#res+1] = ""
|
||||
end
|
||||
return setmetatable(res,list_MT)
|
||||
end
|
||||
|
||||
--- split a string using a pattern. Note that at least one value will be returned!
|
||||
-- @param self the string
|
||||
-- @param re a Lua string pattern (defaults to whitespace)
|
||||
-- @return the parts of the string
|
||||
-- @usage a,b = line:splitv('=')
|
||||
function stringx.splitv (self,re)
|
||||
assert_string(1,self)
|
||||
return utils.splitv(self,re)
|
||||
end
|
||||
|
||||
local function copy(self)
|
||||
return self..''
|
||||
end
|
||||
|
||||
--- count all instances of substring in string.
|
||||
-- @param self the string
|
||||
-- @param sub substring
|
||||
function stringx.count(self,sub)
|
||||
assert_string(1,self)
|
||||
local i,k = _find_all(self,sub,1)
|
||||
return k
|
||||
end
|
||||
|
||||
local function _just(s,w,ch,left,right)
|
||||
local n = #s
|
||||
if w > n then
|
||||
if not ch then ch = ' ' end
|
||||
local f1,f2
|
||||
if left and right then
|
||||
local ln = ceil((w-n)/2)
|
||||
local rn = w - n - ln
|
||||
f1 = rep(ch,ln)
|
||||
f2 = rep(ch,rn)
|
||||
elseif right then
|
||||
f1 = rep(ch,w-n)
|
||||
f2 = ''
|
||||
else
|
||||
f2 = rep(ch,w-n)
|
||||
f1 = ''
|
||||
end
|
||||
return f1..s..f2
|
||||
else
|
||||
return copy(s)
|
||||
end
|
||||
end
|
||||
|
||||
--- left-justify s with width w.
|
||||
-- @param self the string
|
||||
-- @param w width of justification
|
||||
-- @param ch padding character, default ' '
|
||||
function stringx.ljust(self,w,ch)
|
||||
assert_string(1,self)
|
||||
assert_arg(2,w,'number')
|
||||
return _just(self,w,ch,true,false)
|
||||
end
|
||||
|
||||
--- right-justify s with width w.
|
||||
-- @param s the string
|
||||
-- @param w width of justification
|
||||
-- @param ch padding character, default ' '
|
||||
function stringx.rjust(s,w,ch)
|
||||
assert_string(1,s)
|
||||
assert_arg(2,w,'number')
|
||||
return _just(s,w,ch,false,true)
|
||||
end
|
||||
|
||||
--- center-justify s with width w.
|
||||
-- @param s the string
|
||||
-- @param w width of justification
|
||||
-- @param ch padding character, default ' '
|
||||
function stringx.center(s,w,ch)
|
||||
assert_string(1,s)
|
||||
assert_arg(2,w,'number')
|
||||
return _just(s,w,ch,true,true)
|
||||
end
|
||||
|
||||
local function _strip(s,left,right,chrs)
|
||||
if not chrs then
|
||||
chrs = '%s'
|
||||
else
|
||||
chrs = '['..escape(chrs)..']'
|
||||
end
|
||||
if left then
|
||||
local i1,i2 = find(s,'^'..chrs..'*')
|
||||
if i2 >= i1 then
|
||||
s = sub(s,i2+1)
|
||||
end
|
||||
end
|
||||
if right then
|
||||
local i1,i2 = find(s,chrs..'*$')
|
||||
if i2 >= i1 then
|
||||
s = sub(s,1,i1-1)
|
||||
end
|
||||
end
|
||||
return s
|
||||
end
|
||||
|
||||
--- trim any whitespace on the left of s.
|
||||
-- @param self the string
|
||||
-- @param chrs default space, can be a string of characters to be trimmed
|
||||
function stringx.lstrip(self,chrs)
|
||||
assert_string(1,self)
|
||||
return _strip(self,true,false,chrs)
|
||||
end
|
||||
lstrip = stringx.lstrip
|
||||
|
||||
--- trim any whitespace on the right of s.
|
||||
-- @param s the string
|
||||
-- @param chrs default space, can be a string of characters to be trimmed
|
||||
function stringx.rstrip(s,chrs)
|
||||
assert_string(1,s)
|
||||
return _strip(s,false,true,chrs)
|
||||
end
|
||||
|
||||
--- trim any whitespace on both left and right of s.
|
||||
-- @param self the string
|
||||
-- @param chrs default space, can be a string of characters to be trimmed
|
||||
function stringx.strip(self,chrs)
|
||||
assert_string(1,self)
|
||||
return _strip(self,true,true,chrs)
|
||||
end
|
||||
|
||||
-- The partition functions split a string using a delimiter into three parts:
|
||||
-- the part before, the delimiter itself, and the part afterwards
|
||||
local function _partition(p,delim,fn)
|
||||
local i1,i2 = fn(p,delim)
|
||||
if not i1 or i1 == -1 then
|
||||
return p,'',''
|
||||
else
|
||||
if not i2 then i2 = i1 end
|
||||
return sub(p,1,i1-1),sub(p,i1,i2),sub(p,i2+1)
|
||||
end
|
||||
end
|
||||
|
||||
--- partition the string using first occurance of a delimiter
|
||||
-- @param self the string
|
||||
-- @param ch delimiter
|
||||
-- @return part before ch
|
||||
-- @return ch
|
||||
-- @return part after ch
|
||||
function stringx.partition(self,ch)
|
||||
assert_string(1,self)
|
||||
assert_nonempty_string(2,ch)
|
||||
return _partition(self,ch,stringx.lfind)
|
||||
end
|
||||
|
||||
--- partition the string p using last occurance of a delimiter
|
||||
-- @param self the string
|
||||
-- @param ch delimiter
|
||||
-- @return part before ch
|
||||
-- @return ch
|
||||
-- @return part after ch
|
||||
function stringx.rpartition(self,ch)
|
||||
assert_string(1,self)
|
||||
assert_nonempty_string(2,ch)
|
||||
return _partition(self,ch,stringx.rfind)
|
||||
end
|
||||
|
||||
--- return the 'character' at the index.
|
||||
-- @param self the string
|
||||
-- @param idx an index (can be negative)
|
||||
-- @return a substring of length 1 if successful, empty string otherwise.
|
||||
function stringx.at(self,idx)
|
||||
assert_string(1,self)
|
||||
assert_arg(2,idx,'number')
|
||||
return sub(self,idx,idx)
|
||||
end
|
||||
|
||||
--- return an interator over all lines in a string
|
||||
-- @param self the string
|
||||
-- @return an iterator
|
||||
function stringx.lines (self)
|
||||
assert_string(1,self)
|
||||
local s = self
|
||||
if not s:find '\n$' then s = s..'\n' end
|
||||
return s:gmatch('([^\n]*)\n')
|
||||
end
|
||||
|
||||
--- iniital word letters uppercase ('title case').
|
||||
-- Here 'words' mean chunks of non-space characters.
|
||||
-- @param self the string
|
||||
-- @return a string with each word's first letter uppercase
|
||||
function stringx.title(self)
|
||||
return (self:gsub('(%S)(%S*)',function(f,r)
|
||||
return f:upper()..r:lower()
|
||||
end))
|
||||
end
|
||||
|
||||
stringx.capitalize = stringx.title
|
||||
|
||||
local elipsis = '...'
|
||||
local n_elipsis = #elipsis
|
||||
|
||||
--- return a shorted version of a string.
|
||||
-- @param self the string
|
||||
-- @param sz the maxinum size allowed
|
||||
-- @param tail true if we want to show the end of the string (head otherwise)
|
||||
function stringx.shorten(self,sz,tail)
|
||||
if #self > sz then
|
||||
if sz < n_elipsis then return elipsis:sub(1,sz) end
|
||||
if tail then
|
||||
local i = #self - sz + 1 + n_elipsis
|
||||
return elipsis .. self:sub(i)
|
||||
else
|
||||
return self:sub(1,sz-n_elipsis) .. elipsis
|
||||
end
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
function stringx.import(dont_overload)
|
||||
utils.import(stringx,string)
|
||||
end
|
||||
|
||||
return stringx
|
||||
@@ -0,0 +1,766 @@
|
||||
--- Extended operations on Lua tables.
|
||||
-- @class module
|
||||
-- @name pl.tablex
|
||||
local getmetatable,setmetatable,require = getmetatable,setmetatable,require
|
||||
local append,remove = table.insert,table.remove
|
||||
local min,max = math.min,math.max
|
||||
local pairs,type,unpack,next,ipairs,select,tostring = pairs,type,unpack,next,ipairs,select,tostring
|
||||
local utils = require ('pl.utils')
|
||||
local function_arg = utils.function_arg
|
||||
local Set = utils.stdmt.Set
|
||||
local List = utils.stdmt.List
|
||||
local Map = utils.stdmt.Map
|
||||
local assert_arg = utils.assert_arg
|
||||
|
||||
--[[
|
||||
module ('pl.tablex',utils._module)
|
||||
]]
|
||||
|
||||
local tablex = {}
|
||||
|
||||
-- generally, functions that make copies of tables try to preserve the metatable.
|
||||
-- However, when the source has no obvious type, then we attach appropriate metatables
|
||||
-- like List, Map, etc to the result.
|
||||
local function setmeta (res,tbl,def)
|
||||
return setmetatable(res,getmetatable(tbl) or def)
|
||||
end
|
||||
|
||||
local function makelist (res)
|
||||
return setmetatable(res,List)
|
||||
end
|
||||
|
||||
--- copy a table into another, in-place.
|
||||
-- @param t1 destination table
|
||||
-- @param t2 source table
|
||||
-- @return first table
|
||||
function tablex.update (t1,t2)
|
||||
assert_arg(1,t1,'table')
|
||||
assert_arg(2,t2,'table')
|
||||
for k,v in pairs(t2) do
|
||||
t1[k] = v
|
||||
end
|
||||
return t1
|
||||
end
|
||||
|
||||
--- total number of elements in this table. <br>
|
||||
-- Note that this is distinct from #t, which is the number
|
||||
-- of values in the array part; this value will always
|
||||
-- be greater or equal. The difference gives the size of
|
||||
-- the hash part, for practical purposes.
|
||||
-- @param t a table
|
||||
-- @return the size
|
||||
function tablex.size (t)
|
||||
assert_arg(1,t,'table')
|
||||
local i = 0
|
||||
for k in pairs(t) do i = i + 1 end
|
||||
return i
|
||||
end
|
||||
|
||||
--- make a shallow copy of a table
|
||||
-- @param t source table
|
||||
-- @return new table
|
||||
function tablex.copy (t)
|
||||
assert_arg(1,t,'table')
|
||||
local res = {}
|
||||
for k,v in pairs(t) do
|
||||
res[k] = v
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
--- make a deep copy of a table, recursively copying all the keys and fields.
|
||||
-- This will also set the copied table's metatable to that of the original.
|
||||
-- @param t A table
|
||||
-- @return new table
|
||||
function tablex.deepcopy(t)
|
||||
assert_arg(1,t,'table')
|
||||
if type(t) ~= 'table' then return t end
|
||||
local mt = getmetatable(t)
|
||||
local res = {}
|
||||
for k,v in pairs(t) do
|
||||
if type(v) == 'table' then
|
||||
v = tablex.deepcopy(v)
|
||||
end
|
||||
res[k] = v
|
||||
end
|
||||
setmetatable(res,mt)
|
||||
return res
|
||||
end
|
||||
|
||||
local abs = math.abs
|
||||
|
||||
--- compare two values.
|
||||
-- if they are tables, then compare their keys and fields recursively.
|
||||
-- @param t1 A value
|
||||
-- @param t2 A value
|
||||
-- @param ignore_mt if true, ignore __eq metamethod (default false)
|
||||
-- @param eps if defined, then used for any number comparisons
|
||||
-- @return true or false
|
||||
function tablex.deepcompare(t1,t2,ignore_mt,eps)
|
||||
local ty1 = type(t1)
|
||||
local ty2 = type(t2)
|
||||
if ty1 ~= ty2 then return false end
|
||||
-- non-table types can be directly compared
|
||||
if ty1 ~= 'table' then
|
||||
if ty1 == 'number' and eps then return abs(t1-t2) < eps end
|
||||
return t1 == t2
|
||||
end
|
||||
-- as well as tables which have the metamethod __eq
|
||||
local mt = getmetatable(t1)
|
||||
if not ignore_mt and mt and mt.__eq then return t1 == t2 end
|
||||
for k1,v1 in pairs(t1) do
|
||||
local v2 = t2[k1]
|
||||
if v2 == nil or not tablex.deepcompare(v1,v2,ignore_mt,eps) then return false end
|
||||
end
|
||||
for k2,v2 in pairs(t2) do
|
||||
local v1 = t1[k2]
|
||||
if v1 == nil or not tablex.deepcompare(v1,v2,ignore_mt,eps) then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- compare two list-like tables using a predicate.
|
||||
-- @param t1 a table
|
||||
-- @param t2 a table
|
||||
-- @param cmp A comparison function
|
||||
function tablex.compare (t1,t2,cmp)
|
||||
assert_arg(1,t1,'table')
|
||||
assert_arg(2,t2,'table')
|
||||
if #t1 ~= #t2 then return false end
|
||||
cmp = function_arg(3,cmp)
|
||||
for k in ipairs(t1) do
|
||||
if not cmp(t1[k],t2[k]) then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- compare two list-like tables using an optional predicate, without regard for element order.
|
||||
-- @param t1 a list-like table
|
||||
-- @param t2 a list-like table
|
||||
-- @param cmp A comparison function (may be nil)
|
||||
function tablex.compare_no_order (t1,t2,cmp)
|
||||
assert_arg(1,t1,'table')
|
||||
assert_arg(2,t2,'table')
|
||||
if cmp then cmp = function_arg(3,cmp) end
|
||||
if #t1 ~= #t2 then return false end
|
||||
local visited = {}
|
||||
for i = 1,#t1 do
|
||||
local val = t1[i]
|
||||
local gotcha
|
||||
for j = 1,#t2 do if not visited[j] then
|
||||
local match
|
||||
if cmp then match = cmp(val,t2[j]) else match = val == t2[j] end
|
||||
if match then
|
||||
gotcha = j
|
||||
break
|
||||
end
|
||||
end end
|
||||
if not gotcha then return false end
|
||||
visited[gotcha] = true
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
--- return the index of a value in a list.
|
||||
-- Like string.find, there is an optional index to start searching,
|
||||
-- which can be negative.
|
||||
-- @param t A list-like table (i.e. with numerical indices)
|
||||
-- @param val A value
|
||||
-- @param idx index to start; -1 means last element,etc (default 1)
|
||||
-- @return index of value or nil if not found
|
||||
-- @usage find({10,20,30},20) == 2
|
||||
-- @usage find({'a','b','a','c'},'a',2) == 3
|
||||
|
||||
function tablex.find(t,val,idx)
|
||||
assert_arg(1,t,'table')
|
||||
idx = idx or 1
|
||||
if idx < 0 then idx = #t + idx + 1 end
|
||||
for i = idx,#t do
|
||||
if t[i] == val then return i end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
--- return the index of a value in a list, searching from the end.
|
||||
-- Like string.find, there is an optional index to start searching,
|
||||
-- which can be negative.
|
||||
-- @param t A list-like table (i.e. with numerical indices)
|
||||
-- @param val A value
|
||||
-- @param idx index to start; -1 means last element,etc (default 1)
|
||||
-- @return index of value or nil if not found
|
||||
-- @usage rfind({10,10,10},10) == 3
|
||||
function tablex.rfind(t,val,idx)
|
||||
assert_arg(1,t,'table')
|
||||
idx = idx or #t
|
||||
if idx < 0 then idx = #t + idx + 1 end
|
||||
for i = idx,1,-1 do
|
||||
if t[i] == val then return i end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
--- return the index (or key) of a value in a table using a comparison function.
|
||||
-- @param t A table
|
||||
-- @param cmp A comparison function
|
||||
-- @param arg an optional second argument to the function
|
||||
-- @return index of value, or nil if not found
|
||||
-- @return value returned by comparison function
|
||||
function tablex.find_if(t,cmp,arg)
|
||||
assert_arg(1,t,'table')
|
||||
cmp = function_arg(2,cmp)
|
||||
for k,v in pairs(t) do
|
||||
local c = cmp(v,arg)
|
||||
if c then return k,c end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
--- return a list of all values in a table indexed by another list.
|
||||
-- @param tbl a table
|
||||
-- @param idx an index table (a list of keys)
|
||||
-- @return a list-like table
|
||||
-- @usage index_by({10,20,30,40},{2,4}) == {20,40}
|
||||
-- @usage index_by({one=1,two=2,three=3},{'one','three'}) == {1,3}
|
||||
function tablex.index_by(tbl,idx)
|
||||
assert_arg(1,tbl,'table')
|
||||
assert_arg(2,idx,'table')
|
||||
local res = {}
|
||||
for _,i in ipairs(idx) do
|
||||
append(res,tbl[i])
|
||||
end
|
||||
return setmeta(res,tbl,List)
|
||||
end
|
||||
|
||||
--- apply a function to all values of a table.
|
||||
-- This returns a table of the results.
|
||||
-- Any extra arguments are passed to the function.
|
||||
-- @param fun A function that takes at least one argument
|
||||
-- @param t A table
|
||||
-- @param ... optional arguments
|
||||
-- @usage map(function(v) return v*v end, {10,20,30,fred=2}) is {100,400,900,fred=4}
|
||||
function tablex.map(fun,t,...)
|
||||
assert_arg(1,t,'table')
|
||||
fun = function_arg(1,fun)
|
||||
local res = {}
|
||||
for k,v in pairs(t) do
|
||||
res[k] = fun(v,...)
|
||||
end
|
||||
return setmeta(res,t)
|
||||
end
|
||||
|
||||
--- apply a function to all values of a list.
|
||||
-- This returns a table of the results.
|
||||
-- Any extra arguments are passed to the function.
|
||||
-- @param fun A function that takes at least one argument
|
||||
-- @param t a table (applies to array part)
|
||||
-- @param ... optional arguments
|
||||
-- @return a list-like table
|
||||
-- @usage imap(function(v) return v*v end, {10,20,30,fred=2}) is {100,400,900}
|
||||
function tablex.imap(fun,t,...)
|
||||
assert_arg(1,t,'table')
|
||||
fun = function_arg(1,fun)
|
||||
local res = {}
|
||||
for i = 1,#t do
|
||||
res[i] = fun(t[i],...) or false
|
||||
end
|
||||
return setmeta(res,t,List)
|
||||
end
|
||||
|
||||
--- apply a named method to values from a table.
|
||||
-- @param name the method name
|
||||
-- @param t a list-like table
|
||||
-- @param ... any extra arguments to the method
|
||||
function tablex.map_named_method (name,t,...)
|
||||
assert_arg(1,name,'string')
|
||||
assert_arg(2,t,'table')
|
||||
local res = {}
|
||||
for i = 1,#t do
|
||||
local val = t[i]
|
||||
local fun = val[name]
|
||||
res[i] = fun(val,...)
|
||||
end
|
||||
return setmeta(res,t,List)
|
||||
end
|
||||
|
||||
|
||||
--- apply a function to all values of a table, in-place.
|
||||
-- Any extra arguments are passed to the function.
|
||||
-- @param fun A function that takes at least one argument
|
||||
-- @param t a table
|
||||
-- @param ... extra arguments
|
||||
function tablex.transform (fun,t,...)
|
||||
assert_arg(1,t,'table')
|
||||
fun = function_arg(1,fun)
|
||||
for k,v in pairs(t) do
|
||||
t[v] = fun(v,...)
|
||||
end
|
||||
end
|
||||
|
||||
--- generate a table of all numbers in a range
|
||||
-- @param start number
|
||||
-- @param finish number
|
||||
-- @param step optional increment (default 1 for increasing, -1 for decreasing)
|
||||
function tablex.range (start,finish,step)
|
||||
local res = {}
|
||||
local k = 1
|
||||
if not step then
|
||||
if finish > start then step = finish > start and 1 or -1 end
|
||||
end
|
||||
for i=start,finish,step do res[k]=i; k=k+1 end
|
||||
return res
|
||||
end
|
||||
|
||||
--- apply a function to values from two tables.
|
||||
-- @param fun a function of at least two arguments
|
||||
-- @param t1 a table
|
||||
-- @param t2 a table
|
||||
-- @param ... extra arguments
|
||||
-- @return a table
|
||||
-- @usage map2('+',{1,2,3,m=4},{10,20,30,m=40}) is {11,22,23,m=44}
|
||||
function tablex.map2 (fun,t1,t2,...)
|
||||
assert_arg(1,t1,'table')
|
||||
assert_arg(2,t2,'table')
|
||||
fun = function_arg(1,fun)
|
||||
local res = {}
|
||||
for k,v in pairs(t1) do
|
||||
res[k] = fun(v,t2[k],...)
|
||||
end
|
||||
return setmeta(res,t1,List)
|
||||
end
|
||||
|
||||
--- apply a function to values from two arrays.
|
||||
-- @param fun a function of at least two arguments
|
||||
-- @param t1 a list-like table
|
||||
-- @param t2 a list-like table
|
||||
-- @param ... extra arguments
|
||||
-- @usage imap2('+',{1,2,3,m=4},{10,20,30,m=40}) is {11,22,23}
|
||||
function tablex.imap2 (fun,t1,t2,...)
|
||||
assert_arg(2,t1,'table')
|
||||
assert_arg(3,t2,'table')
|
||||
fun = function_arg(1,fun)
|
||||
local res = {}
|
||||
for i = 1,#t1 do
|
||||
res[i] = fun(t1[i],t2[i],...)
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
--- 'reduce' a list using a binary function.
|
||||
-- @param fun a function of two arguments
|
||||
-- @param t a list-like table
|
||||
-- @return the result of the function
|
||||
-- @usage reduce('+',{1,2,3,4}) == 10
|
||||
function tablex.reduce (fun,t)
|
||||
assert_arg(2,t,'table')
|
||||
fun = function_arg(1,fun)
|
||||
local n = #t
|
||||
local res = t[1]
|
||||
for i = 2,n do
|
||||
res = fun(res,t[i])
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
--- apply a function to all elements of a table.
|
||||
-- The arguments to the function will be the value,
|
||||
-- the key and <i>finally</i> any extra arguments passed to this function.
|
||||
-- Note that the Lua 5.0 function table.foreach passed the <i>key</i> first.
|
||||
-- @param t a table
|
||||
-- @param fun a function with at least one argument
|
||||
-- @param ... extra arguments
|
||||
function tablex.foreach(t,fun,...)
|
||||
assert_arg(1,t,'table')
|
||||
fun = function_arg(2,fun)
|
||||
for k,v in pairs(t) do
|
||||
fun(v,k,...)
|
||||
end
|
||||
end
|
||||
|
||||
--- apply a function to all elements of a list-like table in order.
|
||||
-- The arguments to the function will be the value,
|
||||
-- the index and <i>finally</i> any extra arguments passed to this function
|
||||
-- @param t a table
|
||||
-- @param fun a function with at least one argument
|
||||
-- @param ... optional arguments
|
||||
function tablex.foreachi(t,fun,...)
|
||||
assert_arg(1,t,'table')
|
||||
fun = function_arg(2,fun)
|
||||
for k,v in ipairs(t) do
|
||||
fun(v,k,...)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--- Apply a function to a number of tables.
|
||||
-- A more general version of map
|
||||
-- The result is a table containing the result of applying that function to the
|
||||
-- ith value of each table. Length of output list is the minimum length of all the lists
|
||||
-- @param fun a function of n arguments
|
||||
-- @param ... n tables
|
||||
-- @usage mapn(function(x,y,z) return x+y+z end, {1,2,3},{10,20,30},{100,200,300}) is {111,222,333}
|
||||
-- @usage mapn(math.max, {1,20,300},{10,2,3},{100,200,100}) is {100,200,300}
|
||||
-- @param fun A function that takes as many arguments as there are tables
|
||||
function tablex.mapn(fun,...)
|
||||
fun = function_arg(1,fun)
|
||||
local res = {}
|
||||
local lists = {...}
|
||||
local minn = 1e40
|
||||
for i = 1,#lists do
|
||||
minn = min(minn,#(lists[i]))
|
||||
end
|
||||
for i = 1,minn do
|
||||
local args = {}
|
||||
for j = 1,#lists do
|
||||
args[#args+1] = lists[j][i]
|
||||
end
|
||||
res[#res+1] = fun(unpack(args))
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
--- call the function with the key and value pairs from a table.
|
||||
-- The function can return a value and a key (note the order!). If both
|
||||
-- are not nil, then this pair is inserted into the result. If only value is not nil, then
|
||||
-- it is appended to the result.
|
||||
-- @param fun A function which will be passed each key and value as arguments, plus any extra arguments to pairmap.
|
||||
-- @param t A table
|
||||
-- @param ... optional arguments
|
||||
-- @usage pairmap({fred=10,bonzo=20},function(k,v) return v end) is {10,20}
|
||||
-- @usage pairmap({one=1,two=2},function(k,v) return {k,v},k end) is {one={'one',1},two={'two',2}}
|
||||
function tablex.pairmap(fun,t,...)
|
||||
assert_arg(1,t,'table')
|
||||
fun = function_arg(1,fun)
|
||||
local res = {}
|
||||
for k,v in pairs(t) do
|
||||
local rv,rk = fun(k,v,...)
|
||||
if rk then
|
||||
res[rk] = rv
|
||||
else
|
||||
res[#res+1] = rv
|
||||
end
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
local function keys_op(i,v) return i end
|
||||
|
||||
--- return all the keys of a table in arbitrary order.
|
||||
-- @param t A table
|
||||
function tablex.keys(t)
|
||||
assert_arg(1,t,'table')
|
||||
return makelist(tablex.pairmap(keys_op,t))
|
||||
end
|
||||
|
||||
local function values_op(i,v) return v end
|
||||
|
||||
--- return all the values of the table in arbitrary order
|
||||
-- @param t A table
|
||||
function tablex.values(t)
|
||||
assert_arg(1,t,'table')
|
||||
return makelist(tablex.pairmap(values_op,t))
|
||||
end
|
||||
|
||||
local function index_map_op (i,v) return i,v end
|
||||
|
||||
--- create an index map from a list-like table. The original values become keys,
|
||||
-- and the associated values are the indices into the original list.
|
||||
-- @param t a list-like table
|
||||
-- @return a map-like table
|
||||
function tablex.index_map (t)
|
||||
assert_arg(1,t,'table')
|
||||
return setmetatable(tablex.pairmap(index_map_op,t),Map)
|
||||
end
|
||||
|
||||
local function set_op(i,v) return true,v end
|
||||
|
||||
--- create a set from a list-like table. A set is a table where the original values
|
||||
-- become keys, and the associated values are all true.
|
||||
-- @param t a list-like table
|
||||
-- @return a set (a map-like table)
|
||||
function tablex.makeset (t)
|
||||
assert_arg(1,t,'table')
|
||||
return setmetatable(tablex.pairmap(set_op,t),Set)
|
||||
end
|
||||
|
||||
|
||||
--- combine two tables, either as union or intersection. Corresponds to
|
||||
-- set operations for sets () but more general. Not particularly
|
||||
-- useful for list-like tables.
|
||||
-- @param t1 a table
|
||||
-- @param t2 a table
|
||||
-- @param dup true for a union, false for an intersection.
|
||||
-- @usage merge({alice=23,fred=34},{bob=25,fred=34}) is {fred=34}
|
||||
-- @usage merge({alice=23,fred=34},{bob=25,fred=34},true) is {bob=25,fred=34,alice=23}
|
||||
-- @see tablex.index_map
|
||||
function tablex.merge (t1,t2,dup)
|
||||
assert_arg(1,t1,'table')
|
||||
assert_arg(2,t2,'table')
|
||||
local res = {}
|
||||
for k,v in pairs(t1) do
|
||||
if dup or t2[k] then res[k] = v end
|
||||
end
|
||||
for k,v in pairs(t2) do
|
||||
if dup or t1[k] then res[k] = v end
|
||||
end
|
||||
return setmeta(res,t1,Map)
|
||||
end
|
||||
|
||||
--- a new table which is the difference of two tables.
|
||||
-- With sets (where the values are all true) this is set difference and
|
||||
-- symmetric difference depending on the third parameter.
|
||||
-- @param s1 a map-like table or set
|
||||
-- @param s2 a map-like table or set
|
||||
-- @param symm symmetric difference (default false)
|
||||
-- @return a map-like table or set
|
||||
function tablex.difference (s1,s2,symm)
|
||||
assert_arg(1,s1,'table')
|
||||
assert_arg(2,s2,'table')
|
||||
local res = {}
|
||||
for k,v in pairs(s1) do
|
||||
if not s2[k] then res[k] = v end
|
||||
end
|
||||
if symm then
|
||||
for k,v in pairs(s2) do
|
||||
if not s1[k] then res[k] = v end
|
||||
end
|
||||
end
|
||||
return setmeta(res,s1,Map)
|
||||
end
|
||||
|
||||
--- A table where the key/values are the values and value counts of the table.
|
||||
-- @param t a list-like table
|
||||
-- @param cmp a function that defines equality (otherwise uses ==)
|
||||
-- @return a map-like table
|
||||
-- @see seq.count_map
|
||||
function tablex.count_map (t,cmp)
|
||||
assert_arg(1,t,'table')
|
||||
local res,mask = {},{}
|
||||
cmp = function_arg(2,cmp)
|
||||
local n = #t
|
||||
for i,v in ipairs(t) do
|
||||
if not mask[v] then
|
||||
mask[v] = true
|
||||
-- check this value against all other values
|
||||
res[v] = 1 -- there's at least one instance
|
||||
for j = i+1,n do
|
||||
local w = t[j]
|
||||
if cmp and cmp(v,w) or v == w then
|
||||
res[v] = res[v] + 1
|
||||
mask[w] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return setmetatable(res,Map)
|
||||
end
|
||||
|
||||
--- filter a table's values using a predicate function
|
||||
-- @param t a list-like table
|
||||
-- @param pred a boolean function
|
||||
-- @param arg optional argument to be passed as second argument of the predicate
|
||||
function tablex.filter (t,pred,arg)
|
||||
assert_arg(1,t,'table')
|
||||
pred = function_arg(2,pred)
|
||||
local res = {}
|
||||
for k,v in ipairs(t) do
|
||||
if pred(v,arg) then append(res,v) end
|
||||
end
|
||||
return setmeta(res,t,List)
|
||||
end
|
||||
|
||||
--- return a table where each element is a table of the ith values of an arbitrary
|
||||
-- number of tables. It is equivalent to a matrix transpose.
|
||||
-- @usage zip({10,20,30},{100,200,300}) is {{10,100},{20,200},{30,300}}
|
||||
function tablex.zip(...)
|
||||
return tablex.mapn(function(...) return {...} end,...)
|
||||
end
|
||||
|
||||
local _copy
|
||||
function _copy (dest,src,idest,isrc,nsrc,clean_tail)
|
||||
idest = idest or 1
|
||||
isrc = isrc or 1
|
||||
local iend
|
||||
if not nsrc then
|
||||
nsrc = #src
|
||||
iend = #src
|
||||
else
|
||||
iend = isrc + min(nsrc-1,#src-isrc)
|
||||
end
|
||||
if dest == src then -- special case
|
||||
if idest > isrc and iend >= idest then -- overlapping ranges
|
||||
src = tablex.sub(src,isrc,nsrc)
|
||||
isrc = 1; iend = #src
|
||||
end
|
||||
end
|
||||
for i = isrc,iend do
|
||||
dest[idest] = src[i]
|
||||
idest = idest + 1
|
||||
end
|
||||
if clean_tail then
|
||||
tablex.clear(dest,idest)
|
||||
end
|
||||
return dest
|
||||
end
|
||||
|
||||
--- copy an array into another one, resizing the destination if necessary. <br>
|
||||
-- @param dest a list-like table
|
||||
-- @param src a list-like table
|
||||
-- @param idest where to start copying values from source (default 1)
|
||||
-- @param isrc where to start copying values into destination (default 1)
|
||||
-- @param nsrc number of elements to copy from source (default source size)
|
||||
function tablex.icopy (dest,src,idest,isrc,nsrc)
|
||||
assert_arg(1,dest,'table')
|
||||
assert_arg(2,src,'table')
|
||||
return _copy(dest,src,idest,isrc,nsrc,true)
|
||||
end
|
||||
|
||||
--- copy an array into another one. <br>
|
||||
-- @param dest a list-like table
|
||||
-- @param src a list-like table
|
||||
-- @param idest where to start copying values from source (default 1)
|
||||
-- @param isrc where to start copying values into destination (default 1)
|
||||
-- @param nsrc number of elements to copy from source (default source size)
|
||||
function tablex.move (dest,src,idest,isrc,nsrc)
|
||||
assert_arg(1,dest,'table')
|
||||
assert_arg(2,src,'table')
|
||||
return _copy(dest,src,idest,isrc,nsrc,false)
|
||||
end
|
||||
|
||||
function tablex._normalize_slice(self,first,last)
|
||||
local sz = #self
|
||||
if not first then first=1 end
|
||||
if first<0 then first=sz+first+1 end
|
||||
-- make the range _inclusive_!
|
||||
if not last then last=sz end
|
||||
if last < 0 then last=sz+1+last end
|
||||
return first,last
|
||||
end
|
||||
|
||||
--- Extract a range from a table, like 'string.sub'.
|
||||
-- If first or last are negative then they are relative to the end of the list
|
||||
-- eg. sub(t,-2) gives last 2 entries in a list, and
|
||||
-- sub(t,-4,-2) gives from -4th to -2nd
|
||||
-- @param t a list-like table
|
||||
-- @param first An index
|
||||
-- @param last An index
|
||||
-- @return a new List
|
||||
function tablex.sub(t,first,last)
|
||||
assert_arg(1,t,'table')
|
||||
first,last = tablex._normalize_slice(t,first,last)
|
||||
local res={}
|
||||
for i=first,last do append(res,t[i]) end
|
||||
return setmeta(res,t,List)
|
||||
end
|
||||
|
||||
--- set an array range to a value. If it's a function we use the result
|
||||
-- of applying it to the indices.
|
||||
-- @param t a list-like table
|
||||
-- @param val a value
|
||||
-- @param i1 start range (default 1)
|
||||
-- @param i2 end range (default table size)
|
||||
function tablex.set (t,val,i1,i2)
|
||||
i1,i2 = i1 or 1,i2 or #t
|
||||
if utils.is_callable(val) then
|
||||
for i = i1,i2 do
|
||||
t[i] = val(i)
|
||||
end
|
||||
else
|
||||
for i = i1,i2 do
|
||||
t[i] = val
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- create a new array of specified size with initial value.
|
||||
-- @param n size
|
||||
-- @param val initial value (can be nil, but don't expect # to work!)
|
||||
-- @return the table
|
||||
function tablex.new (n,val)
|
||||
local res = {}
|
||||
tablex.set(res,val,1,n)
|
||||
return res
|
||||
end
|
||||
|
||||
--- clear out the contents of a table.
|
||||
-- @param t a table
|
||||
-- @param istart optional start position
|
||||
function tablex.clear(t,istart)
|
||||
istart = istart or 1
|
||||
for i = istart,#t do remove(t) end
|
||||
end
|
||||
|
||||
--- insert values into a table. <br>
|
||||
-- insertvalues(t, [pos,] values) <br>
|
||||
-- similar to table.insert but inserts values from given table "values",
|
||||
-- not the object itself, into table "t" at position "pos".
|
||||
function tablex.insertvalues(t, ...)
|
||||
local pos, values
|
||||
if select('#', ...) == 1 then
|
||||
pos,values = #t+1, ...
|
||||
else
|
||||
pos,values = ...
|
||||
end
|
||||
if #values > 0 then
|
||||
for i=#t,pos,-1 do
|
||||
t[i+#values] = t[i]
|
||||
end
|
||||
local offset = 1 - pos
|
||||
for i=pos,pos+#values-1 do
|
||||
t[i] = values[i + offset]
|
||||
end
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
--- remove a range of values from a table.
|
||||
-- @param t a list-like table
|
||||
-- @param i1 start index
|
||||
-- @param i2 end index
|
||||
-- @return the table
|
||||
function tablex.removevalues (t,i1,i2)
|
||||
i1,i2 = tablex._normalize_slice(t,i1,i2)
|
||||
for i = i1,i2 do
|
||||
remove(t,i1)
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
local _find
|
||||
_find = function (t,value,tables)
|
||||
for k,v in pairs(t) do
|
||||
if v == value then return k end
|
||||
end
|
||||
for k,v in pairs(t) do
|
||||
if not tables[v] and type(v) == 'table' then
|
||||
tables[v] = true
|
||||
local res = _find(v,value,tables)
|
||||
if res then
|
||||
res = tostring(res)
|
||||
if type(k) ~= 'string' then
|
||||
return '['..k..']'..res
|
||||
else
|
||||
return k..'.'..res
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- find a value in a table by recursive search.
|
||||
-- @param t the table
|
||||
-- @param value the value
|
||||
-- @param exclude any tables to avoid searching
|
||||
-- @usage search(_G,math.sin,{package.path}) == 'math.sin'
|
||||
-- @return a fieldspec, e.g. 'a.b' or 'math.sin'
|
||||
function tablex.search (t,value,exclude)
|
||||
assert_arg(1,t,'table')
|
||||
local tables = {[t]=true}
|
||||
if exclude then
|
||||
for _,v in pairs(exclude) do tables[v] = true end
|
||||
end
|
||||
return _find(t,value,tables)
|
||||
end
|
||||
|
||||
return tablex
|
||||
@@ -0,0 +1,99 @@
|
||||
--- A template preprocessor.
|
||||
-- Originally by <a href="http://lua-users.org/wiki/SlightlyLessSimpleLuaPreprocessor">Ricki Lake</a>
|
||||
-- <p>There are two rules: <ul>
|
||||
-- <li>lines starting with # are Lua</li>
|
||||
-- <li> otherwise, `$(expr)` is the result of evaluating `expr`</li>
|
||||
-- </ul>
|
||||
-- <pre class=example>
|
||||
-- # for i = 1,3 do
|
||||
-- $(i) Hello, Word!
|
||||
-- # end
|
||||
-- </pre>
|
||||
-- Other escape characters can be used, when the defaults conflict
|
||||
-- with the output language.
|
||||
-- <pre class=example>
|
||||
-- > for _,n in pairs{'one','two','three'} do
|
||||
-- static int l_${n} (luaState *state);
|
||||
-- > end
|
||||
-- </pre>
|
||||
-- See <a href="../../index.html#rici_templates">the Guide</a>.
|
||||
-- @class module
|
||||
-- @name pl.template
|
||||
|
||||
--[[
|
||||
module('pl.template')
|
||||
]]
|
||||
|
||||
local utils = require 'pl.utils'
|
||||
local append,format = table.insert,string.format
|
||||
|
||||
local function parseHashLines(chunk,brackets,esc)
|
||||
local exec_pat = "()$(%b"..brackets..")()"
|
||||
|
||||
local function parseDollarParen(pieces, chunk, s, e)
|
||||
local s = 1
|
||||
for term, executed, e in chunk:gmatch (exec_pat) do
|
||||
executed = '('..executed:sub(2,-2)..')'
|
||||
append(pieces,
|
||||
format("%q..(%s or '')..",chunk:sub(s, term - 1), executed))
|
||||
s = e
|
||||
end
|
||||
append(pieces, format("%q", chunk:sub(s)))
|
||||
end
|
||||
|
||||
local esc_pat = esc.."+([^\n]*\n?)"
|
||||
local esc_pat1, esc_pat2 = "^"..esc_pat, "\n"..esc_pat
|
||||
local pieces, s = {"return function(_put) ", n = 1}, 1
|
||||
while true do
|
||||
local ss, e, lua = chunk:find (esc_pat1, s)
|
||||
if not e then
|
||||
ss, e, lua = chunk:find(esc_pat2, s)
|
||||
append(pieces, "_put(")
|
||||
parseDollarParen(pieces, chunk:sub(s, ss))
|
||||
append(pieces, ")")
|
||||
if not e then break end
|
||||
end
|
||||
append(pieces, lua)
|
||||
s = e + 1
|
||||
end
|
||||
append(pieces, " end")
|
||||
return table.concat(pieces)
|
||||
end
|
||||
|
||||
local template = {}
|
||||
|
||||
--- expand the template using the specified environment.
|
||||
-- @param str the template string
|
||||
-- @param env the environment (by default empty). <br>
|
||||
-- There are three special fields in the environment table <ul>
|
||||
-- <li><code>_parent</code> continue looking up in this table</li>
|
||||
-- <li><code>_brackets</code>; default is '()', can be any suitable bracket pair</li>
|
||||
-- <li><code>_escape</code>; default is '#' </li>
|
||||
-- </ul>
|
||||
function template.substitute(str,env)
|
||||
env = env or {}
|
||||
if rawget(env,"_parent") then
|
||||
setmetatable(env,{__index = env._parent})
|
||||
end
|
||||
local brackets = rawget(env,"_brackets") or '()'
|
||||
local escape = rawget(env,"_escape") or '#'
|
||||
local code = parseHashLines(str,brackets,escape)
|
||||
local fn,err = utils.load(code,'TMP','t',env)
|
||||
if not fn then return nil,err end
|
||||
fn = fn()
|
||||
local out = {}
|
||||
local res,err = xpcall(function() fn(function(s)
|
||||
out[#out+1] = s
|
||||
end) end,debug.traceback)
|
||||
if not res then
|
||||
if env._debug then print(code) end
|
||||
return nil,err
|
||||
end
|
||||
return table.concat(out)
|
||||
end
|
||||
|
||||
return template
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
--- Useful test utilities.
|
||||
-- @module pl.test
|
||||
|
||||
local tablex = require 'pl.tablex'
|
||||
local utils = require 'pl.utils'
|
||||
local pretty = require 'pl.pretty'
|
||||
local path = require 'pl.path'
|
||||
local print,type = print,type
|
||||
local clock = os.clock
|
||||
local debug = require 'debug'
|
||||
local io,debug = io,debug
|
||||
|
||||
local function dump(x)
|
||||
if type(x) == 'table' and not (getmetatable(x) and getmetatable(x).__tostring) then
|
||||
return pretty.write(x,' ',true)
|
||||
else
|
||||
return tostring(x)
|
||||
end
|
||||
end
|
||||
|
||||
local test = {}
|
||||
|
||||
local function complain (x,y,msg)
|
||||
local i = debug.getinfo(3)
|
||||
local err = io.stderr
|
||||
err:write(path.basename(i.short_src)..':'..i.currentline..': assertion failed\n')
|
||||
err:write("got:\t",dump(x),'\n')
|
||||
err:write("needed:\t",dump(y),'\n')
|
||||
utils.quit(1,msg or "these values were not equal")
|
||||
end
|
||||
|
||||
--- like assert, except takes two arguments that must be equal and can be tables.
|
||||
-- If they are plain tables, it will use tablex.deepcompare.
|
||||
-- @param x any value
|
||||
-- @param y a value equal to x
|
||||
-- @param eps an optional tolerance for numerical comparisons
|
||||
function test.asserteq (x,y,eps)
|
||||
local res = x == y
|
||||
if not res then
|
||||
res = tablex.deepcompare(x,y,true,eps)
|
||||
end
|
||||
if not res then
|
||||
complain(x,y)
|
||||
end
|
||||
end
|
||||
|
||||
--- assert that the first string matches the second.
|
||||
-- @param s1 a string
|
||||
-- @param s2 a string
|
||||
function test.assertmatch (s1,s2)
|
||||
if not s1:match(s2) then
|
||||
complain (s1,s2,"these strings did not match")
|
||||
end
|
||||
end
|
||||
|
||||
function test.assertraise(fn,e)
|
||||
local ok, err = pcall(unpack(fn))
|
||||
if not err or err:match(e)==nil then
|
||||
complain (err,e,"these errors did not match")
|
||||
end
|
||||
end
|
||||
|
||||
--- a version of asserteq that takes two pairs of values.
|
||||
-- <code>x1==y1 and x2==y2</code> must be true. Useful for functions that naturally
|
||||
-- return two values.
|
||||
-- @param x1 any value
|
||||
-- @param x2 any value
|
||||
-- @param y1 any value
|
||||
-- @param y2 any value
|
||||
function test.asserteq2 (x1,x2,y1,y2)
|
||||
if x1 ~= y1 then complain(x1,y1) end
|
||||
if x2 ~= y2 then complain(x2,y2) end
|
||||
end
|
||||
|
||||
-- tuple type --
|
||||
|
||||
local tuple_mt = {}
|
||||
|
||||
function tuple_mt.__tostring(self)
|
||||
local ts = {}
|
||||
for i=1, self.n do
|
||||
local s = self[i]
|
||||
ts[i] = type(s) == 'string' and string.format('%q', s) or tostring(s)
|
||||
end
|
||||
return 'tuple(' .. table.concat(ts, ', ') .. ')'
|
||||
end
|
||||
|
||||
function tuple_mt.__eq(a, b)
|
||||
if a.n ~= b.n then return false end
|
||||
for i=1, a.n do
|
||||
if a[i] ~= b[i] then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- encode an arbitrary argument list as a tuple.
|
||||
-- This can be used to compare to other argument lists, which is
|
||||
-- very useful for testing functions which return a number of values.
|
||||
-- @usage asserteq(tuple( ('ab'):find 'a'), tuple(1,1))
|
||||
function test.tuple(...)
|
||||
return setmetatable({n=select('#', ...), ...}, tuple_mt)
|
||||
end
|
||||
|
||||
--- Time a function. Call the function a given number of times, and report the number of seconds taken,
|
||||
-- together with a message. Any extra arguments will be passed to the function.
|
||||
-- @param msg a descriptive message
|
||||
-- @param n number of times to call the function
|
||||
-- @param fun the function
|
||||
-- @param ... optional arguments to fun
|
||||
function test.timer(msg,n,fun,...)
|
||||
local start = clock()
|
||||
for i = 1,n do fun(...) end
|
||||
utils.printf("%s: took %7.2f sec\n",msg,clock()-start)
|
||||
end
|
||||
|
||||
return test
|
||||
@@ -0,0 +1,241 @@
|
||||
--- Text processing utilities. <p>
|
||||
-- This provides a Template class (modeled after the same from the Python
|
||||
-- libraries, see string.Template). It also provides similar functions to those
|
||||
-- found in the textwrap module.
|
||||
-- See <a href="../../index.html#templates">the Guide</a>.
|
||||
-- <p>
|
||||
-- Calling <code>text.format_operator()</code> overloads the % operator for strings to give Python/Ruby style formated output.
|
||||
-- This is extended to also do template-like substitution for map-like data.
|
||||
-- <pre class=example>
|
||||
-- > require 'pl.text'.format_operator()
|
||||
-- > = '%s = %5.3f' % {'PI',math.pi}
|
||||
-- PI = 3.142
|
||||
-- > = '$name = $value' % {name='dog',value='Pluto'}
|
||||
-- dog = Pluto
|
||||
-- </pre>
|
||||
-- @class module
|
||||
-- @name pl.text
|
||||
|
||||
local gsub = string.gsub
|
||||
local concat,append = table.concat,table.insert
|
||||
local utils = require 'pl.utils'
|
||||
local bind1,usplit,assert_arg,is_callable = utils.bind1,utils.split,utils.assert_arg,utils.is_callable
|
||||
|
||||
local function lstrip(str) return (str:gsub('^%s+','')) end
|
||||
local function strip(str) return (lstrip(str):gsub('%s+$','')) end
|
||||
local function make_list(l) return setmetatable(l,utils.stdmt.List) end
|
||||
local function split(s,delim) return make_list(usplit(s,delim)) end
|
||||
|
||||
local function imap(f,t,...)
|
||||
local res = {}
|
||||
for i = 1,#t do res[i] = f(t[i],...) end
|
||||
return res
|
||||
end
|
||||
|
||||
--[[
|
||||
module ('pl.text',utils._module)
|
||||
]]
|
||||
|
||||
local text = {}
|
||||
|
||||
local function _indent (s,sp)
|
||||
local sl = split(s,'\n')
|
||||
return concat(imap(bind1('..',sp),sl),'\n')..'\n'
|
||||
end
|
||||
|
||||
--- indent a multiline string.
|
||||
-- @param s the string
|
||||
-- @param n the size of the indent
|
||||
-- @param ch the character to use when indenting (default ' ')
|
||||
-- @return indented string
|
||||
function text.indent (s,n,ch)
|
||||
assert_arg(1,s,'string')
|
||||
assert_arg(2,s,'number')
|
||||
return _indent(s,string.rep(ch or ' ',n))
|
||||
end
|
||||
|
||||
--- dedent a multiline string by removing any initial indent.
|
||||
-- useful when working with [[..]] strings.
|
||||
-- @param s the string
|
||||
-- @return a string with initial indent zero.
|
||||
function text.dedent (s)
|
||||
assert_arg(1,s,'string')
|
||||
local sl = split(s,'\n')
|
||||
local i1,i2 = sl[1]:find('^%s*')
|
||||
sl = imap(string.sub,sl,i2+1)
|
||||
return concat(sl,'\n')..'\n'
|
||||
end
|
||||
|
||||
--- format a paragraph into lines so that they fit into a line width.
|
||||
-- It will not break long words, so lines can be over the length
|
||||
-- to that extent.
|
||||
-- @param s the string
|
||||
-- @param width the margin width, default 70
|
||||
-- @return a list of lines
|
||||
function text.wrap (s,width)
|
||||
assert_arg(1,s,'string')
|
||||
width = width or 70
|
||||
s = s:gsub('\n',' ')
|
||||
local i,nxt = 1
|
||||
local lines,line = {}
|
||||
while i < #s do
|
||||
nxt = i+width
|
||||
if s:find("[%w']",nxt) then -- inside a word
|
||||
nxt = s:find('%W',nxt+1) -- so find word boundary
|
||||
end
|
||||
line = s:sub(i,nxt)
|
||||
i = i + #line
|
||||
append(lines,strip(line))
|
||||
end
|
||||
return make_list(lines)
|
||||
end
|
||||
|
||||
--- format a paragraph so that it fits into a line width.
|
||||
-- @param s the string
|
||||
-- @param width the margin width, default 70
|
||||
-- @return a string
|
||||
-- @see wrap
|
||||
function text.fill (s,width)
|
||||
return concat(text.wrap(s,width),'\n') .. '\n'
|
||||
end
|
||||
|
||||
local Template = {}
|
||||
text.Template = Template
|
||||
Template.__index = Template
|
||||
setmetatable(Template, {
|
||||
__call = function(obj,tmpl)
|
||||
return Template.new(tmpl)
|
||||
end})
|
||||
|
||||
function Template.new(tmpl)
|
||||
assert_arg(1,tmpl,'string')
|
||||
local res = {}
|
||||
res.tmpl = tmpl
|
||||
setmetatable(res,Template)
|
||||
return res
|
||||
end
|
||||
|
||||
local function _substitute(s,tbl,safe)
|
||||
local subst
|
||||
if is_callable(tbl) then
|
||||
subst = tbl
|
||||
else
|
||||
function subst(f)
|
||||
local s = tbl[f]
|
||||
if not s then
|
||||
if safe then
|
||||
return f
|
||||
else
|
||||
error("not present in table "..f)
|
||||
end
|
||||
else
|
||||
return s
|
||||
end
|
||||
end
|
||||
end
|
||||
local res = gsub(s,'%${([%w_]+)}',subst)
|
||||
return (gsub(res,'%$([%w_]+)',subst))
|
||||
end
|
||||
|
||||
--- substitute values into a template, throwing an error.
|
||||
-- This will throw an error if no name is found.
|
||||
-- @param tbl a table of name-value pairs.
|
||||
function Template:substitute(tbl)
|
||||
assert_arg(1,tbl,'table')
|
||||
return _substitute(self.tmpl,tbl,false)
|
||||
end
|
||||
|
||||
--- substitute values into a template.
|
||||
-- This version just passes unknown names through.
|
||||
-- @param tbl a table of name-value pairs.
|
||||
function Template:safe_substitute(tbl)
|
||||
assert_arg(1,tbl,'table')
|
||||
return _substitute(self.tmpl,tbl,true)
|
||||
end
|
||||
|
||||
--- substitute values into a template, preserving indentation. <br>
|
||||
-- If the value is a multiline string _or_ a template, it will insert
|
||||
-- the lines at the correct indentation. <br>
|
||||
-- Furthermore, if a template, then that template will be subsituted
|
||||
-- using the same table.
|
||||
-- @param tbl a table of name-value pairs.
|
||||
function Template:indent_substitute(tbl)
|
||||
assert_arg(1,tbl,'table')
|
||||
if not self.strings then
|
||||
self.strings = split(self.tmpl,'\n')
|
||||
end
|
||||
-- the idea is to substitute line by line, grabbing any spaces as
|
||||
-- well as the $var. If the value to be substituted contains newlines,
|
||||
-- then we split that into lines and adjust the indent before inserting.
|
||||
local function subst(line)
|
||||
return line:gsub('(%s*)%$([%w_]+)',function(sp,f)
|
||||
local subtmpl
|
||||
local s = tbl[f]
|
||||
if not s then error("not present in table "..f) end
|
||||
if getmetatable(s) == Template then
|
||||
subtmpl = s
|
||||
s = s.tmpl
|
||||
else
|
||||
s = tostring(s)
|
||||
end
|
||||
if s:find '\n' then
|
||||
s = _indent(s,sp)
|
||||
end
|
||||
if subtmpl then return _substitute(s,tbl)
|
||||
else return s
|
||||
end
|
||||
end)
|
||||
end
|
||||
local lines = imap(subst,self.strings)
|
||||
return concat(lines,'\n')..'\n'
|
||||
end
|
||||
|
||||
------- Python-style formatting operator ------
|
||||
-- (see <a href="http://lua-users.org/wiki/StringInterpolation">the lua-users wiki</a>) --
|
||||
|
||||
function text.format_operator()
|
||||
|
||||
local format = string.format
|
||||
|
||||
-- a more forgiving version of string.format, which applies
|
||||
-- tostring() to any value with a %s format.
|
||||
local function formatx (fmt,...)
|
||||
local args = {...}
|
||||
local i = 1
|
||||
for p in fmt:gmatch('%%.') do
|
||||
if p == '%s' and type(args[i]) ~= 'string' then
|
||||
args[i] = tostring(args[i])
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
return format(fmt,unpack(args))
|
||||
end
|
||||
|
||||
local function basic_subst(s,t)
|
||||
return (s:gsub('%$([%w_]+)',t))
|
||||
end
|
||||
|
||||
-- Note this goes further than the original, and will allow these cases:
|
||||
-- 1. a single value
|
||||
-- 2. a list of values
|
||||
-- 3. a map of var=value pairs
|
||||
-- 4. a function, as in gsub
|
||||
-- For the second two cases, it uses $-variable substituion.
|
||||
getmetatable("").__mod = function(a, b)
|
||||
if b == nil then
|
||||
return a
|
||||
elseif type(b) == "table" and getmetatable(b) == nil then
|
||||
if #b == 0 then -- assume a map-like table
|
||||
return _substitute(a,b,true)
|
||||
else
|
||||
return formatx(a,unpack(b))
|
||||
end
|
||||
elseif type(b) == 'function' then
|
||||
return basic_subst(a,b)
|
||||
else
|
||||
return formatx(a,b)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return text
|
||||
@@ -0,0 +1,529 @@
|
||||
--- Generally useful routines.
|
||||
-- See <a href="../../index.html#utils">the Guide</a>.
|
||||
-- @class module
|
||||
-- @name pl.utils
|
||||
local format,gsub,byte = string.format,string.gsub,string.byte
|
||||
local clock = os.clock
|
||||
local stdout = io.stdout
|
||||
local append = table.insert
|
||||
|
||||
local collisions = {}
|
||||
|
||||
local utils = {}
|
||||
|
||||
utils._VERSION = "0.9.4"
|
||||
|
||||
utils.dir_separator = _G.package.config:sub(1,1)
|
||||
|
||||
--- end this program gracefully.
|
||||
-- @param code The exit code or a message to be printed
|
||||
-- @param ... extra arguments for message's format'
|
||||
-- @see utils.fprintf
|
||||
function utils.quit(code,...)
|
||||
if type(code) == 'string' then
|
||||
utils.fprintf(io.stderr,code,...)
|
||||
code = -1
|
||||
else
|
||||
utils.fprintf(io.stderr,...)
|
||||
end
|
||||
io.stderr:write('\n')
|
||||
os.exit(code)
|
||||
end
|
||||
|
||||
--- print an arbitrary number of arguments using a format.
|
||||
-- @param fmt The format (see string.format)
|
||||
-- @param ... Extra arguments for format
|
||||
function utils.printf(fmt,...)
|
||||
utils.fprintf(stdout,fmt,...)
|
||||
end
|
||||
|
||||
--- write an arbitrary number of arguments to a file using a format.
|
||||
-- @param f File handle to write to.
|
||||
-- @param fmt The format (see string.format).
|
||||
-- @param ... Extra arguments for format
|
||||
function utils.fprintf(f,fmt,...)
|
||||
utils.assert_string(2,fmt)
|
||||
f:write(format(fmt,...))
|
||||
end
|
||||
|
||||
local function import_symbol(T,k,v,libname)
|
||||
local key = rawget(T,k)
|
||||
-- warn about collisions!
|
||||
if key and k ~= '_M' and k ~= '_NAME' and k ~= '_PACKAGE' and k ~= '_VERSION' then
|
||||
utils.printf("warning: '%s.%s' overrides existing symbol\n",libname,k)
|
||||
end
|
||||
rawset(T,k,v)
|
||||
end
|
||||
|
||||
local function lookup_lib(T,t)
|
||||
for k,v in pairs(T) do
|
||||
if v == t then return k end
|
||||
end
|
||||
return '?'
|
||||
end
|
||||
|
||||
local already_imported = {}
|
||||
|
||||
--- take a table and 'inject' it into the local namespace.
|
||||
-- @param t The Table
|
||||
-- @param T An optional destination table (defaults to callers environment)
|
||||
function utils.import(t,T)
|
||||
T = T or _G
|
||||
t = t or utils
|
||||
if type(t) == 'string' then
|
||||
t = require (t)
|
||||
end
|
||||
local libname = lookup_lib(T,t)
|
||||
if already_imported[t] then return end
|
||||
already_imported[t] = libname
|
||||
for k,v in pairs(t) do
|
||||
import_symbol(T,k,v,libname)
|
||||
end
|
||||
end
|
||||
|
||||
utils.patterns = {
|
||||
FLOAT = '[%+%-%d]%d*%.?%d*[eE]?[%+%-]?%d*',
|
||||
INTEGER = '[+%-%d]%d*',
|
||||
IDEN = '[%a_][%w_]*',
|
||||
FILE = '[%a%.\\][:%][%w%._%-\\]*'
|
||||
}
|
||||
|
||||
--- escape any 'magic' characters in a string
|
||||
-- @param s The input string
|
||||
function utils.escape(s)
|
||||
utils.assert_string(1,s)
|
||||
return (s:gsub('[%-%.%+%[%]%(%)%$%^%%%?%*]','%%%1'))
|
||||
end
|
||||
|
||||
--- return either of two values, depending on a condition.
|
||||
-- @param cond A condition
|
||||
-- @param value1 Value returned if cond is true
|
||||
-- @param value2 Value returned if cond is false (can be optional)
|
||||
function utils.choose(cond,value1,value2)
|
||||
if cond then return value1
|
||||
else return value2
|
||||
end
|
||||
end
|
||||
|
||||
local raise
|
||||
|
||||
--- return the contents of a file as a string
|
||||
-- @param filename The file path
|
||||
-- @param is_bin open in binary mode
|
||||
-- @return file contents
|
||||
function utils.readfile(filename,is_bin)
|
||||
local mode = is_bin and 'b' or ''
|
||||
utils.assert_string(1,filename)
|
||||
local f,err = io.open(filename,'r'..mode)
|
||||
if not f then return utils.raise (err) end
|
||||
local res,err = f:read('*a')
|
||||
f:close()
|
||||
if not res then return raise (err) end
|
||||
return res
|
||||
end
|
||||
|
||||
--- write a string to a file
|
||||
-- @param filename The file path
|
||||
-- @param str The string
|
||||
-- @return true or nil
|
||||
-- @return error message
|
||||
-- @raise error if filename or str aren't strings
|
||||
function utils.writefile(filename,str)
|
||||
utils.assert_string(1,filename)
|
||||
utils.assert_string(2,str)
|
||||
local f,err = io.open(filename,'w')
|
||||
if not f then return raise(err) end
|
||||
f:write(str)
|
||||
f:close()
|
||||
return true
|
||||
end
|
||||
|
||||
--- return the contents of a file as a list of lines
|
||||
-- @param filename The file path
|
||||
-- @return file contents as a table
|
||||
-- @raise errror if filename is not a string
|
||||
function utils.readlines(filename)
|
||||
utils.assert_string(1,filename)
|
||||
local f,err = io.open(filename,'r')
|
||||
if not f then return raise(err) end
|
||||
local res = {}
|
||||
for line in f:lines() do
|
||||
append(res,line)
|
||||
end
|
||||
f:close()
|
||||
return res
|
||||
end
|
||||
|
||||
--- split a string into a list of strings separated by a delimiter.
|
||||
-- @param s The input string
|
||||
-- @param re A Lua string pattern; defaults to '%s+'
|
||||
-- @param plain don't use Lua patterns
|
||||
-- @param n optional maximum number of splits
|
||||
-- @return a list-like table
|
||||
-- @raise error if s is not a string
|
||||
function utils.split(s,re,plain,n)
|
||||
utils.assert_string(1,s)
|
||||
local find,sub,append = string.find, string.sub, table.insert
|
||||
local i1,ls = 1,{}
|
||||
if not re then re = '%s+' end
|
||||
if re == '' then return {s} end
|
||||
while true do
|
||||
local i2,i3 = find(s,re,i1,plain)
|
||||
if not i2 then
|
||||
local last = sub(s,i1)
|
||||
if last ~= '' then append(ls,last) end
|
||||
if #ls == 1 and ls[1] == '' then
|
||||
return {}
|
||||
else
|
||||
return ls
|
||||
end
|
||||
end
|
||||
append(ls,sub(s,i1,i2-1))
|
||||
if n and #ls == n then
|
||||
ls[#ls] = sub(s,i1)
|
||||
return ls
|
||||
end
|
||||
i1 = i3+1
|
||||
end
|
||||
end
|
||||
|
||||
--- split a string into a number of values.
|
||||
-- @param s the string
|
||||
-- @param re the delimiter, default space
|
||||
-- @return n values
|
||||
-- @usage first,next = splitv('jane:doe',':')
|
||||
-- @see split
|
||||
function utils.splitv (s,re)
|
||||
return unpack(utils.split(s,re))
|
||||
end
|
||||
|
||||
local lua52 = table.pack ~= nil
|
||||
local lua51_load = load
|
||||
|
||||
if not lua52 then -- define Lua 5.2 style load()
|
||||
function utils.load(str,src,mode,env)
|
||||
local chunk,err
|
||||
if type(str) == 'string' then
|
||||
chunk,err = loadstring(str,src)
|
||||
else
|
||||
chunk,err = lua51_load(str,src)
|
||||
end
|
||||
if chunk and env then setfenv(chunk,env) end
|
||||
return chunk,err
|
||||
end
|
||||
else
|
||||
utils.load = load
|
||||
-- setfenv/getfenv replacements for Lua 5.2
|
||||
-- by Sergey Rozhenko
|
||||
-- http://lua-users.org/lists/lua-l/2010-06/msg00313.html
|
||||
-- Roberto Ierusalimschy notes that it is possible for getfenv to return nil
|
||||
-- in the case of a function with no globals:
|
||||
-- http://lua-users.org/lists/lua-l/2010-06/msg00315.html
|
||||
function setfenv(f, t)
|
||||
f = (type(f) == 'function' and f or debug.getinfo(f + 1, 'f').func)
|
||||
local name
|
||||
local up = 0
|
||||
repeat
|
||||
up = up + 1
|
||||
name = debug.getupvalue(f, up)
|
||||
until name == '_ENV' or name == nil
|
||||
if name then
|
||||
debug.upvaluejoin(f, up, function() return name end, 1) -- use unique upvalue
|
||||
debug.setupvalue(f, up, t)
|
||||
end
|
||||
end
|
||||
|
||||
function getfenv(f)
|
||||
f = (type(f) == 'function' and f or debug.getinfo(f + 1, 'f').func)
|
||||
local name, val
|
||||
local up = 0
|
||||
repeat
|
||||
up = up + 1
|
||||
name, val = debug.getupvalue(f, up)
|
||||
until name == '_ENV' or name == nil
|
||||
return val
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--- execute a shell command.
|
||||
-- This is a compatibility function that returns the same for Lua 5.1 and Lua 5.2
|
||||
-- @param cmd a shell command
|
||||
-- @return true if successful
|
||||
-- @return actual return code
|
||||
function utils.execute (cmd)
|
||||
local res1,res2,res2 = os.execute(cmd)
|
||||
if not lua52 then
|
||||
return res1==0,res1
|
||||
else
|
||||
return res1,res2
|
||||
end
|
||||
end
|
||||
|
||||
if not lua52 then
|
||||
function table.pack (...)
|
||||
local n = select('#',...)
|
||||
return {n=n; ...},n
|
||||
end
|
||||
local sep = package.config:sub(1,1)
|
||||
function package.searchpath (mod,path)
|
||||
mod = mod:gsub('%.',sep)
|
||||
for m in path:gmatch('[^;]+') do
|
||||
local nm = m:gsub('?',mod)
|
||||
local f = io.open(nm,'r')
|
||||
if f then f:close(); return nm end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not table.pack then table.pack = _G.pack end
|
||||
if not rawget(_G,"pack") then _G.pack = table.pack end
|
||||
|
||||
--- take an arbitrary set of arguments and make into a table.
|
||||
-- This returns the table and the size; works fine for nil arguments
|
||||
-- @param ... arguments
|
||||
-- @return table
|
||||
-- @return table size
|
||||
-- @usage local t,n = utils.args(...)
|
||||
|
||||
--- 'memoize' a function (cache returned value for next call).
|
||||
-- This is useful if you have a function which is relatively expensive,
|
||||
-- but you don't know in advance what values will be required, so
|
||||
-- building a table upfront is wasteful/impossible.
|
||||
-- @param func a function of at least one argument
|
||||
-- @return a function with at least one argument, which is used as the key.
|
||||
function utils.memoize(func)
|
||||
return setmetatable({}, {
|
||||
__index = function(self, k, ...)
|
||||
local v = func(k,...)
|
||||
self[k] = v
|
||||
return v
|
||||
end,
|
||||
__call = function(self, k) return self[k] end
|
||||
})
|
||||
end
|
||||
|
||||
--- is the object either a function or a callable object?.
|
||||
-- @param obj Object to check.
|
||||
function utils.is_callable (obj)
|
||||
return type(obj) == 'function' or getmetatable(obj) and getmetatable(obj).__call
|
||||
end
|
||||
|
||||
--- is the object of the specified type?.
|
||||
-- If the type is a string, then use type, otherwise compare with metatable
|
||||
-- @param obj An object to check
|
||||
-- @param tp String of what type it should be
|
||||
function utils.is_type (obj,tp)
|
||||
if type(tp) == 'string' then return type(obj) == tp end
|
||||
local mt = getmetatable(obj)
|
||||
return tp == mt
|
||||
end
|
||||
|
||||
local fileMT = getmetatable(io.stdout)
|
||||
|
||||
--- a string representation of a type.
|
||||
-- For tables with metatables, we assume that the metatable has a `_name`
|
||||
-- field. Knows about Lua file objects.
|
||||
-- @param obj an object
|
||||
-- @return a string like 'number', 'table' or 'List'
|
||||
function utils.type (obj)
|
||||
local t = type(obj)
|
||||
if t == 'table' or t == 'userdata' then
|
||||
local mt = getmetatable(obj)
|
||||
if mt == fileMT then
|
||||
return 'file'
|
||||
else
|
||||
return mt._name or "unknown "..t
|
||||
end
|
||||
else
|
||||
return t
|
||||
end
|
||||
end
|
||||
|
||||
--- is this number an integer?
|
||||
-- @param x a number
|
||||
-- @raise error if x is not a number
|
||||
function utils.is_integer (x)
|
||||
return math.ceil(x)==x
|
||||
end
|
||||
|
||||
utils.stdmt = {
|
||||
List = {_name='List'}, Map = {_name='Map'},
|
||||
Set = {_name='Set'}, MultiMap = {_name='MultiMap'}
|
||||
}
|
||||
|
||||
local _function_factories = {}
|
||||
|
||||
--- associate a function factory with a type.
|
||||
-- A function factory takes an object of the given type and
|
||||
-- returns a function for evaluating it
|
||||
-- @param mt metatable
|
||||
-- @param fun a callable that returns a function
|
||||
function utils.add_function_factory (mt,fun)
|
||||
_function_factories[mt] = fun
|
||||
end
|
||||
|
||||
local function _string_lambda(f)
|
||||
local raise = utils.raise
|
||||
if f:find '^|' or f:find '_' then
|
||||
local args,body = f:match '|([^|]*)|(.+)'
|
||||
if f:find '_' then
|
||||
args = '_'
|
||||
body = f
|
||||
else
|
||||
if not args then return raise 'bad string lambda' end
|
||||
end
|
||||
local fstr = 'return function('..args..') return '..body..' end'
|
||||
local fn,err = loadstring(fstr)
|
||||
if not fn then return raise(err) end
|
||||
fn = fn()
|
||||
return fn
|
||||
else return raise 'not a string lambda'
|
||||
end
|
||||
end
|
||||
|
||||
--- an anonymous function as a string. This string is either of the form
|
||||
-- '|args| expression' or is a function of one argument, '_'
|
||||
-- @param lf function as a string
|
||||
-- @return a function
|
||||
-- @usage string_lambda '|x|x+1' (2) == 3
|
||||
-- @usage string_lambda '_+1 (2) == 3
|
||||
utils.string_lambda = utils.memoize(_string_lambda)
|
||||
|
||||
local ops
|
||||
|
||||
--- process a function argument.
|
||||
-- This is used throughout Penlight and defines what is meant by a function:
|
||||
-- Something that is callable, or an operator string as defined by <code>pl.operator</code>,
|
||||
-- such as '>' or '#'. If a function factory has been registered for the type, it will
|
||||
-- be called to get the function.
|
||||
-- @param idx argument index
|
||||
-- @param f a function, operator string, or callable object
|
||||
-- @param msg optional error message
|
||||
-- @return a callable
|
||||
-- @raise if idx is not a number or if f is not callable
|
||||
-- @see utils.is_callable
|
||||
function utils.function_arg (idx,f,msg)
|
||||
utils.assert_arg(1,idx,'number')
|
||||
local tp = type(f)
|
||||
if tp == 'function' then return f end -- no worries!
|
||||
-- ok, a string can correspond to an operator (like '==')
|
||||
if tp == 'string' then
|
||||
if not ops then ops = require 'pl.operator'.optable end
|
||||
local fn = ops[f]
|
||||
if fn then return fn end
|
||||
local fn, err = utils.string_lambda(f)
|
||||
if not fn then error(err..': '..f) end
|
||||
return fn
|
||||
elseif tp == 'table' or tp == 'userdata' then
|
||||
local mt = getmetatable(f)
|
||||
if not mt then error('not a callable object',2) end
|
||||
local ff = _function_factories[mt]
|
||||
if not ff then
|
||||
if not mt.__call then error('not a callable object',2) end
|
||||
return f
|
||||
else
|
||||
return ff(f) -- we have a function factory for this type!
|
||||
end
|
||||
end
|
||||
if not msg then msg = " must be callable" end
|
||||
if idx > 0 then
|
||||
error("argument "..idx..": "..msg,2)
|
||||
else
|
||||
error(msg,2)
|
||||
end
|
||||
end
|
||||
|
||||
--- bind the first argument of the function to a value.
|
||||
-- @param fn a function of at least two values (may be an operator string)
|
||||
-- @param p a value
|
||||
-- @return a function such that f(x) is fn(p,x)
|
||||
-- @raise same as @{function_arg}
|
||||
-- @see pl.func.curry
|
||||
function utils.bind1 (fn,p)
|
||||
fn = utils.function_arg(1,fn)
|
||||
return function(...) return fn(p,...) end
|
||||
end
|
||||
|
||||
--- assert that the given argument is in fact of the correct type.
|
||||
-- @param n argument index
|
||||
-- @param val the value
|
||||
-- @param tp the type
|
||||
-- @param verify an optional verfication function
|
||||
-- @param msg an optional custom message
|
||||
-- @param lev optional stack position for trace, default 2
|
||||
-- @raise if the argument n is not the correct type
|
||||
-- @usage assert_arg(1,t,'table')
|
||||
-- @usage assert_arg(n,val,'string',path.isdir,'not a directory')
|
||||
function utils.assert_arg (n,val,tp,verify,msg,lev)
|
||||
if type(val) ~= tp then
|
||||
error(("argument %d expected a '%s', got a '%s'"):format(n,tp,type(val)),2)
|
||||
end
|
||||
if verify and not verify(val) then
|
||||
error(("argument %d: '%s' %s"):format(n,val,msg),lev or 2)
|
||||
end
|
||||
end
|
||||
|
||||
--- assert the common case that the argument is a string.
|
||||
-- @param n argument index
|
||||
-- @param val a value that must be a string
|
||||
-- @raise val must be a string
|
||||
function utils.assert_string (n,val)
|
||||
utils.assert_arg(n,val,'string',nil,nil,nil,3)
|
||||
end
|
||||
|
||||
local err_mode = 'default'
|
||||
|
||||
--- control the error strategy used by Penlight.
|
||||
-- Controls how <code>utils.raise</code> works; the default is for it
|
||||
-- to return nil and the error string, but if the mode is 'error' then
|
||||
-- it will throw an error. If mode is 'quit' it will immediately terminate
|
||||
-- the program.
|
||||
-- @param mode - either 'default', 'quit' or 'error'
|
||||
-- @see utils.raise
|
||||
function utils.on_error (mode)
|
||||
err_mode = mode
|
||||
end
|
||||
|
||||
--- used by Penlight functions to return errors. Its global behaviour is controlled
|
||||
-- by <code>utils.on_error</code>
|
||||
-- @param err the error string.
|
||||
-- @see utils.on_error
|
||||
function utils.raise (err)
|
||||
if err_mode == 'default' then return nil,err
|
||||
elseif err_mode == 'quit' then utils.quit(err)
|
||||
else error(err,2)
|
||||
end
|
||||
end
|
||||
|
||||
raise = utils.raise
|
||||
|
||||
--- load a code string or bytecode chunk.
|
||||
-- @param code Lua code as a string or bytecode
|
||||
-- @param name for source errors
|
||||
-- @param mode kind of chunk, 't' for text, 'b' for bytecode, 'bt' for all (default)
|
||||
-- @param env the environment for the new chunk (default nil)
|
||||
-- @return compiled chunk
|
||||
-- @return error message (chunk is nil)
|
||||
-- @function utils.load
|
||||
|
||||
|
||||
--- Lua 5.2 Compatible Functions
|
||||
-- @section lua52
|
||||
|
||||
--- pack an argument list into a table.
|
||||
-- @param ... any arguments
|
||||
-- @return a table with field n set to the length
|
||||
-- @return the length
|
||||
-- @function table.pack
|
||||
|
||||
------
|
||||
-- return the full path where a Lua module name would be matched.
|
||||
-- @param mod module name, possibly dotted
|
||||
-- @param path a path in the same form as package.path or package.cpath
|
||||
-- @see path.package_path
|
||||
-- @function package.searchpath
|
||||
|
||||
return utils
|
||||
|
||||
|
||||
@@ -0,0 +1,676 @@
|
||||
--- XML LOM Utilities.
|
||||
-- This implements some useful things on LOM documents, such as returned by lxp.lom.parse.
|
||||
-- In particular, it can convert LOM back into XML text, with optional pretty-printing control.
|
||||
-- It's based on stanza.lua from Prosody http://hg.prosody.im/trunk/file/4621c92d2368/util/stanza.lua)
|
||||
--
|
||||
-- Can be used as a lightweight one-stop-shop for simple XML processing; a simple XML parser is included
|
||||
-- but the default is to use lxp.lom if it can be found.
|
||||
-- <pre>
|
||||
-- Prosody IM
|
||||
-- Copyright (C) 2008-2010 Matthew Wild
|
||||
-- Copyright (C) 2008-2010 Waqas Hussain
|
||||
--
|
||||
-- classic Lua XML parser by Roberto Ierusalimschy.
|
||||
-- modified to output LOM format.
|
||||
-- http://lua-users.org/wiki/LuaXml
|
||||
-- </pre>
|
||||
-- @module pl.xml
|
||||
|
||||
local t_insert = table.insert;
|
||||
local t_concat = table.concat;
|
||||
local t_remove = table.remove;
|
||||
local s_format = string.format;
|
||||
local s_match = string.match;
|
||||
local tostring = tostring;
|
||||
local setmetatable = setmetatable;
|
||||
local getmetatable = getmetatable;
|
||||
local pairs = pairs;
|
||||
local ipairs = ipairs;
|
||||
local type = type;
|
||||
local next = next;
|
||||
local print = print;
|
||||
local unpack = unpack or table.unpack;
|
||||
local s_gsub = string.gsub;
|
||||
local s_char = string.char;
|
||||
local s_find = string.find;
|
||||
local os = os;
|
||||
local pcall,require,io = pcall,require,io
|
||||
local split = require 'pl.utils'.split
|
||||
|
||||
local _M = {}
|
||||
local Doc = { __type = "doc" };
|
||||
Doc.__index = Doc;
|
||||
|
||||
--- create a new document node.
|
||||
-- @param tag the tag name
|
||||
-- @param attr optional attributes (table of name-value pairs)
|
||||
function _M.new(tag, attr)
|
||||
local doc = { tag = tag, attr = attr or {}, last_add = {}};
|
||||
return setmetatable(doc, Doc);
|
||||
end
|
||||
|
||||
--- parse an XML document. By default, this uses lxp.lom.parse, but
|
||||
-- falls back to basic_parse, or if use_basic is true
|
||||
-- @param text_or_file file or string representation
|
||||
-- @param is_file whether text_or_file is a file name or not
|
||||
-- @param use_basic do a basic parse
|
||||
-- @return a parsed LOM document with the document metatatables set
|
||||
-- @return nil, error the error can either be a file error or a parse error
|
||||
function _M.parse(text_or_file, is_file, use_basic)
|
||||
local parser,status,lom
|
||||
if use_basic then parser = _M.basic_parse
|
||||
else
|
||||
status,lom = pcall(require,'lxp.lom')
|
||||
if not status then parser = _M.basic_parse else parser = lom.parse end
|
||||
end
|
||||
if is_file then
|
||||
local f,err = io.open(text_or_file)
|
||||
if not f then return nil,err end
|
||||
text_or_file = f:read '*a'
|
||||
f:close()
|
||||
end
|
||||
local doc,err = parser(text_or_file)
|
||||
if not doc then return nil,err end
|
||||
if lom then
|
||||
_M.walk(doc,false,function(_,d)
|
||||
setmetatable(d,Doc)
|
||||
end)
|
||||
end
|
||||
return doc
|
||||
end
|
||||
|
||||
---- convenient function to add a document node, This updates the last inserted position.
|
||||
-- @param tag a tag name
|
||||
-- @param attrs optional set of attributes (name-string pairs)
|
||||
function Doc:addtag(tag, attrs)
|
||||
local s = _M.new(tag, attrs);
|
||||
(self.last_add[#self.last_add] or self):add_direct_child(s);
|
||||
t_insert(self.last_add, s);
|
||||
return self;
|
||||
end
|
||||
|
||||
--- convenient function to add a text node. This updates the last inserted position.
|
||||
-- @param text a string
|
||||
function Doc:text(text)
|
||||
(self.last_add[#self.last_add] or self):add_direct_child(text);
|
||||
return self;
|
||||
end
|
||||
|
||||
---- go up one level in a document
|
||||
function Doc:up()
|
||||
t_remove(self.last_add);
|
||||
return self;
|
||||
end
|
||||
|
||||
function Doc:reset()
|
||||
local last_add = self.last_add;
|
||||
for i = 1,#last_add do
|
||||
last_add[i] = nil;
|
||||
end
|
||||
return self;
|
||||
end
|
||||
|
||||
--- append a child to a document directly.
|
||||
-- @param child a child node (either text or a document)
|
||||
function Doc:add_direct_child(child)
|
||||
t_insert(self, child);
|
||||
end
|
||||
|
||||
--- append a child to a document at the last element added
|
||||
-- @param child a child node (either text or a document)
|
||||
function Doc:add_child(child)
|
||||
(self.last_add[#self.last_add] or self):add_direct_child(child);
|
||||
return self;
|
||||
end
|
||||
|
||||
--accessing attributes: useful not to have to expose implementation (attr)
|
||||
--but also can allow attr to be nil in any future optimizations
|
||||
|
||||
--- set attributes of a document node.
|
||||
-- @param t a table containing attribute/value pairs
|
||||
function Doc:set_attribs (t)
|
||||
for k,v in pairs(t) do
|
||||
self.attr[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
--- set a single attribute of a document node.
|
||||
-- @param a attribute
|
||||
-- @param v its value
|
||||
function Doc:set_attrib(a,v)
|
||||
self.attr[a] = v
|
||||
end
|
||||
|
||||
--- access the attributes of a document node.
|
||||
function Doc:get_attribs()
|
||||
return self.attr
|
||||
end
|
||||
|
||||
--- function to create an element with a given tag name and a set of children.
|
||||
-- @param tag a tag name
|
||||
-- @param items either text or a table where the hash part is the attributes and the list part is the children.
|
||||
function _M.elem(tag,items)
|
||||
local s = _M.new(tag)
|
||||
if type(items) == 'string' then items = {items} end
|
||||
if _M.is_tag(items) then
|
||||
t_insert(s,items)
|
||||
elseif type(items) == 'table' then
|
||||
for k,v in pairs(items) do
|
||||
if type(k) == 'string' then
|
||||
s.attr[k] = v
|
||||
t_insert(s.attr,k)
|
||||
else
|
||||
s[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
return s
|
||||
end
|
||||
|
||||
--- given a list of names, return a number of element constructors.
|
||||
-- @param list a list of names, or a comma-separated string.
|
||||
-- @usage local parent,children = doc.tags 'parent,children' <br>
|
||||
-- doc = parent {child 'one', child 'two'}
|
||||
function _M.tags(list)
|
||||
local ctors = {}
|
||||
local elem = _M.elem
|
||||
if type(list) == 'string' then list = split(list,'%s*,%s*') end
|
||||
for _,tag in ipairs(list) do
|
||||
local ctor = function(items) return _M.elem(tag,items) end
|
||||
t_insert(ctors,ctor)
|
||||
end
|
||||
return unpack(ctors)
|
||||
end
|
||||
|
||||
local templ_cache = {}
|
||||
|
||||
local function is_data(data)
|
||||
return #data == 0 or type(data[1]) ~= 'table'
|
||||
end
|
||||
|
||||
local function prepare_data(data)
|
||||
-- a hack for ensuring that $1 maps to first element of data, etc.
|
||||
-- Either this or could change the gsub call just below.
|
||||
for i,v in ipairs(data) do
|
||||
data[tostring(i)] = v
|
||||
end
|
||||
end
|
||||
|
||||
--- create a substituted copy of a document,
|
||||
-- @param templ may be a document or a string representation which will be parsed and cached
|
||||
-- @param data a table of name-value pairs or a list of such tables
|
||||
-- @return an XML document
|
||||
function Doc.subst(templ, data)
|
||||
if type(data) ~= 'table' or not next(data) then return nil, "data must be a non-empty table" end
|
||||
if is_data(data) then
|
||||
prepare_data(data)
|
||||
end
|
||||
if type(templ) == 'string' then
|
||||
if templ_cache[templ] then
|
||||
templ = templ_cache[templ]
|
||||
else
|
||||
local str,err = templ
|
||||
templ,err = _M.parse(str)
|
||||
if not templ then return nil,err end
|
||||
templ_cache[str] = templ
|
||||
end
|
||||
end
|
||||
local function _subst(item)
|
||||
return _M.clone(templ,function(s)
|
||||
return s:gsub('%$(%w+)',item)
|
||||
end)
|
||||
end
|
||||
if is_data(data) then return _subst(data) end
|
||||
local list = {}
|
||||
for _,item in ipairs(data) do
|
||||
prepare_data(item)
|
||||
t_insert(list,_subst(item))
|
||||
end
|
||||
if data.tag then
|
||||
list = _M.elem(data.tag,list)
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
|
||||
--- get the first child with a given tag name.
|
||||
-- @param tag the tag name
|
||||
function Doc:child_with_name(tag)
|
||||
for _, child in ipairs(self) do
|
||||
if child.tag == tag then return child; end
|
||||
end
|
||||
end
|
||||
|
||||
local _children_with_name
|
||||
function _children_with_name(self,tag,list,recurse)
|
||||
for _, child in ipairs(self) do if type(child) == 'table' then
|
||||
if child.tag == tag then t_insert(list,child) end
|
||||
if recurse then _children_with_name(child,tag,list,recurse) end
|
||||
end end
|
||||
end
|
||||
|
||||
--- get all elements in a document that have a given tag.
|
||||
-- @param tag a tag name
|
||||
-- @param dont_recurse optionally only return the immediate children with this tag name
|
||||
-- @return a list of elements
|
||||
function Doc:get_elements_with_name(tag,dont_recurse)
|
||||
local res = {}
|
||||
_children_with_name(self,tag,res,not dont_recurse)
|
||||
return res
|
||||
end
|
||||
|
||||
-- iterate over all children of a document node, including text nodes.
|
||||
function Doc:children()
|
||||
local i = 0;
|
||||
return function (a)
|
||||
i = i + 1
|
||||
return a[i];
|
||||
end, self, i;
|
||||
end
|
||||
|
||||
-- return the first child element of a node, if it exists.
|
||||
function Doc:first_childtag()
|
||||
if #self == 0 then return end
|
||||
for _,t in ipairs(self) do
|
||||
if type(t) == 'table' then return t end
|
||||
end
|
||||
end
|
||||
|
||||
function Doc:matching_tags(tag, xmlns)
|
||||
xmlns = xmlns or self.attr.xmlns;
|
||||
local tags = self;
|
||||
local start_i, max_i = 1, #tags;
|
||||
return function ()
|
||||
for i=start_i,max_i do
|
||||
v = tags[i];
|
||||
if (not tag or v.tag == tag)
|
||||
and (not xmlns or xmlns == v.attr.xmlns) then
|
||||
start_i = i+1;
|
||||
return v;
|
||||
end
|
||||
end
|
||||
end, tags, i;
|
||||
end
|
||||
|
||||
--- iterate over all child elements of a document node.
|
||||
function Doc:childtags()
|
||||
local i = 0;
|
||||
return function (a)
|
||||
local v
|
||||
repeat
|
||||
i = i + 1
|
||||
v = self[i]
|
||||
if v and type(v) == 'table' then return v; end
|
||||
until not v
|
||||
end, self[1], i;
|
||||
end
|
||||
|
||||
--- visit child element of a node and call a function, possibility modifying the document.
|
||||
-- @param callback a function passed the node (text or element). If it returns nil, that node will be removed.
|
||||
-- If it returns a value, that will replace the current node.
|
||||
function Doc:maptags(callback)
|
||||
local is_tag = _M.is_tag
|
||||
local i = 1;
|
||||
while i <= #self do
|
||||
if is_tag(self[i]) then
|
||||
local ret = callback(self[i]);
|
||||
if ret == nil then
|
||||
t_remove(self, i);
|
||||
else
|
||||
self[i] = ret;
|
||||
i = i + 1;
|
||||
end
|
||||
end
|
||||
end
|
||||
return self;
|
||||
end
|
||||
|
||||
local xml_escape
|
||||
do
|
||||
local escape_table = { ["'"] = "'", ["\""] = """, ["<"] = "<", [">"] = ">", ["&"] = "&" };
|
||||
function xml_escape(str) return (s_gsub(str, "['&<>\"]", escape_table)); end
|
||||
_M.xml_escape = xml_escape;
|
||||
end
|
||||
|
||||
-- pretty printing
|
||||
-- if indent, then put each new tag on its own line
|
||||
-- if attr_indent, put each new attribute on its own line
|
||||
local function _dostring(t, buf, self, xml_escape, parentns, idn, indent, attr_indent)
|
||||
local nsid = 0;
|
||||
local tag = t.tag
|
||||
local lf,alf = ""," "
|
||||
if indent then lf = '\n'..idn end
|
||||
if attr_indent then alf = '\n'..idn..attr_indent end
|
||||
t_insert(buf, lf.."<"..tag);
|
||||
for k, v in pairs(t.attr) do
|
||||
if type(k) ~= 'number' then -- LOM attr table has list-like part
|
||||
if s_find(k, "\1", 1, true) then
|
||||
local ns, attrk = s_match(k, "^([^\1]*)\1?(.*)$");
|
||||
nsid = nsid + 1;
|
||||
t_insert(buf, " xmlns:ns"..nsid.."='"..xml_escape(ns).."' ".."ns"..nsid..":"..attrk.."='"..xml_escape(v).."'");
|
||||
elseif not(k == "xmlns" and v == parentns) then
|
||||
t_insert(buf, alf..k.."='"..xml_escape(v).."'");
|
||||
end
|
||||
end
|
||||
end
|
||||
local len,has_children = #t;
|
||||
if len == 0 then
|
||||
local out = "/>"
|
||||
if attr_indent then out = '\n'..idn..out end
|
||||
t_insert(buf, out);
|
||||
else
|
||||
t_insert(buf, ">");
|
||||
for n=1,len do
|
||||
local child = t[n];
|
||||
if child.tag then
|
||||
self(child, buf, self, xml_escape, t.attr.xmlns,idn and idn..indent, indent, attr_indent );
|
||||
has_children = true
|
||||
else -- text element
|
||||
t_insert(buf, xml_escape(child));
|
||||
end
|
||||
end
|
||||
t_insert(buf, (has_children and lf or '').."</"..tag..">");
|
||||
end
|
||||
end
|
||||
|
||||
---- pretty-print an XML document
|
||||
--- @param t an XML document
|
||||
--- @param idn an initial indent (indents are all strings)
|
||||
--- @param indent an indent for each level
|
||||
--- @param attr_indent if given, indent each attribute pair and put on a separate line
|
||||
--- @return a string representation
|
||||
function _M.tostring(t,idn,indent, attr_indent)
|
||||
local buf = {};
|
||||
_dostring(t, buf, _dostring, xml_escape, nil,idn,indent, attr_indent);
|
||||
return t_concat(buf);
|
||||
end
|
||||
|
||||
Doc.__tostring = _M.tostring
|
||||
|
||||
--- get the full text value of an element
|
||||
function Doc:get_text()
|
||||
local res = {}
|
||||
for i,el in ipairs(self) do
|
||||
if type(el) == 'string' then t_insert(res,el) end
|
||||
end
|
||||
return t_concat(res);
|
||||
end
|
||||
|
||||
--- make a copy of a document
|
||||
-- @param doc the original document
|
||||
-- @param strsubst an optional function for handling string copying which could do substitution, etc.
|
||||
function _M.clone(doc, strsubst)
|
||||
local lookup_table = {};
|
||||
local function _copy(object)
|
||||
if type(object) ~= "table" then
|
||||
if strsubst and type(object) == 'string' then return strsubst(object)
|
||||
else return object;
|
||||
end
|
||||
elseif lookup_table[object] then
|
||||
return lookup_table[object];
|
||||
end
|
||||
local new_table = {};
|
||||
lookup_table[object] = new_table;
|
||||
for index, value in pairs(object) do
|
||||
new_table[_copy(index)] = _copy(value); -- is cloning keys much use, hm?
|
||||
end
|
||||
return setmetatable(new_table, getmetatable(object));
|
||||
end
|
||||
|
||||
return _copy(doc)
|
||||
end
|
||||
|
||||
--- compare two documents.
|
||||
-- @param t1 any value
|
||||
-- @param t2 any value
|
||||
function _M.compare(t1,t2)
|
||||
local ty1 = type(t1)
|
||||
local ty2 = type(t2)
|
||||
if ty1 ~= ty2 then return false, 'type mismatch' end
|
||||
if ty1 == 'string' then
|
||||
return t1 == t2 and true or 'text '..t1..' ~= text '..t2
|
||||
end
|
||||
if ty1 ~= 'table' or ty2 ~= 'table' then return false, 'not a document' end
|
||||
if t1.tag ~= t2.tag then return false, 'tag '..t1.tag..' ~= tag '..t2.tag end
|
||||
if #t1 ~= #t2 then return false, 'size '..#t1..' ~= size '..#t2..' for tag '..t1.tag end
|
||||
-- compare attributes
|
||||
for k,v in pairs(t1.attr) do
|
||||
if t2.attr[k] ~= v then return false, 'mismatch attrib' end
|
||||
end
|
||||
for k,v in pairs(t2.attr) do
|
||||
if t1.attr[k] ~= v then return false, 'mismatch attrib' end
|
||||
end
|
||||
-- compare children
|
||||
for i = 1,#t1 do
|
||||
local yes,err = _M.compare(t1[i],t2[i])
|
||||
if not yes then return err end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- is this value a document element?
|
||||
-- @param d any value
|
||||
function _M.is_tag(d)
|
||||
return type(d) == 'table' and type(d.tag) == 'string'
|
||||
end
|
||||
|
||||
--- call the desired function recursively over the document.
|
||||
-- @param doc the document
|
||||
-- @param depth_first visit child notes first, then the current node
|
||||
-- @param operation a function which will receive the current tag name and current node.
|
||||
function _M.walk (doc, depth_first, operation)
|
||||
if not depth_first then operation(doc.tag,doc) end
|
||||
for _,d in ipairs(doc) do
|
||||
if _M.is_tag(d) then
|
||||
_M.walk(d,depth_first,operation)
|
||||
end
|
||||
end
|
||||
if depth_first then operation(doc.tag,doc) end
|
||||
end
|
||||
|
||||
local escapes = { quot = "\"", apos = "'", lt = "<", gt = ">", amp = "&" }
|
||||
local function unescape(str) return (str:gsub( "&(%a+);", escapes)); end
|
||||
|
||||
local function parseargs(s)
|
||||
local arg = {}
|
||||
s:gsub("([%w:]+)%s*=%s*([\"'])(.-)%2", function (w, _, a)
|
||||
arg[w] = unescape(a)
|
||||
end)
|
||||
return arg
|
||||
end
|
||||
|
||||
--- Parse a simple XML document using a pure Lua parser based on Robero Ierusalimschy's original version.
|
||||
-- @param s the XML document to be parsed.
|
||||
-- @param all_text if true, preserves all whitespace. Otherwise only text containing non-whitespace is included.
|
||||
function _M.basic_parse(s,all_text)
|
||||
local t_insert,t_remove = table.insert,table.remove
|
||||
local s_find,s_sub = string.find,string.sub
|
||||
local stack = {}
|
||||
local top = {}
|
||||
t_insert(stack, top)
|
||||
local ni,c,label,xarg, empty
|
||||
local i, j = 1, 1
|
||||
-- we're not interested in <?xml version="1.0"?>
|
||||
local _,istart = s_find(s,'^%s*<%?[^%?]+%?>%s*')
|
||||
if istart then i = istart+1 end
|
||||
while true do
|
||||
ni,j,c,label,xarg, empty = s_find(s, "<(%/?)([%w:%-_]+)(.-)(%/?)>", i)
|
||||
if not ni then break end
|
||||
local text = s_sub(s, i, ni-1)
|
||||
if all_text or not s_find(text, "^%s*$") then
|
||||
t_insert(top, unescape(text))
|
||||
end
|
||||
if empty == "/" then -- empty element tag
|
||||
t_insert(top, setmetatable({tag=label, attr=parseargs(xarg), empty=1},Doc))
|
||||
elseif c == "" then -- start tag
|
||||
top = setmetatable({tag=label, attr=parseargs(xarg)},Doc)
|
||||
t_insert(stack, top) -- new level
|
||||
else -- end tag
|
||||
local toclose = t_remove(stack) -- remove top
|
||||
top = stack[#stack]
|
||||
if #stack < 1 then
|
||||
error("nothing to close with "..label)
|
||||
end
|
||||
if toclose.tag ~= label then
|
||||
error("trying to close "..toclose.tag.." with "..label)
|
||||
end
|
||||
t_insert(top, toclose)
|
||||
end
|
||||
i = j+1
|
||||
end
|
||||
local text = s_sub(s, i)
|
||||
if all_text or not s_find(text, "^%s*$") then
|
||||
t_insert(stack[#stack], unescape(text))
|
||||
end
|
||||
if #stack > 1 then
|
||||
error("unclosed "..stack[#stack].tag)
|
||||
end
|
||||
local res = stack[1]
|
||||
return type(res[1])=='string' and res[2] or res[1]
|
||||
end
|
||||
|
||||
local function empty(attr) return not attr or not next(attr) end
|
||||
local function is_text(s) return type(s) == 'string' end
|
||||
local function is_element(d) return type(d) == 'table' and d.tag ~= nil end
|
||||
|
||||
-- returns the key,value pair from a table if it has exactly one entry
|
||||
local function has_one_element(t)
|
||||
local key,value = next(t)
|
||||
if next(t,key) ~= nil then return false end
|
||||
return key,value
|
||||
end
|
||||
|
||||
local function append_capture(res,tbl)
|
||||
if not empty(tbl) then -- no point in capturing empty tables...
|
||||
local key
|
||||
if tbl._ then -- if $_ was set then it is meant as the top-level key for the captured table
|
||||
key = tbl._
|
||||
tbl._ = nil
|
||||
if empty(tbl) then return end
|
||||
end
|
||||
-- a table with only one pair {[0]=value} shall be reduced to that value
|
||||
local numkey,val = has_one_element(tbl)
|
||||
if numkey == 0 then tbl = val end
|
||||
if key then
|
||||
res[key] = tbl
|
||||
else -- otherwise, we append the captured table
|
||||
t_insert(res,tbl)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function make_number(pat)
|
||||
if pat:find '^%d+$' then -- $1 etc means use this as an array location
|
||||
pat = tonumber(pat)
|
||||
end
|
||||
return pat
|
||||
end
|
||||
|
||||
local function capture_attrib(res,pat,value)
|
||||
pat = make_number(pat:sub(2))
|
||||
res[pat] = value
|
||||
return true
|
||||
end
|
||||
|
||||
local match
|
||||
function match(d,pat,res,keep_going)
|
||||
local ret = true
|
||||
if d == nil then return false end
|
||||
-- attribute string matching is straight equality, except if the pattern is a $ capture,
|
||||
-- which always succeeds.
|
||||
if type(d) == 'string' then
|
||||
if type(pat) ~= 'string' then return false end
|
||||
if _M.debug then print(d,pat) end
|
||||
if pat:find '^%$' then
|
||||
return capture_attrib(res,pat,d)
|
||||
else
|
||||
return d == pat
|
||||
end
|
||||
else
|
||||
if _M.debug then print(d.tag,pat.tag) end
|
||||
-- this is an element node. For a match to succeed, the attributes must
|
||||
-- match as well.
|
||||
-- a tagname in the pattern ending with '-' is a wildcard and matches like an attribute
|
||||
local tagpat = pat.tag:match '^(.-)%-$'
|
||||
if tagpat then
|
||||
tagpat = make_number(tagpat)
|
||||
res[tagpat] = d.tag
|
||||
end
|
||||
if d.tag == pat.tag or tagpat then
|
||||
|
||||
if not empty(pat.attr) then
|
||||
if empty(d.attr) then ret = false
|
||||
else
|
||||
for prop,pval in pairs(pat.attr) do
|
||||
local dval = d.attr[prop]
|
||||
if not match(dval,pval,res) then ret = false; break end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- the pattern may have child nodes. We match partially, so that {P1,P2} shall match {X,P1,X,X,P2,..}
|
||||
if ret and #pat > 0 then
|
||||
local i,j = 1,1
|
||||
local function next_elem()
|
||||
j = j + 1 -- next child element of data
|
||||
if is_text(d[j]) then j = j + 1 end
|
||||
return j <= #d
|
||||
end
|
||||
repeat
|
||||
local p = pat[i]
|
||||
-- repeated {{<...>}} patterns shall match one or more elements
|
||||
-- so e.g. {P+} will match {X,X,P,P,X,P,X,X,X}
|
||||
if is_element(p) and p.repeated then
|
||||
local found
|
||||
repeat
|
||||
local tbl = {}
|
||||
ret = match(d[j],p,tbl,false)
|
||||
if ret then
|
||||
found = false --true
|
||||
append_capture(res,tbl)
|
||||
end
|
||||
until not next_elem() or (found and not ret)
|
||||
i = i + 1
|
||||
else
|
||||
ret = match(d[j],p,res,false)
|
||||
if ret then i = i + 1 end
|
||||
end
|
||||
until not next_elem() or i > #pat -- run out of elements or patterns to match
|
||||
-- if every element in our pattern matched ok, then it's been a successful match
|
||||
if i > #pat then return true end
|
||||
end
|
||||
if ret then return true end
|
||||
else
|
||||
ret = false
|
||||
end
|
||||
-- keep going anyway - look at the children!
|
||||
if keep_going then
|
||||
for child in d:childtags() do
|
||||
ret = match(child,pat,res,keep_going)
|
||||
if ret then break end
|
||||
end
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
function Doc:match(pat)
|
||||
if is_text(pat) then
|
||||
pat = _M.parse(pat,false,true)
|
||||
end
|
||||
_M.walk(pat,false,function(_,d)
|
||||
if is_text(d[1]) and is_element(d[2]) and is_text(d[3]) and
|
||||
d[1]:find '%s*{{' and d[3]:find '}}%s*' then
|
||||
t_remove(d,1)
|
||||
t_remove(d,2)
|
||||
d[1].repeated = true
|
||||
end
|
||||
end)
|
||||
|
||||
local res = {}
|
||||
local ret = match(self,pat,res,true)
|
||||
return res,ret
|
||||
end
|
||||
|
||||
|
||||
return _M
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
--------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2012-2014 Sierra Wireless.
|
||||
-- All rights reserved. This program and the accompanying materials
|
||||
-- are made available under the terms of the Eclipse Public License v1.0
|
||||
-- which accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Kevin KIN-FOO <kkinfoo@sierrawireless.com>
|
||||
-- - initial API and implementation and initial documentation
|
||||
--------------------------------------------------------------------------------
|
||||
return[[#
|
||||
<div id="content">
|
||||
# --
|
||||
# -- Module name
|
||||
# --
|
||||
# if _file.name then
|
||||
<h$(i)>Module <code>$(_file.name)</code></h$(i)>
|
||||
# end
|
||||
# --
|
||||
# -- Descriptions
|
||||
# --
|
||||
# if _file.shortdescription then
|
||||
$( format(_file.shortdescription) )
|
||||
# end
|
||||
# if _file.description and #_file.description > 0 then
|
||||
$( format(_file.description) )
|
||||
# end
|
||||
# --
|
||||
# -- Handle "@usage" special tag
|
||||
# --
|
||||
#if _file.metadata and _file.metadata.usage then
|
||||
$( applytemplate(_file.metadata.usage, i+1) )
|
||||
#end
|
||||
# --
|
||||
# -- Show quick description of current type
|
||||
# --
|
||||
#
|
||||
# -- show quick description for globals
|
||||
# if not isempty(_file.globalvars) then
|
||||
<h$(i+1)>Global(s)</h$(i+1)>
|
||||
<table class="function_list">
|
||||
# for _, item in sortedpairs(_file.globalvars) do
|
||||
<tr>
|
||||
<td class="name" nowrap="nowrap">$( fulllinkto(item) )</td>
|
||||
<td class="summary">$( format(item.shortdescription) )</td>
|
||||
</tr>
|
||||
# end
|
||||
</table>
|
||||
# end
|
||||
#
|
||||
# -- get type corresponding to this file (module)
|
||||
# local currenttype
|
||||
# local typeref = _file:moduletyperef()
|
||||
# if typeref and typeref.tag == "internaltyperef" then
|
||||
# local typedef = _file.types[typeref.typename]
|
||||
# if typedef and typedef.tag == "recordtypedef" then
|
||||
# currenttype = typedef
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# -- show quick description type exposed by module
|
||||
# if currenttype and not isempty(currenttype.fields) then
|
||||
<h$(i+1)><a id="$(anchor(currenttype))" >Type <code>$(currenttype.name)</code></a></h$(i+1)>
|
||||
$( applytemplate(currenttype, i+2, 'index') )
|
||||
# end
|
||||
# --
|
||||
# -- Show quick description of other types
|
||||
# --
|
||||
# if _file.types then
|
||||
# for name, type in sortedpairs( _file.types ) do
|
||||
# if type ~= currenttype and type.tag == 'recordtypedef' and not isempty(type.fields) then
|
||||
<h$(i+1)><a id="$(anchor(type))">Type <code>$(name)</code></a></h$(i+1)>
|
||||
$( applytemplate(type, i+2, 'index') )
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# --
|
||||
# -- Long description of globals
|
||||
# --
|
||||
# if not isempty(_file.globalvars) then
|
||||
<h$(i+1)>Global(s)</h$(i+1)>
|
||||
# for name, item in sortedpairs(_file.globalvars) do
|
||||
$( applytemplate(item, i+2) )
|
||||
# end
|
||||
# end
|
||||
# --
|
||||
# -- Long description of current type
|
||||
# --
|
||||
# if currenttype then
|
||||
<h$(i+1)><a id="$(anchor(currenttype))" >Type <code>$(currenttype.name)</code></a></h$(i+1)>
|
||||
$( applytemplate(currenttype, i+2) )
|
||||
# end
|
||||
# --
|
||||
# -- Long description of other types
|
||||
# --
|
||||
# if not isempty( _file.types ) then
|
||||
# for name, type in sortedpairs( _file.types ) do
|
||||
# if type ~= currenttype and type.tag == 'recordtypedef' then
|
||||
<h$(i+1)><a id="$(anchor(type))" >Type <code>$(name)</code></a></h$(i+1)>
|
||||
$( applytemplate(type, i+2) )
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
</div>
|
||||
]]
|
||||
@@ -0,0 +1,28 @@
|
||||
--------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2012-2014 Sierra Wireless.
|
||||
-- All rights reserved. This program and the accompanying materials
|
||||
-- are made available under the terms of the Eclipse Public License v1.0
|
||||
-- which accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Kevin KIN-FOO <kkinfoo@sierrawireless.com>
|
||||
-- - initial API and implementation and initial documentation
|
||||
--------------------------------------------------------------------------------
|
||||
return
|
||||
[[#
|
||||
#if _index.modules then
|
||||
<div id="content">
|
||||
<h2>Module$( #_index.modules > 1 and 's' )</h2>
|
||||
<table class="module_list">
|
||||
# for _, module in sortedpairs( _index.modules ) do
|
||||
# if module.tag ~= 'index' then
|
||||
<tr>
|
||||
<td class="name" nowrap="nowrap">$( fulllinkto(module) )</td>
|
||||
<td class="summary">$( module.description and format(module.shortdescription) )</td>
|
||||
</tr>
|
||||
# end
|
||||
# end
|
||||
</table>
|
||||
</div>
|
||||
#end ]]
|
||||
@@ -0,0 +1,23 @@
|
||||
--------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2012-2014 Sierra Wireless.
|
||||
-- All rights reserved. This program and the accompanying materials
|
||||
-- are made available under the terms of the Eclipse Public License v1.0
|
||||
-- which accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Kevin KIN-FOO <kkinfoo@sierrawireless.com>
|
||||
-- - initial API and implementation and initial documentation
|
||||
--------------------------------------------------------------------------------
|
||||
return [[#
|
||||
# if not isempty(_recordtypedef.fields) then
|
||||
<table class="function_list">
|
||||
# for _, item in sortedpairs( _recordtypedef.fields ) do
|
||||
<tr>
|
||||
<td class="name" nowrap="nowrap">$( fulllinkto(item) )</td>
|
||||
<td class="summary">$( format(item.shortdescription) )</td>
|
||||
</tr>
|
||||
# end
|
||||
</table>
|
||||
# end
|
||||
# ]]
|
||||
@@ -0,0 +1,167 @@
|
||||
--------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2012-2014 Sierra Wireless.
|
||||
-- All rights reserved. This program and the accompanying materials
|
||||
-- are made available under the terms of the Eclipse Public License v1.0
|
||||
-- which accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Kevin KIN-FOO <kkinfoo@sierrawireless.com>
|
||||
-- - initial API and implementation and initial documentation
|
||||
--------------------------------------------------------------------------------
|
||||
return
|
||||
[[<dl class="function">
|
||||
<dt>
|
||||
# --
|
||||
# -- Resolve item type definition
|
||||
# --
|
||||
# local typedef = _item:resolvetype()
|
||||
|
||||
# --
|
||||
# -- Show item type for internal type
|
||||
# --
|
||||
#if _item.type and (not typedef or typedef.tag ~= 'functiontypedef') then
|
||||
# --Show link only when available
|
||||
# local link = fulllinkto(_item.type)
|
||||
# if link then
|
||||
<em>$( link )</em>
|
||||
# else
|
||||
<em>$(prettyname(_item.type))</em>
|
||||
# end
|
||||
#end
|
||||
<a id="$(anchor(_item))" >
|
||||
<strong>$( prettyname(_item) )</strong>
|
||||
</a>
|
||||
</dt>
|
||||
<dd>
|
||||
# if _item.shortdescription then
|
||||
$( format(_item.shortdescription) )
|
||||
# end
|
||||
# if _item.description and #_item.description > 0 then
|
||||
$( format(_item.description) )
|
||||
# end
|
||||
#
|
||||
# --
|
||||
# -- For function definitions, describe parameters and return values
|
||||
# --
|
||||
#if typedef and typedef.tag == 'functiontypedef' then
|
||||
# --
|
||||
# -- Describe parameters
|
||||
# --
|
||||
# local fdef = typedef
|
||||
#
|
||||
# -- Adjust parameter count if first one is 'self'
|
||||
# local paramcount
|
||||
# if #fdef.params > 0 and isinvokable(_item) then
|
||||
# paramcount = #fdef.params - 1
|
||||
# else
|
||||
# paramcount = #fdef.params
|
||||
# end
|
||||
#
|
||||
# -- List parameters
|
||||
# if paramcount > 0 then
|
||||
<h$(i)>Parameter$( paramcount > 1 and 's' )</h$(i)>
|
||||
<ul>
|
||||
# for position, param in ipairs( fdef.params ) do
|
||||
# if not (position == 1 and isinvokable(_item)) then
|
||||
<li>
|
||||
# local paramline = "<code><em>"
|
||||
# if param.type then
|
||||
# local link = linkto( param.type )
|
||||
# local name = prettyname( param.type )
|
||||
# if link then
|
||||
# paramline = paramline .. '<a href=\"' .. link .. '\">' .. name .. "</a>"
|
||||
# else
|
||||
# paramline = paramline .. name
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# paramline = paramline .. " " .. param.name .. " "
|
||||
#
|
||||
# if param.optional then
|
||||
# paramline = paramline .. "optional" .. " "
|
||||
# end
|
||||
# if param.hidden then
|
||||
# paramline = paramline .. "hidden"
|
||||
# end
|
||||
#
|
||||
# paramline = paramline .. "</em></code>: "
|
||||
#
|
||||
# if param.description and #param.description > 0 then
|
||||
# paramline = paramline .. "\n" .. param.description
|
||||
# end
|
||||
#
|
||||
$( format (paramline))
|
||||
</li>
|
||||
# end
|
||||
# end
|
||||
</ul>
|
||||
# end
|
||||
#
|
||||
# --
|
||||
# -- Describe returns types
|
||||
# --
|
||||
# if fdef and #fdef.returns > 0 then
|
||||
<h$(i)>Return value$(#fdef.returns > 1 and 's')</h$(i)>
|
||||
# --
|
||||
# -- Format nice type list
|
||||
# --
|
||||
# local function niceparmlist( parlist )
|
||||
# local typelist = {}
|
||||
# for position, type in ipairs(parlist) do
|
||||
# local link = linkto( type )
|
||||
# local name = prettyname( type )
|
||||
# if link then
|
||||
# typelist[#typelist + 1] = '<a href="'..link..'">'..name..'</a>'
|
||||
# else
|
||||
# typelist[#typelist + 1] = name
|
||||
# end
|
||||
# -- Append end separator or separating comma
|
||||
# typelist[#typelist + 1] = position == #parlist and ':' or ', '
|
||||
# end
|
||||
# return table.concat( typelist )
|
||||
# end
|
||||
# --
|
||||
# -- Generate a list if they are several return clauses
|
||||
# --
|
||||
# if #fdef.returns > 1 then
|
||||
<ol>
|
||||
# for position, ret in ipairs(fdef.returns) do
|
||||
<li>
|
||||
# local returnline = "";
|
||||
#
|
||||
# local paramlist = niceparmlist(ret.types)
|
||||
# if #ret.types > 0 and #paramlist > 0 then
|
||||
# returnline = "<em>" .. paramlist .. "</em>"
|
||||
# end
|
||||
# returnline = returnline .. "\n" .. ret.description
|
||||
$( format (returnline))
|
||||
</li>
|
||||
# end
|
||||
</ol>
|
||||
# else
|
||||
# local paramlist = niceparmlist(fdef.returns[1].types)
|
||||
# local isreturn = fdef.returns and #fdef.returns > 0 and #paramlist > 0
|
||||
# local isdescription = fdef.returns and fdef.returns[1].description and #format(fdef.returns[1].description) > 0
|
||||
#
|
||||
# local returnline = "";
|
||||
# -- Show return type if provided
|
||||
# if isreturn then
|
||||
# returnline = "<em>"..paramlist.."</em>"
|
||||
# end
|
||||
# if isdescription then
|
||||
# returnline = returnline .. "\n" .. fdef.returns[1].description
|
||||
# end
|
||||
$( format(returnline))
|
||||
# end
|
||||
# end
|
||||
#end
|
||||
#
|
||||
#--
|
||||
#-- Show usage samples
|
||||
#--
|
||||
#if _item.metadata and _item.metadata.usage then
|
||||
$( applytemplate(_item.metadata.usage, i) )
|
||||
#end
|
||||
</dd>
|
||||
</dl>]]
|
||||
@@ -0,0 +1,68 @@
|
||||
--------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2012-2014 Sierra Wireless.
|
||||
-- All rights reserved. This program and the accompanying materials
|
||||
-- are made available under the terms of the Eclipse Public License v1.0
|
||||
-- which accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Kevin KIN-FOO <kkinfoo@sierrawireless.com>
|
||||
-- - initial API and implementation and initial documentation
|
||||
--------------------------------------------------------------------------------
|
||||
return
|
||||
[[<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html>
|
||||
#if _page.headers and #_page.headers > 0 then
|
||||
<head>
|
||||
# for _, header in ipairs(_page.headers) do
|
||||
$(header)
|
||||
# end
|
||||
</head>
|
||||
#end
|
||||
<body>
|
||||
<div id="container">
|
||||
<div id="product">
|
||||
<div id="product_logo"></div>
|
||||
<div id="product_name"><big><b></b></big></div>
|
||||
<div id="product_description"></div>
|
||||
</div>
|
||||
<div id="main">
|
||||
# --
|
||||
# -- Generating lateral menu
|
||||
# --
|
||||
<div id="navigation">
|
||||
# local index = 'index'
|
||||
# if _page.modules then
|
||||
<h2>Modules</h2>
|
||||
# -- Check if an index is defined
|
||||
# if _page.modules [ index ] then
|
||||
# local module = _page.modules [ index ]
|
||||
<ul><li>
|
||||
# if module ~= _page.currentmodule then
|
||||
<a href="$( linkto(module) )">$(module.name)</a>
|
||||
# else
|
||||
$(module.name)
|
||||
# end
|
||||
</li></ul>
|
||||
# end
|
||||
#
|
||||
<ul>
|
||||
# -- Generating links for all modules
|
||||
# for _, module in sortedpairs( _page.modules ) do
|
||||
# -- Except for current one
|
||||
# if module.name ~= index then
|
||||
# if module ~= _page.currentmodule then
|
||||
<li><a href="$( linkto(module) )">$(module.name)</a></li>
|
||||
# else
|
||||
<li>$(module.name)</li>
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
</ul>
|
||||
# end
|
||||
</div>
|
||||
$( applytemplate(_page.currentmodule) )
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
]]
|
||||
@@ -0,0 +1,36 @@
|
||||
--------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2012-2014 Sierra Wireless.
|
||||
-- All rights reserved. This program and the accompanying materials
|
||||
-- are made available under the terms of the Eclipse Public License v1.0
|
||||
-- which accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Kevin KIN-FOO <kkinfoo@sierrawireless.com>
|
||||
-- - initial API and implementation and initial documentation
|
||||
--------------------------------------------------------------------------------
|
||||
return [[#
|
||||
# --
|
||||
# -- Descriptions
|
||||
# --
|
||||
#if _recordtypedef.shortdescription and #_recordtypedef.shortdescription > 0 then
|
||||
$( format( _recordtypedef.shortdescription ) )
|
||||
#end
|
||||
#if _recordtypedef.description and #_recordtypedef.description > 0 then
|
||||
$( format( _recordtypedef.description ) )
|
||||
#end
|
||||
#--
|
||||
#-- Describe usage
|
||||
#--
|
||||
#if _recordtypedef.metadata and _recordtypedef.metadata.usage then
|
||||
$( applytemplate(_recordtypedef.metadata.usage, i) )
|
||||
#end
|
||||
# --
|
||||
# -- Describe type fields
|
||||
# --
|
||||
#if not isempty( _recordtypedef.fields ) then
|
||||
<h$(i)>Field(s)</h$(i)>
|
||||
# for name, item in sortedpairs( _recordtypedef.fields ) do
|
||||
$( applytemplate(item, i) )
|
||||
# end
|
||||
#end ]]
|
||||
@@ -0,0 +1,33 @@
|
||||
--------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2012-2014 Sierra Wireless.
|
||||
-- All rights reserved. This program and the accompanying materials
|
||||
-- are made available under the terms of the Eclipse Public License v1.0
|
||||
-- which accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Marc AUBRY <maubry@sierrawireless.com>
|
||||
-- - initial API and implementation
|
||||
--------------------------------------------------------------------------------
|
||||
return[[#
|
||||
#--
|
||||
#-- Show usage samples
|
||||
#--
|
||||
#if _usage then
|
||||
# if #_usage > 1 then
|
||||
# -- Show all usages
|
||||
<h$(i)>Usages:</h$(i)>
|
||||
<ul>
|
||||
# -- Loop over several usage description
|
||||
# for _, usage in ipairs(_usage) do
|
||||
<li><pre class="example"><code>$( securechevrons(usage.description) )</code></pre></li>
|
||||
# end
|
||||
</ul>
|
||||
# elseif #_usage == 1 then
|
||||
# -- Show unique usage sample
|
||||
<h$(i)>Usage:</h$(i)>
|
||||
# local usage = _usage[1]
|
||||
<pre class="example"><code>$( securechevrons(usage.description) )</code></pre>
|
||||
# end
|
||||
#end
|
||||
#]]
|
||||
@@ -0,0 +1,470 @@
|
||||
--------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2012-2014 Sierra Wireless.
|
||||
-- All rights reserved. This program and the accompanying materials
|
||||
-- are made available under the terms of the Eclipse Public License v1.0
|
||||
-- which accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Kevin KIN-FOO <kkinfoo@sierrawireless.com>
|
||||
-- - initial API and implementation and initial documentation
|
||||
--------------------------------------------------------------------------------
|
||||
local apimodel = require 'models.apimodel'
|
||||
|
||||
---
|
||||
-- @module docutils
|
||||
-- Handles link generation, node quick description.
|
||||
--
|
||||
-- Provides:
|
||||
-- * link generation
|
||||
-- * anchor generation
|
||||
-- * node quick description
|
||||
local M = {}
|
||||
|
||||
function M.isempty(map)
|
||||
local f = pairs(map)
|
||||
return f(map) == nil
|
||||
end
|
||||
|
||||
---
|
||||
-- Provide a handling function for all supported anchor types
|
||||
-- recordtypedef => #(typename)
|
||||
-- item (field of recordtypedef) => #(typename).itemname
|
||||
-- item (global) => itemname
|
||||
M.anchortypes = {
|
||||
recordtypedef = function (o) return string.format('#(%s)', o.name) end,
|
||||
item = function(o)
|
||||
if not o.parent or o.parent.tag == 'file' then
|
||||
-- Handle items referencing globals
|
||||
return o.name
|
||||
elseif o.parent and o.parent.tag == 'recordtypedef' then
|
||||
-- Handle items included in recordtypedef
|
||||
local recordtypedef = o.parent
|
||||
local recordtypedefanchor = M.anchor(recordtypedef)
|
||||
if not recordtypedefanchor then
|
||||
return nil, 'Unable to generate anchor for `recordtypedef parent.'
|
||||
end
|
||||
return string.format('%s.%s', recordtypedefanchor, o.name)
|
||||
end
|
||||
return nil, 'Unable to generate anchor for `item'
|
||||
end
|
||||
}
|
||||
|
||||
---
|
||||
-- Provides anchor string for an object of API mode
|
||||
--
|
||||
-- @function [parent = #docutils] anchor
|
||||
-- @param modelobject Object form API model
|
||||
-- @result #string Anchor for an API model object, this function __may rise an error__
|
||||
-- @usage # -- In a template
|
||||
-- # local anchorname = anchor(someobject)
|
||||
-- <a id="$(anchorname)" />
|
||||
function M.anchor( modelobject )
|
||||
local tag = modelobject.tag
|
||||
if M.anchortypes[ tag ] then
|
||||
return M.anchortypes[ tag ](modelobject)
|
||||
end
|
||||
return nil, string.format('No anchor available for `%s', tag)
|
||||
end
|
||||
|
||||
local function getexternalmodule( item )
|
||||
-- Get file which contains this item
|
||||
local file
|
||||
if item.parent then
|
||||
if item.parent.tag =='recordtypedef' then
|
||||
local recordtypedefparent = item.parent.parent
|
||||
if recordtypedefparent and recordtypedefparent.tag =='file'then
|
||||
file = recordtypedefparent
|
||||
end
|
||||
elseif item.parent.tag =='file' then
|
||||
file = item.parent
|
||||
else
|
||||
return nil, 'Unable to fetch item parent'
|
||||
end
|
||||
end
|
||||
return file
|
||||
end
|
||||
|
||||
---
|
||||
-- Provide a handling function for all supported link types
|
||||
--
|
||||
-- internaltyperef => ##(typename)
|
||||
-- => #anchor(recordtyperef)
|
||||
-- externaltyperef => modulename.html##(typename)
|
||||
-- => linkto(file)#anchor(recordtyperef)
|
||||
-- file(module) => modulename.html
|
||||
-- index => index.html
|
||||
-- recordtypedef => ##(typename)
|
||||
-- => #anchor(recordtyperef)
|
||||
-- item (internal field of recordtypedef) => ##(typename).itemname
|
||||
-- => #anchor(item)
|
||||
-- item (internal global) => #itemname
|
||||
-- => #anchor(item)
|
||||
-- item (external field of recordtypedef) => modulename.html##(typename).itemname
|
||||
-- => linkto(file)#anchor(item)
|
||||
-- item (externalglobal) => modulename.html#itemname
|
||||
-- => linkto(file)#anchor(item)
|
||||
M.linktypes = {
|
||||
internaltyperef = function(o) return string.format('##(%s)', o.typename) end,
|
||||
externaltyperef = function(o) return string.format('%s.html##(%s)', o.modulename, o.typename) end,
|
||||
file = function(o) return string.format('%s.html', o.name) end,
|
||||
index = function() return 'index.html' end,
|
||||
recordtypedef = function(o)
|
||||
local anchor = M.anchor(o)
|
||||
if not anchor then
|
||||
return nil, 'Unable to generate anchor for `recordtypedef.'
|
||||
end
|
||||
return string.format('#%s', anchor)
|
||||
end,
|
||||
item = function(o)
|
||||
|
||||
-- For every item get anchor
|
||||
local anchor = M.anchor(o)
|
||||
if not anchor then
|
||||
return nil, 'Unable to generate anchor for `item.'
|
||||
end
|
||||
|
||||
-- Built local link to item
|
||||
local linktoitem = string.format('#%s', anchor)
|
||||
|
||||
--
|
||||
-- For external item, prefix with the link to the module.
|
||||
--
|
||||
-- The "external item" concept is used only here for short/embedded
|
||||
-- notation purposed. This concept and the `.external` field SHALL NOT
|
||||
-- be used elsewhere.
|
||||
--
|
||||
if o.external then
|
||||
|
||||
-- Get link to file which contains this item
|
||||
local file = getexternalmodule( o )
|
||||
local linktofile = file and M.linkto( file )
|
||||
if not linktofile then
|
||||
return nil, 'Unable to generate link for external `item.'
|
||||
end
|
||||
|
||||
-- Built external link to item
|
||||
linktoitem = string.format("%s%s", linktofile, linktoitem)
|
||||
end
|
||||
|
||||
return linktoitem
|
||||
end
|
||||
}
|
||||
|
||||
---
|
||||
-- Generates text for HTML links from API model element
|
||||
--
|
||||
-- @function [parent = #docutils]
|
||||
-- @param modelobject Object form API model
|
||||
-- @result #string Links text for an API model element, this function __may rise an error__.
|
||||
-- @usage # -- In a template
|
||||
-- <a href="$( linkto(api) )">Some text</a>
|
||||
function M.linkto( apiobject )
|
||||
local tag = apiobject.tag
|
||||
if M.linktypes[ tag ] then
|
||||
return M.linktypes[tag](apiobject)
|
||||
end
|
||||
if not tag then
|
||||
return nil, 'Link generation is impossible as no tag has been provided.'
|
||||
end
|
||||
return nil, string.format('No link generation available for `%s.', tag)
|
||||
end
|
||||
|
||||
---
|
||||
-- Provide a handling function for all supported pretty name types
|
||||
-- primitivetyperef => #typename
|
||||
-- internaltyperef => #typename
|
||||
-- externaltyperef => modulename#typename
|
||||
-- file(module) => modulename
|
||||
-- index => index
|
||||
-- recordtypedef => typename
|
||||
-- item (internal function of recordtypedef) => typename.itemname(param1, param2,...)
|
||||
-- item (internal func with self of recordtypedef) => typename:itemname(param2)
|
||||
-- item (internal non func field of recordtypedef) => typename.itemname
|
||||
-- item (internal func global) => functionname(param1, param2,...)
|
||||
-- item (internal non func global) => itemname
|
||||
-- item (external function of recordtypedef) => modulename#typename.itemname(param1, param2,...)
|
||||
-- item (external func with self of recordtypedef) => modulename#typename:itemname(param2)
|
||||
-- item (external non func field of recordtypedef) => modulename#typename.itemname
|
||||
-- item (external func global) => functionname(param1, param2,...)
|
||||
-- item (external non func global) => itemname
|
||||
M.prettynametypes = {
|
||||
primitivetyperef = function(o) return string.format('#%s', o.typename) end,
|
||||
externaltyperef = function(o) return string.format('%s#%s', o.modulename, o.typename) end,
|
||||
index = function(o) return "index" end,
|
||||
file = function(o) return o.name end,
|
||||
recordtypedef = function(o) return o.name end,
|
||||
item = function( o )
|
||||
|
||||
-- Determine item name
|
||||
-- ----------------------
|
||||
local itemname = o.name
|
||||
|
||||
-- Determine scope
|
||||
-- ----------------------
|
||||
local parent = o.parent
|
||||
local isglobal = parent and parent.tag == 'file'
|
||||
local isfield = parent and parent.tag == 'recordtypedef'
|
||||
|
||||
-- Determine type name
|
||||
-- ----------------------
|
||||
|
||||
local typename = isfield and parent.name
|
||||
|
||||
-- Fetch item definition
|
||||
-- ----------------------
|
||||
-- Get file object
|
||||
local file
|
||||
if isglobal then
|
||||
file = parent
|
||||
elseif isfield then
|
||||
file = parent.parent
|
||||
end
|
||||
-- Get definition
|
||||
local definition = o:resolvetype (file)
|
||||
|
||||
|
||||
|
||||
-- Build prettyname
|
||||
-- ----------------------
|
||||
local prettyname
|
||||
if not definition or definition.tag ~= 'functiontypedef' then
|
||||
-- Fields
|
||||
if isglobal or not typename then
|
||||
prettyname = itemname
|
||||
else
|
||||
prettyname = string.format('%s.%s', typename, itemname)
|
||||
end
|
||||
else
|
||||
-- Functions
|
||||
-- Build parameter list
|
||||
local paramlist = {}
|
||||
local isinvokable = M.isinvokable(o)
|
||||
for position, param in ipairs(definition.params) do
|
||||
-- For non global function, when first parameter is 'self',
|
||||
-- it will not be part of listed parameters
|
||||
if not (position == 1 and isinvokable and isfield) then
|
||||
table.insert(paramlist, param.name)
|
||||
if position ~= #definition.params then
|
||||
table.insert(paramlist, ', ')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if isglobal or not typename then
|
||||
prettyname = string.format('%s(%s)',itemname, table.concat(paramlist))
|
||||
else
|
||||
-- Determine function prefix operator,
|
||||
-- ':' if 'self' is first parameter, '.' else way
|
||||
local operator = isinvokable and ':' or '.'
|
||||
|
||||
-- Append function parameters
|
||||
prettyname = string.format('%s%s%s(%s)',typename, operator, itemname, table.concat(paramlist))
|
||||
end
|
||||
end
|
||||
|
||||
-- Manage external Item prettyname
|
||||
-- ----------------------
|
||||
local externalmodule = o.external and getexternalmodule( o )
|
||||
local externalmodulename = externalmodule and externalmodule.name
|
||||
|
||||
if externalmodulename then
|
||||
return string.format('%s#%s',externalmodulename,prettyname)
|
||||
else
|
||||
return prettyname
|
||||
end
|
||||
end
|
||||
}
|
||||
M.prettynametypes.internaltyperef = M.prettynametypes.primitivetyperef
|
||||
|
||||
---
|
||||
-- Check if the given item is a function that can be invoked
|
||||
function M.isinvokable(item)
|
||||
--test if the item is global
|
||||
if item.parent and item.parent.tag == 'file' then
|
||||
return false
|
||||
end
|
||||
-- check first param
|
||||
local definition = item:resolvetype()
|
||||
if definition and definition.tag == 'functiontypedef' then
|
||||
if (#definition.params > 0) then
|
||||
return definition.params[1].name == 'self'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---
|
||||
-- Provide human readable overview from an API model element
|
||||
--
|
||||
-- Resolve all element needed to summurize nicely an element form API model.
|
||||
-- @usage $ print( prettyname(item) )
|
||||
-- module:somefunction(secondparameter)
|
||||
-- @function [parent = #docutils]
|
||||
-- @param apiobject Object form API model
|
||||
-- @result #string Human readable description of given element.
|
||||
-- @result #nil, #string In case of error.
|
||||
function M.prettyname( apiobject )
|
||||
local tag = apiobject.tag
|
||||
if M.prettynametypes[tag] then
|
||||
return M.prettynametypes[tag](apiobject)
|
||||
elseif not tag then
|
||||
return nil, 'No pretty name available as no tag has been provided.'
|
||||
end
|
||||
return nil, string.format('No pretty name for `%s.', tag)
|
||||
end
|
||||
|
||||
---
|
||||
-- Just make a string print table in HTML.
|
||||
-- @function [parent = #docutils] securechevrons
|
||||
-- @param #string String to convert.
|
||||
-- @usage securechevrons('<markup>') => '<markup>'
|
||||
-- @return #string Converted string.
|
||||
function M.securechevrons( str )
|
||||
if not str then return nil, 'String expected.' end
|
||||
return string.gsub(str:gsub('<', '<'), '>', '>')
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Handling format of @{some#type} tag.
|
||||
-- Following functions enable to recognize several type of references between
|
||||
-- "{}".
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
---
|
||||
-- Provide API Model elements from string describing global elements
|
||||
-- such as:
|
||||
-- * `global#foo`
|
||||
-- * `foo#global.bar`
|
||||
local globals = function(str)
|
||||
-- Handling globals from modules
|
||||
for modulename, fieldname in str:gmatch('([%a%.%d_]+)#global%.([%a%.%d_]+)') do
|
||||
local item = apimodel._item(fieldname)
|
||||
local file = apimodel._file()
|
||||
file.name = modulename
|
||||
file:addglobalvar( item )
|
||||
return item
|
||||
end
|
||||
-- Handling other globals
|
||||
for name in str:gmatch('global#([%a%.%d_]+)') do
|
||||
-- print("globale", name)
|
||||
return apimodel._externaltypref('global', name)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---
|
||||
-- Transform a string like `module#(type).field` in an API Model item
|
||||
local field = function( str )
|
||||
|
||||
-- Match `module#type.field`
|
||||
local mod, typename, fieldname = str:gmatch('([%a%.%d_]*)#([%a%.%d_]+)%.([%a%.%d_]+)')()
|
||||
|
||||
-- Try matching `module#(type).field`
|
||||
if not mod then
|
||||
mod, typename, fieldname = str:gmatch('([%a%.%d_]*)#%(([%a%.%d_]+)%)%.([%a%.%d_]+)')()
|
||||
if not mod then
|
||||
-- No match
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Build according `item
|
||||
local modulefielditem = apimodel._item( fieldname )
|
||||
local moduletype = apimodel._recordtypedef(typename)
|
||||
moduletype:addfield( modulefielditem )
|
||||
local typeref
|
||||
if #mod > 0 then
|
||||
local modulefile = apimodel._file()
|
||||
modulefile.name = mod
|
||||
modulefile:addtype( moduletype )
|
||||
typeref = apimodel._externaltypref(mod, typename)
|
||||
modulefielditem.external = true
|
||||
else
|
||||
typeref = apimodel._internaltyperef(typename)
|
||||
end
|
||||
modulefielditem.type = typeref
|
||||
return modulefielditem
|
||||
end
|
||||
|
||||
---
|
||||
-- Build an API internal reference from a string like: `#typeref`
|
||||
local internal = function ( typestring )
|
||||
for name in typestring:gmatch('#([%a%.%d_]+)') do
|
||||
-- Do not handle this name is it starts with reserved name "global"
|
||||
if name:find("global.") == 1 then return nil end
|
||||
return apimodel._internaltyperef(name)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---
|
||||
-- Build an API external reference from a string like: `mod.ule#type`
|
||||
local extern = function (type)
|
||||
|
||||
-- Match `mod.ule#ty.pe`
|
||||
local modulename, typename = type:gmatch('([%a%.%d_]+)#([%a%.%d_]+)')()
|
||||
|
||||
-- Trying `mod.ule#(ty.pe)`
|
||||
if not modulename then
|
||||
modulename, typename = type:gmatch('([%a%.%d_]+)#%(([%a%.%d_]+)%)')()
|
||||
|
||||
-- No match at all
|
||||
if not modulename then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
return apimodel._externaltypref(modulename, typename)
|
||||
end
|
||||
|
||||
---
|
||||
-- Build an API external reference from a string like: `mod.ule`
|
||||
local file = function (type)
|
||||
for modulename in type:gmatch('([%a%.%d_]+)') do
|
||||
local file = apimodel._file()
|
||||
file.name = modulename
|
||||
return file
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
---
|
||||
-- Provide API Model element from a string
|
||||
-- @usage local externaltyperef = getelement("somemodule#somefield")
|
||||
function M.getelement( str )
|
||||
|
||||
-- Order matters, more restrictive are at begin of table
|
||||
local extractors = {
|
||||
globals,
|
||||
field,
|
||||
extern,
|
||||
internal,
|
||||
file
|
||||
}
|
||||
-- Loop over extractors.
|
||||
-- First valid result is used
|
||||
for _, extractor in ipairs( extractors ) do
|
||||
local result = extractor( str )
|
||||
if result then return result end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Iterator that iterates on the table in key ascending order.
|
||||
--
|
||||
-- @function [parent=#utils.table] sortedPairs
|
||||
-- @param t table to iterate.
|
||||
-- @return iterator function.
|
||||
function M.sortedpairs(t)
|
||||
local a = {}
|
||||
local insert = table.insert
|
||||
for n in pairs(t) do insert(a, n) end
|
||||
table.sort(a)
|
||||
local i = 0
|
||||
return function()
|
||||
i = i + 1
|
||||
return a[i], t[a[i]]
|
||||
end
|
||||
end
|
||||
return M
|
||||
@@ -0,0 +1,116 @@
|
||||
--------------------------------------------------------------------------------
|
||||
-- Copyright (c) 2011-2012 Sierra Wireless.
|
||||
-- All rights reserved. This program and the accompanying materials
|
||||
-- are made available under the terms of the Eclipse Public License v1.0
|
||||
-- which accompanies this distribution, and is available at
|
||||
-- http://www.eclipse.org/legal/epl-v10.html
|
||||
--
|
||||
-- Contributors:
|
||||
-- Kevin KIN-FOO <kkinfoo@sierrawireless.com>
|
||||
-- - initial API and implementation and initial documentation
|
||||
--------------------------------------------------------------------------------
|
||||
---
|
||||
-- This library provide html description of elements from the externalapi
|
||||
local M = {}
|
||||
|
||||
-- Load template engine
|
||||
local pltemplate = require 'pl.template'
|
||||
|
||||
-- Markdown handling
|
||||
local markdown = require 'markdown'
|
||||
|
||||
-- apply template to the given element
|
||||
function M.applytemplate(elem, ident, templatetype)
|
||||
-- define environment
|
||||
local env = M.getenv(elem, ident)
|
||||
|
||||
-- load template
|
||||
local template = M.gettemplate(elem,templatetype)
|
||||
if not template then
|
||||
templatetype = templatetype and string.format(' "%s"', templatetype) or ''
|
||||
local elementname = string.format(' for %s', elem.tag or 'untagged element')
|
||||
error(string.format('Unable to load %s template %s', templatetype, elementname))
|
||||
end
|
||||
|
||||
-- apply template
|
||||
local str, err = pltemplate.substitute(template, env)
|
||||
|
||||
--manage errors
|
||||
if not str then
|
||||
local templateerror = templatetype and string.format(' parsing "%s" template ', templatetype) or ''
|
||||
error(string.format('An error occured%s for "%s"\n%s',templateerror, elem.tag, err))
|
||||
end
|
||||
return str
|
||||
end
|
||||
|
||||
-- get the a new environment for this element
|
||||
function M.getenv(elem, ident)
|
||||
local currentenv ={}
|
||||
for k,v in pairs(M.env) do currentenv[k] = v end
|
||||
if elem and elem.tag then
|
||||
currentenv['_'..elem.tag]= elem
|
||||
end
|
||||
currentenv['i']= ident or 1
|
||||
return currentenv
|
||||
end
|
||||
|
||||
-- get the template for this element
|
||||
function M.gettemplate(elem,templatetype)
|
||||
local tag = elem and elem.tag
|
||||
if tag then
|
||||
if templatetype then
|
||||
return require ("template." .. templatetype.. "." .. tag)
|
||||
else
|
||||
return require ("template." .. tag)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
---
|
||||
-- Allow user to format text in descriptions.
|
||||
-- Default implementation replaces @{---} tags with links and apply markdown.
|
||||
-- @return #string
|
||||
local function format(string)
|
||||
-- Allow to replace encountered tags with valid links
|
||||
local replace = function(found)
|
||||
local apiobj = M.env.getelement(found)
|
||||
if apiobj then
|
||||
return M.env.fulllinkto(apiobj)
|
||||
end
|
||||
return found
|
||||
end
|
||||
string = string:gsub('@{%s*(.-)%s*}', replace)
|
||||
return M.env.markdown( string )
|
||||
end
|
||||
---
|
||||
-- Provide a full link to an element using `prettyname` and `linkto`.
|
||||
-- Default implementation is for HTML.
|
||||
local function fulllinkto(o)
|
||||
local ref = M.env.linkto(o)
|
||||
local name = M.env.prettyname(o)
|
||||
if not ref then
|
||||
return name
|
||||
end
|
||||
return string.format('<a href="%s">%s</a>', ref, name)
|
||||
end
|
||||
--
|
||||
-- Define default template environnement
|
||||
--
|
||||
local defaultenv = {
|
||||
table = table,
|
||||
ipairs = ipairs,
|
||||
pairs = pairs,
|
||||
markdown = markdown,
|
||||
applytemplate = M.applytemplate,
|
||||
format = format,
|
||||
linkto = function(str) return str end,
|
||||
fulllinkto = fulllinkto,
|
||||
prettyname = function(s) return s end,
|
||||
getelement = function(s) return nil end
|
||||
}
|
||||
|
||||
-- this is the global env accessible in the templates
|
||||
-- env should be redefine by docgenerator user to add functions or redefine it.
|
||||
M.env = defaultenv
|
||||
return M
|
||||
Reference in New Issue
Block a user