Module:ar-verb

Definition from Wiktionary, the free dictionary
Jump to: navigation, search

This module provides helper functions for Arabic verb inflection tables and Arabic verb headword lines.

The arguments are meant to be passed in through the parent frame, i.e. arguments specified to the template that invokes this module, rather than to the invocation itself.

The external entry point for implementing {{ar-conj}} is show, meant for calling from templates, and there is a debugging entry point show2 meant for calling from the debug console, which takes arguments for the current and parent frame and constructs mock-up frames. The template (parent frame) arguments are documented thoroughly in {{ar-conj}}.

Similarly, the external entry point for implementing {{ar-verb}} is headword, and the external entry point for implementing {{ar-past3sm}} is past3sm. Debugging entry points headword2 and past3sm2 exist in both cases.

All of the templates are similar in that they take arguments specifying the form, past and non-past vowels (for form I), and optional radicals. If radicals are missing, they are inferred from the headword. The templates differ slightly in the way the form is specified: {{ar-verb}} takes a form= argument (for compatibility with a previous implementation), whereas the others take the form as the first unnamed argument.


--[[

Author: User:Benwing, from early version by User:Atitarev, User:ZxxZxxZ

Todo:

1. Finish unimplemented conjugation types. Only IX-final-weak left (extremely
   rare, possibly only one verb اِعْمَايَ (according to Haywood and Nahmad p. 244,
   who are very specific about the irregular occurrence of alif + yā instead
   of expected اِعْمَيَّ with doubled yā). Not in Hans Wehr.
2. Implement irregular verbs as special cases and recognize them, e.g.
   -- laysa "to not be"; only exists in the past tense, no non-past, no
	  imperative, no participles, no passive, no verbal noun. Irregular
	  alternation las-/lays-.
   -- istaḥā yastaḥī "be ashamed of" -- this is complex according to Hans Wehr
	  because there are two verbs, regular istaḥyā yastaḥyī "to spare
	  (someone)'s life" and irregular istaḥyā yastaḥyī "to be ashamed to face
	  (someone)", which is irregular because it has the alternate irregular
	  form istaḥā yastaḥī which only applies to this meaning. Currently we
	  follow Haywood and Nahmad in saying that both varieties can be spelled
	  istaḥyā/istaḥā/istaḥḥā, but we should instead use a variant= param
	  similar to حَيَّ to distinguish the two possibilities, and maybe not
	  include istaḥḥā.
   -- ʿayya/ʿayiya yaʿayyu/yaʿyā "to not find the right way, be incapable of,
	  stammer, falter, fall ill". This appears to be a mixture of a geminate
	  and final-weak verb. Unclear what the whole paradigm looks like. Do
	  the consonant-ending parts in the past follow the final-weak paradigm?
	  Is it the same in the non-past? Or can you conjugate the non-past
	  fully as either geminate or final-weak?
   -- اِنْمَحَى inmaḥā or يمَّحَى immaḥā "to be effaced, obliterated; to disappear, vanish"
	  has irregular assimilation of inm- to imm- as an alternative. inmalasa
	  "to become smooth; to glide; to slip away; to escape" also has immalasa
	  as an alternative. The only other form VII verbs in Hans Wehr beginning
	  with -m- are inmalaḵa "to be pulled out, torn out, wrenched" and inmāʿa
	  "to be melted, to melt, to dissolve", which are not listed with imm-
	  alternatives, but might have them; if so, we should handle this generally.
3. Implement individual override parameters for each pardigm part. See
   Module:fro-verb for an example of how to do this generally. Note that
   {{temp|ar-conj-I}} and other of the older templates already had such
   individual override params.

Irregular verbs already implemented:

   -- [ḥayya/ḥayiya yaḥyā "live" -- behaves like a normal final-weak verb
	  (e.g. past first singular ḥayītu) except in the past-tense parts with
	  vowel-initial endings (all the third person except for the third feminine
	  plural). The normal singular and dual endings have -yiya- in them, which
	  compresses to -yya-, with the normal endings the less preferred ones.
	  In masculine third plural, expected ḥayū is replaced by ḥayyū by
	  analogy to the -yy- parts, and the regular form is not given as an
	  alternant in John Mace. Barron's 201 verbs appears to have the regular
	  ḥayū as the part, however. Note also that final -yā appears with tall
	  alif. This appears to be a spelling convention of Arabic, also applying
	  in ḥayyā (form II, "to keep (someone) alive") and 'aḥyā (form IV,
	  "to animate, revive, give birth to, give new life to").] -- implemented
   -- [ittaxadha yattaxidhu "take"] -- implemented
   -- [sa'ala yas'alu "ask" with alternative jussive/imperative yasal/sal] -- implemented
   -- [ra'ā yarā "see"] -- implemented
   -- ['arā yurī "show"] -- implemented
   -- ['akala ya'kulu "eat" with imperative kul] -- implemented
   -- ['axadha ya'xudhu "take" with imperative xudh] -- implemented
   -- ['amara ya'muru "order" with imperative mur] -- implemented

--]]

local m_headword = require("Module:headword")
local m_utilities = require("Module:utilities")
local m_links = require("Module:links")
local ar_translit = require("Module:ar-translit")
local ar_utilities = require("Module:ar-utilities")
local ar_nominals = require("Module:ar-nominals")

local lang = require("Module:languages").getByCode("ar")
local curtitle = mw.title.getCurrentTitle().fullText
local yesno = require("Module:yesno")

local rfind = mw.ustring.find
local rsubn = mw.ustring.gsub
local rmatch = mw.ustring.match
local rsplit = mw.text.split
local usub = mw.ustring.sub
local ulen = mw.ustring.len
local u = mw.ustring.char

-- don't display i3rab in nominal forms (verbal nouns, participles)
local no_nominal_i3rab = true

local export = {}

-- Within this module, conjugations are the functions that do the actual
-- conjugating by creating the parts of a basic verb.
-- They are defined further down.
local conjugations = {}
-- hamza variants
local HAMZA            = u(0x0621) -- hamza on the line (stand-alone hamza) = ء
local HAMZA_ON_ALIF    = u(0x0623)
local HAMZA_ON_W       = u(0x0624)
local HAMZA_UNDER_ALIF = u(0x0625)
local HAMZA_ON_Y       = u(0x0626)
local HAMZA_ANY        = "[" .. HAMZA .. HAMZA_ON_ALIF .. HAMZA_UNDER_ALIF .. HAMZA_ON_W .. HAMZA_ON_Y .. "]"
local HAMZA_PH         = u(0xFFF0) -- hamza placeholder

-- diacritics
local A  = u(0x064E) -- fatḥa
local AN = u(0x064B) -- fatḥatān (fatḥa tanwīn)
local U  = u(0x064F) -- ḍamma
local UN = u(0x064C) -- ḍammatān (ḍamma tanwīn)
local I  = u(0x0650) -- kasra
local IN = u(0x064D) -- kasratān (kasra tanwīn)
local SK = u(0x0652) -- sukūn = no vowel
local SH = u(0x0651) -- šadda = gemination of consonants
local DAGGER_ALIF = u(0x0670)
local DIACRITIC_ANY_BUT_SH = "[" .. A .. I .. U .. AN .. IN .. UN .. SK .. DAGGER_ALIF .. "]"
-- Pattern matching short vowels
local AIU = "[" .. A .. I .. U .. "]"
-- Pattern matching short vowels or sukūn
local AIUSK = "[" .. A .. I .. U .. SK .. "]"
-- Pattern matching any diacritics that may be on a consonant
local DIACRITIC = SH .. "?" .. DIACRITIC_ANY_BUT_SH
-- Suppressed UN; we don't show -un i3rab any more, but this can be changed to show it
local UNS = no_nominal_i3rab and "" or UN

local dia = {a = A, i = I, u = U}

-- various letters and signs
local ALIF   = u(0x0627) -- ʾalif = ا
local AMAQ   = u(0x0649) -- ʾalif maqṣūra = ى
local AMAD   = u(0x0622) -- ʾalif madda = آ
local TAM    = u(0x0629) -- tāʾ marbūṭa = ة
local T      = u(0x062A) -- tāʾ = ت
local HYPHEN = u(0x0640)
local N      = u(0x0646) -- nūn = ن
local W      = u(0x0648) -- wāw = و
local Y      = u(0x064A) -- yā = ي
local S      = "س"
local M      = "م"
local LRM    = u(0x200e) -- left-to-right mark

-- common combinations
local AH    = A .. TAM
local AT    = A .. T
local AA    = A .. ALIF
local AAMAQ = A .. AMAQ
local AAH   = AA .. TAM
local AAT   = AA .. T
local II    = I .. Y
local IY    = II
local UU    = U .. W
local AY    = A .. Y
local AW    = A .. W
local AYSK  = AY .. SK
local AWSK  = AW .. SK
local NA    = N .. A
local NI    = N .. I
local AAN   = AA .. N
local AANI  = AA .. NI
local AYNI  = AYSK .. NI
local AWNA  = AWSK .. NA
local AYNA  = AYSK .. NA
local AYAAT = AY .. AAT
local UNU   = "[" .. UN .. U .. "]"
local MA    = M .. A
local MU    = M .. U

--------------------
-- Utility functions
--------------------

-- "if not empty" -- convert empty strings to nil; also strip quotes around
-- strings, to allow embedded spaces to be included
local function ine(x)
	if x == nil then
		return nil
	elseif rfind(x, '^".*"$') then
		local ret = rmatch(x, '^"(.*)"$')
		return ret
	elseif rfind(x, "^'.*'$") then
		local ret = rmatch(x, "^'(.*)'$")
		return ret
	elseif x == "" then
		return nil
	else
		return x
	end
end

-- true if array contains item
local function contains(tab, item)
	for _, value in pairs(tab) do
		if value == item then
			return true
		end
	end
	return false
end

-- append array to array
local function append_array(tab, items)
	for _, item in ipairs(items) do
		table.insert(tab, item)
	end
end

-- append to array if element not already present
local function insert_if_not(tab, item)
	if not contains(tab, item) then
		table.insert(tab, item)
	end
end

-- version of rsubn() that discards all but the first return value
function rsub(term, foo, bar)
	local retval = rsubn(term, foo, bar)
	return retval
end

local function links(text, face)
	if text == "" or text == "?" or text == "—" or text == "—" then --mdash
		return text
	else
		return m_links.full_link(text, nil, lang, nil, face, nil, {tr = "-"}, false)
	end
end

local function tag_text(text, tag, class)
	return m_links.full_link(nil, text, lang, nil, nil, nil, {tr = "-"}, false)
end

function track(page)
	require("Module:debug").track("ar-verb/" .. page)
	return true
end

function reorder_shadda(word)
	-- shadda+short-vowel (including tanwīn vowels, i.e. -an -in -un) gets
	-- replaced with short-vowel+shadda during NFC normalisation, which
	-- MediaWiki does for all Unicode strings; however, it makes various
	-- processes inconvenient, so undo it.
	word = rsub(word, "(" .. DIACRITIC_ANY_BUT_SH .. ")" .. SH, SH .. "%1")
	return word
end

-- synthesize a frame so that exported functions meant to be called from
-- templates can be called from the debug console.
function debug_frame(parargs, args)
	return {args = args, getParent = function() return {args = parargs} end}
end

---------------------------------------
-- Properties of different verbal forms
---------------------------------------

-- no longer supported
--local numeric_to_roman_form = {
--	["1"] = "I", ["2"] = "II", ["3"] = "III", ["4"] = "IV", ["5"] = "V",
--	["6"] = "VI", ["7"] = "VII", ["8"] = "VIII", ["9"] = "IX", ["10"] = "X",
--	["11"] = "XI", ["12"] = "XII", ["13"] = "XIII", ["14"] = "XIV", ["15"] = "XV",
--	["1q"] = "Iq", ["2q"] = "IIq", ["3q"] = "IIIq", ["4q"] = "IVq"
--}
--
---- convert numeric form to roman-numeral form
--local function canonicalize_form(form)
--	return numeric_to_roman_form[form] or form
--end

local allowed_forms = {"I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX",
	"X", "XI", "XII", "XIII", "XIV", "XV", "Iq", "IIq", "IIIq", "IVq"}

local function form_supports_final_weak(form)
	return form ~= "XI" and form ~= "XV" and form ~= "IVq"
end

local function form_supports_geminate(form)
	return form == "I" or form == "III" or form == "IV" or
		form == "VI" or form == "VII" or form == "VIII" or form == "X"
end

local function form_supports_hollow(form)
	return form == "I" or form == "IV" or form == "VII" or form == "VIII" or
		form == "X"
end

local function form_probably_impersonal_passive(form)
	return form == "VI"
end

local function form_probably_no_passive(form, weakness, past_vowel, nonpast_vowel)
	return form == "I" and weakness ~= "hollow" and contains(past_vowel, "u") or
		form == "VII" or form == "IX" or form == "XI" or form == "XII" or
		form == "XIII" or form == "XIV" or form == "XV" or form == "IIq" or
		form == "IIIq" or form == "IVq"
end

local function form_is_quadriliteral(form)
	return form == "Iq" or form == "IIq" or form == "IIIq" or form == "IVq"
end

-- Active forms II, III, IV, Iq use non-past prefixes in -u- instead of -a-.
function prefix_vowel_from_form(form)
	if form == "II" or form == "III" or form == "IV" or form == "Iq" then
		return "u"
	else
		return "a"
	end
end

-- true if the active non-past takes a-vowelling rather than i-vowelling
-- in its last syllable
function form_nonpast_a_vowel(form)
	return form == "V" or form == "VI" or form == "XV" or form == "IIq"
end

---------------------------------------------------
-- Radicals associated with various irregular verbs
---------------------------------------------------

-- Form-I verb أخذ or form-VIII verb اتخذ
local function axadh_radicals(rad1, rad2, rad3)
	return rad1 == HAMZA and rad2 == "خ" and rad3 == "ذ"
end

-- Form-I verb whose imperative has a reduced form: أكل and أخذ and أمر
local function reduced_imperative_verb(rad1, rad2, rad3)
	return axadh_radicals(rad1, rad2, rad3) or rad1 == HAMZA and (
		rad2 == "ك" and rad3 == "ل" or
		rad2 == "م" and rad3 == "ر")
end

-- Form-I verb رأى and form-IV verb أرى
local function raa_radicals(rad1, rad2, rad3)
	return rad1 == "ر" and rad2 == HAMZA and rad3 == Y
end

-- Form-I verb سأل
local function saal_radicals(rad1, rad2, rad3)
	return rad1 == "س" and rad2 == HAMZA and rad3 == "ل"
end

-- Form-I verb حيّ or حيي and form-X verb استحيا or استحى or يستحّى
local function hayy_radicals(rad1, rad2, rad3)
	return rad1 == "ح" and rad2 == Y and rad3 == Y
end

-----------------------
-- sets of past endings
-----------------------

-- the 13 endings of the sound/hollow/geminate past tense
local past_endings = {
	-- singular
	SK .. "تُ", SK .. "تَ", SK .. "تِ", A, A .. "تْ",
	--dual
	SK .. "تُمَا", AA, A .. "تَا",
	-- plural
	SK .. "نَا", SK .. "تُمْ",
	-- two Arabic diacritics don't work together in Wikimedia
	--SK .. "تُنَّ",
	SK .. "تُن" .. SH .. A, UU .. ALIF, SK .. "نَ"
}

-- make endings for final-weak past in -aytu or -awtu. AYAW is AY or AW
-- as appropriate. Note that AA and AW are global variables.
local function make_past_endings_ay_aw(ayaw, third_sg_masc)
	return {
	-- singular
	ayaw .. SK .. "تُ", ayaw ..  SK .. "تَ", ayaw .. SK .. "تِ",
	third_sg_masc, A .. "تْ",
	--dual
	ayaw .. SK .. "تُمَا", ayaw .. AA, A .. "تَا",
	-- plural
	ayaw .. SK .. "نَا", ayaw .. SK .. "تُمْ",
	-- two Arabic diacritics don't work together in Wikimedia
	--ayaw .. SK .. "تُنَّ",
	ayaw .. SK .. "تُن" .. SH .. A, AW .. SK .. ALIF, ayaw .. SK .. "نَ"
	}
end

-- past final-weak -aytu endings
local past_endings_ay = make_past_endings_ay_aw(AY, AAMAQ)
-- past final-weak -awtu endings
local past_endings_aw = make_past_endings_ay_aw(AW, AA)

-- Make endings for final-weak past in -ītu or -ūtu. IIUU is ī or ū as
-- appropriate. Note that AA and UU are global variables.
local function make_past_endings_ii_uu(iiuu)
	return {
	-- singular
	iiuu .. "تُ", iiuu .. "تَ", iiuu .. "تِ", iiuu .. A, iiuu .. A .. "تْ",
	--dual
	iiuu .. "تُمَا", iiuu .. AA, iiuu .. A .. "تَا",
	-- plural
	iiuu .. "نَا", iiuu .. "تُمْ",
	-- two Arabic diacritics don't work together in Wikimedia
	--iiuu .. "تُنَّ",
	iiuu .. "تُن" .. SH .. A, UU .. ALIF, iiuu .. "نَ"
	}
end

-- past final-weak -ītu endings
local past_endings_ii = make_past_endings_ii_uu(II)
-- past final-weak -ūtu endings
local past_endings_uu = make_past_endings_ii_uu(UU)

----------------------------------------
-- sets of non-past prefixes and endings
----------------------------------------

-- prefixes for non-past forms in -a-
local nonpast_prefixes_a = {
	-- singular
	HAMZA .. A, "تَ", "تَ", "يَ", "تَ",
	--dual
	"تَ", "يَ", "تَ",
	-- plural
	"نَ", "تَ", "تَ", "يَ", "يَ"
}

-- prefixes for non-past forms in -u- (passive; active forms II, III, IV, Iq)
local nonpast_prefixes_u = {
	-- singular
	HAMZA .. U, "تُ", "تُ", "يُ", "تُ",
	--dual
	"تُ", "يُ", "تُ",
	-- plural
	"نُ", "تُ", "تُ", "يُ", "يُ"
}

-- There are only five distinct endings in all non-past verbs. Make any set of
-- non-past endings given these five distinct endings.
local function make_nonpast_endings(null, fem, dual, pl, fempl)
	return {
	-- singular
	null, null, fem, null, null,
	-- dual
	dual, dual, dual,
	-- plural
	null, pl, fempl, pl, fempl
	}
end

-- endings for non-past indicative
local indic_endings = make_nonpast_endings(
	U,
	I .. "ينَ",
	A .. "انِ",
	U .. "ونَ",
	SK .. "نَ"
)

-- make the endings for non-past subjunctive/jussive, given the vowel diacritic
-- used in "null" endings (1s/2sm/3sm/3sf/1p)
local function make_subj_juss_endings(dia_null)
	return make_nonpast_endings(
	dia_null,
	I .. "ي",
	A .. "ا",
	U .. "و",
	SK .. "نَ"
	)
end

-- endings for non-past subjunctive
local subj_endings = make_subj_juss_endings(A)

-- endings for non-past jussive
local juss_endings = make_subj_juss_endings(SK)

-- endings for alternative geminate non-past jussive in -a; same as subjunctive
local juss_endings_alt_a = subj_endings

-- endings for alternative geminate non-past jussive in -i
local juss_endings_alt_i = make_subj_juss_endings(I)

-- endings for final-weak non-past indicative in -ā. Note that AY, AW and
-- AAMAQ are global variables.
local indic_endings_aa = make_nonpast_endings(
	AAMAQ,
	AYSK .. "نَ",
	AY .. A .. "انِ",
	AWSK .. "نَ",
	AYSK .. "نَ"
)

-- make endings for final-weak non-past indicative in -ī or -ū; IIUU is
-- ī or ū as appropriate. Note that II and UU are global variables.
local function make_indic_endings_ii_uu(iiuu)
	return make_nonpast_endings(
	iiuu,
	II .. "نَ",
	iiuu .. A .. "انِ",
	UU .. "نَ",
	iiuu .. "نَ"
	)
end

-- endings for final-weak non-past indicative in -ī
local indic_endings_ii = make_indic_endings_ii_uu(II)

-- endings for final-weak non-past indicative in -ū
local indic_endings_uu = make_indic_endings_ii_uu(UU)

-- endings for final-weak non-past subjunctive in -ā. Note that AY, AW, ALIF,
-- AAMAQ are global variables.
local subj_endings_aa = make_nonpast_endings(
	AAMAQ,
	AYSK,
	AY .. AA,
	AWSK .. ALIF,
	AYSK .. "نَ"
)

-- make endings for final-weak non-past subjunctive in -ī or -ū. IIUU is
-- ī or ū as appropriate. Note that AA, II, UU, ALIF are global variables.
local function make_subj_endings_ii_uu(iiuu)
	return make_nonpast_endings(
	iiuu .. A,
	II,
	iiuu .. AA,
	UU .. ALIF,
	iiuu .. "نَ"
	)
end

-- endings for final-weak non-past subjunctive in -ī
local subj_endings_ii = make_subj_endings_ii_uu(II)

-- endings for final-weak non-past subjunctive in -ū
local subj_endings_uu = make_subj_endings_ii_uu(UU)

-- endings for final-weak non-past jussive in -ā
local juss_endings_aa = make_nonpast_endings(
	A,
	AYSK,
	AY .. AA,
	AWSK .. ALIF,
	AYSK .. "نَ"
)

-- Make endings for final-weak non-past jussive in -ī or -ū. IU is short i or u,
-- IIUU is long ī or ū as appropriate. Note that AA, II, UU, ALIF are global
-- variables.
local function make_juss_endings_ii_uu(iu, iiuu)
	return make_nonpast_endings(
	iu,
	II,
	iiuu .. AA,
	UU .. ALIF,
	iiuu .. "نَ"
	)
end

-- endings for final-weak non-past jussive in -ī
local juss_endings_ii = make_juss_endings_ii_uu(I, II)

-- endings for final-weak non-past jussive in -ū
local juss_endings_uu = make_juss_endings_ii_uu(U, UU)

-----------------------------
-- sets of imperative endings
-----------------------------

-- extract the second person jussive endings to get corresponding imperative
-- endings
local function imperative_endings_from_jussive(endings)
	return {endings[2], endings[3], endings[6], endings[10], endings[11]}
end

-- normal imperative endings
local impr_endings = imperative_endings_from_jussive(juss_endings)
-- alternative geminate imperative endings in -a
local impr_endings_alt_a = imperative_endings_from_jussive(juss_endings_alt_a)
-- alternative geminate imperative endings in -i
local impr_endings_alt_i = imperative_endings_from_jussive(juss_endings_alt_i)
-- final-weak imperative endings in -ā
local impr_endings_aa = imperative_endings_from_jussive(juss_endings_aa)
-- final-weak imperative endings in -ī
local impr_endings_ii = imperative_endings_from_jussive(juss_endings_ii)
-- final-weak imperative endings in -ū
local impr_endings_uu = imperative_endings_from_jussive(juss_endings_uu)

-----------------------------
-- Main conjugation functions
-----------------------------

function get_args(args)
	local origargs = args
	local args = {}
	-- Convert empty arguments to nil, and "" or '' arguments to empty
	for k, v in pairs(origargs) do
		args[k] = ine(v)
	end
	return origargs, args
end

function get_frame_args(frame)
	return get_args(frame:getParent().args)
end

-- Implement {{ar-conj}}.
function export.show(frame)
	local origargs, args = get_frame_args(frame)

	local data, form, weakness, past_vowel, nonpast_vowel = conjugate(args, 1)

	-- if the value is "yes" or variants, the verb is intransitive;
	-- if the value is "no" or variants, the verb is transitive.
	-- If not specified, default is intransitive if passive == false or
	-- passive == "impers", else transitive.
	local intrans = args["intrans"]
	if not intrans then
		intrans = data.passive == false or data.passive == "impers" or data.passive == "only-impers"
	else
		intrans = yesno(intrans, "unknown")
		if intrans == "unknown" then
			error("Unrecognized value '" .. args["intrans"] ..
				"' to argument intrans=; use 'yes'/'y'/'true'/'1' or 'no'/'n'/'false'/'0'")
		end
	end
	if intrans then
		table.insert(data.categories, "Arabic intransitive verbs")
	else
		table.insert(data.categories, "Arabic transitive verbs")
	end

	-- initialize title, with weakness indicated by conjugation
	-- (FIXME should it be by form?)
	local title = "form-" .. form .. " " .. weakness

	if data.passive == "only" or data.passive == "only-impers" then
		title = title .. " passive"
	end

	if data.irregular then
		table.insert(data.categories, "Arabic irregular verbs")
		title = title .. " irregular"
	end

	return make_table(data, title, form, intrans) ..
		m_utilities.format_categories(data.categories, lang)
end

-- Version of main entry point meant for calling from the debug console.
-- An example call might be as follows:
--
-- =p.show2({'I', 'a', 'u', 'ك', 'ت', 'ب'})
--
-- This is equivalent to the following template call:
--
-- {{ar-conj|I|a|u|ك|ت|ب}}
--
-- Note that the radicals were actually typed in with ك first, followed by ت
-- and then ب, but they appear in opposite order due to right-to-left
-- display issues. It's necessary to specify the radicals in the call,
-- unlike in a template call where they are inferred from the page name,
-- because there is (currently) no way to control the page name used in
-- the radical-inference code, where it will appear as 'Module:ar-verb'.
function export.show2(parargs, args)
	return export.show(debug_frame(parargs, args))
end

-- Implement {{ar-verb}}.
-- TODO: Move this into [[Module:ar-headword]]
function export.headword(frame)
	local origargs, args = get_frame_args(frame)

	local data, form, weakness, past_vowel, nonpast_vowel = conjugate(args, 1)
	local use_params = form == "I" or args["useparam"]

	local arabic_3sm_perf, latin_3sm_perf
	local arabic_3sm_imperf, latin_3sm_imperf
	if data.passive == "only" or data.passive == "only-impers" then
		arabic_3sm_perf, latin_3sm_perf = get_spans(data.forms["3sm-ps-perf"])
		arabic_3sm_imperf, latin_3sm_imperf = get_spans(data.forms["3sm-ps-impf"])
	else
		arabic_3sm_perf, latin_3sm_perf = get_spans(data.forms["3sm-perf"])
		arabic_3sm_imperf, latin_3sm_imperf = get_spans(data.forms["3sm-impf"])
	end

	local PAGENAME = mw.title.getCurrentTitle().text
	-- set to PAGENAME if left empty
	local head, tr
	if use_params and form ~= "I" then
		table.insert(data.headword_categories, "Arabic augmented verbs with parameter override")
	end
	if use_params and args["head"] then
		head = {args["head"], args["head2"], args["head3"]}
		tr = {args["tr"], args["tr2"], args["tr3"]}
		if form == "I" then
			table.insert(data.headword_categories, "Arabic form-I verbs with headword perfect determined through param, not past vowel")
		end
	elseif use_params and args["tr"] then
		head = PAGENAME
		tr = args["tr"]
		if form == "I" then
			table.insert(data.headword_categories, "Arabic form-I verbs with headword perfect determined through param, not past vowel")
		end
	elseif form == "I" and #past_vowel == 0 then
		head = PAGENAME
		table.insert(data.headword_categories, "Arabic form-I verbs with missing past vowel in headword")
	else
		-- Massive hack to get the order correct. It gets reversed for some
		-- reason, possibly due to being surrounded by lang=ar or
		-- class=Arabic headword, so reverse the order before concatenating.
		headrev = {}
		for _, ar in ipairs(arabic_3sm_perf) do
			table.insert(headrev, 1, ar)
		end
		head = table.concat(headrev, " <small style=\"color: #888\">or</small> ")
		tr = table.concat(latin_3sm_perf, " <small style=\"color: #888\">or</small> ")
	end

	local form_text = ' <span class="gender">[[Appendix:Arabic verbs#Form ' .. form .. '|<abbr title="Verb form ' ..
	  form .. '">' .. form .. '</abbr>]]</span>'

	local impf_arabic, impf_tr
	if use_params and args["impf"] then
		impf_arabic = args["impfhead"] or args["impf"]
		impf_tr = args["impftr"] or ar_translit.tr(impf_arabic, nil, nil, nil)
		impf_arabic = {impf_arabic}
		if form == "I" then
			table.insert(data.headword_categories, "Arabic form-I verbs with headword imperfect determined through param, not non-past vowel")
		end
	elseif form == "I" and #nonpast_vowel == 0 then
		impf_arabic = {}
		table.insert(data.headword_categories, "Arabic form-I verbs with missing non-past vowel in headword")
	else
		impf_arabic = arabic_3sm_imperf
		impf_tr = table.concat(latin_3sm_imperf, " <small style=\"color: #888\">or</small> ")
	end

	-- convert Arabic terms to bolded links
	for i, entry in ipairs(impf_arabic) do
		impf_arabic[i] = links(entry, "bold")
	end
	-- create non-past text by concatenating Arabic spans and adding
	-- transliteration, but only if we can do it non-ambiguously (either we're
	-- not form I or the non-past vowel was specified)
	local impf_text = ""
	if #impf_arabic > 0 then
		impf_text = ', <i>non-past</i> ' .. table.concat(impf_arabic, " <small style=\"color: #888\">or</small> ")
		if impf_tr then
			impf_text = impf_text .. LRM .. " (" .. impf_tr .. ")"
		end
	end

	return
		m_headword.full_headword(lang, nil, head, tr, nil, nil, {"Arabic verbs"}, args["sort"]) ..
		form_text .. impf_text ..
		m_utilities.format_categories(data.headword_categories, lang)
end

-- Version of headword entry point meant for calling from the debug console.
-- See export.show2().
function export.headword2(parargs, args)
	return export.headword(debug_frame(parargs, args))
end

-- Implementation of export.past3sm() and export.past3sm_all().
function past3sm(frameargs, doall)
	local origargs, args = get_args(frameargs)

	local data, form, weakness, past_vowel, nonpast_vowel =	conjugate(args, 1)

	local arabic_3sm_perf, latin_3sm_perf
	if data.passive == "only" or data.passive == "only-impers" then
		arabic_3sm_perf, latin_3sm_perf = get_spans(data.forms["3sm-ps-perf"])
	else
		arabic_3sm_perf, latin_3sm_perf = get_spans(data.forms["3sm-perf"])
	end

	if doall then
		return table.concat(arabic_3sm_perf, ",")
	else
		return arabic_3sm_perf[1]
	end
end

-- Implement {{ar-past3sm}}.
--
-- Generate the 3rd singular masculine past tense (the dictionary form), given
-- the form, radicals and (for form I) past/non-past vowels (the non-past
-- vowel is ignored, but specified for compatibility with export.headword()
-- and export.show()). Form, radicals, past/non-past vowel arguments are the
-- same as for export.show(). Note that the form returned may be active or
-- passive depending on the passive= param (some values specify that the
-- verb is a passive-only verb). If there are multiple alternatives,
-- return only the first one.
function export.past3sm(frame)
	return past3sm(frame:getParent().args, false)
end

-- Version of past3sm entry point meant for calling from the debug console.
-- See export.show2().
function export.past3sm2(parargs, args)
	return export.past3sm(debug_frame(parargs, args))
end

-- Implement {{ar-past3sm-all}}.
--
-- Same as export.past3sm() but return all possible values, separated by
-- a comma. Multiple values largely come from alternative hamza seats.
function export.past3sm_all(frame)
	return past3sm(frame:getParent().args, true)
end

-- Version of past3sm_all entry point meant for calling from the debug console.
-- See export.show2().
function export.past3sm_all2(parargs, args)
	return export.past3sm_all(debug_frame(parargs, args))
end

-- Implementation of export.verb_part() and export.verb_part_all().
function verb_part(frameargs, doall)
	local origargs, args = get_args(frameargs)
	local part = args[1]
	local data, form, weakness, past_vowel, nonpast_vowel =	conjugate(args, 2)
	local arabic, latin = get_spans(data.forms[part])

	if doall then
		return table.concat(arabic, ",")
	else
		return arabic[1]
	end
end

-- Implement {{ar-verb-part}}.
--
-- TODO: Move this into [[Module:ar-headword]]
-- Generate an arbitrary part of the verbal paradigm. If there are multiple
-- possible alternatives, return only the first one.
function export.verb_part(frame)
	return verb_part(frame:getParent().args, false)
end

-- Version of verb_part entry point meant for calling from the debug console.
-- See export.show2().
function export.verb_part2(parargs, args)
	return export.verb_part(debug_frame(parargs, args))
end

-- Implement {{ar-verb-part-all}}.
--
-- TODO: Move this into [[Module:ar-headword]]
-- Generate an arbitrary part of the verbal paradigm. If there are multiple
-- possible alternatives, return all, separated by commas.
function export.verb_part_all(frame)
	return verb_part(frame:getParent().args, true)
end

-- Version of verb_part_all entry point meant for calling from the debug
-- console. See export.show2().
function export.verb_part_all2(parargs, args)
	return export.verb_part_all(debug_frame(parargs, args))
end

-- Return a property of the conjugation other than a verb part.
function verb_prop(frameargs)
	local origargs, args = get_args(frameargs)

	local prop = args[1]
	local data, form, weakness, past_vowel, nonpast_vowel = conjugate(args, 2)
	if prop == "form" then
		return form
	elseif prop == "weakness" then
		return weakness
	elseif prop == "form-weakness" then
		return form .. "-" .. weakness
	elseif prop == "past-vowel" then
		return table.concat(past_vowel, ",")
	elseif prop == "nonpast-vowel" then
		return table.concat(nonpast_vowel, ",")
	elseif prop == "rad1" then
		return data.rad1 or ""
	elseif prop == "rad2" then
		return data.rad2 or ""
	elseif prop == "rad3" then
		return data.rad3 or ""
	elseif prop == "rad4" then
		return data.rad4 or ""
	elseif prop == "unreg-rad1" then
		return data.unreg_rad1 or ""
	elseif prop == "unreg-rad2" then
		return data.unreg_rad2 or ""
	elseif prop == "unreg-rad3" then
		return data.unreg_rad3 or ""
	elseif prop == "unreg-rad4" then
		return data.unreg_rad4 or ""
	elseif prop == "radicals" then
		return table.concat({data.rad1, data.rad2, data.rad3, data.rad4}, ",")
	elseif prop == "unreg-radicals" then
		return table.concat({data.unreg_rad1, data.unreg_rad2,
		data.unreg_rad3, data.unreg_rad4}, ",")
	elseif prop == "passive" then
		return data.passive == true and "yes" or not data.passive and "no"
			or data.passive
	elseif prop == "passive-uncertain" then
		return data.passive_uncertain and "yes" or "no"
	elseif prop == "vn-uncertain" then
		return data.vn_uncertain and "yes" or "no"
	elseif prop == "irregular" then
		return data.irregular and "yes" or "no"
	--elseif prop == "intrans" then
	--	return data.intrans
	else
		error("Unrecognized property '" .. prop .. "'")
	end
end

-- Return a property of the conjugation other than a verb part.
function export.verb_prop(frame)
	return verb_prop(frame:getParent().args)
end

-- Version of verb_prop entry point meant for calling from the debug console.
-- See export.show2().
function export.verb_prop2(parargs, args)
	return export.verb_prop(debug_frame(parargs, args))
end

function export.verb_forms(frame)
	local origargs, args = get_frame_args(frame)
	local i = 1
	local past_vowel_re = "^[aui,]*$"
	local combined_root = nil
	if not args[i] or rfind(args[i], past_vowel_re) then
		combined_root = mw.title.getCurrentTitle().text
		if not rfind(combined_root, " ") then
			error("When inferring roots from page title, need spaces in page title: " .. combined_root)
		end
	elseif rfind(args[i], " ") then
		combined_root = args[i]
		i = i + 1
	else
		local separate_roots = {}
		while args[i] and not rfind(args[i], past_vowel_re) do
			table.insert(separate_roots, args[i])
			i = i + 1
		end
		combined_root = table.concat(separate_roots, " ")
	end
	local past_vowel = args[i]
	i = i + 1
	if past_vowel and not rfind(past_vowel, past_vowel_re) then
		error("Unrecognized past vowel, should be 'a', 'i', 'u', 'a,u', etc. or empty: " .. past_vowel)
	end
	if not past_vowel then
		past_vowel = ""
	end

	local split_root = rsplit(combined_root, " ")
	-- Map from verb forms (I, II, etc.) to a table of verb properties,
	-- which has entries e.g. for "verb" (either true to autogenerate the verb
	-- head, or an explicitly specified verb head using e.g. argument "I-head"),
	-- and for "verb-gloss" (which comes from e.g. the argument "I" or "I-gloss"),
	-- and for "vn" and "vn-gloss", "ap" and "ap-gloss", "pp" and "pp-gloss".
	local verb_properties = {}
	for _, form in ipairs(allowed_forms) do
		local formpropslist = {}
		local derivs = {{"verb", ""}, {"vn", "-vn"}, {"ap", "-ap"}, {"pp", "-pp"}}
		local index = 1
		while true do
			local formprops = {}
			local prefix = index == 1 and form or form .. index
			if prefix == "I" then
				formprops["pv"] = past_vowel
			end
			if args[prefix .. "-pv"] then
				formprops["pv"] = args[prefix .. "-pv"]
			end
			for _, deriv in ipairs(derivs) do
				local prop = deriv[1]
				local extn = deriv[2]
				if args[prefix .. extn] == "+" then
					formprops[prop] = true
				elseif args[prefix .. extn] == "-" then
					formprops[prop] = false
				elseif args[prefix .. extn] then
					formprops[prop] = true
					formprops[prop .. "-gloss"] = args[prefix .. extn]
				end
				if args[prefix .. extn .. "-head"] then
					if formprops[prop] == nil then
						formprops[prop] = true
					end
					formprops[prop] = args[prefix .. extn .. "-head"]
				end
				if args[prefix .. extn .. "-gloss"] then
					if formprops[prop] == nil then
						formprops[prop] = true
					end
					formprops[prop .. "-gloss"] = args[prefix .. extn .. "-gloss"]
				end
			end
			if formprops["verb"] then
				-- If a verb form specified, also turn on vn (unless form I, with
				-- unpredictable vn) and ap, and maybe pp, according to form,
				-- weakness and past vowel. But don't turn these on if there's
				-- an explicit on/off specification for them (e.g. I-pp=-).
				if form ~= "I" and formprops["vn"] == nil then
					formprops["vn"] = true
				end
				if formprops["ap"] == nil then
					formprops["ap"] = true
				end
				local weakness = weakness_from_radicals(form, split_root[1],
					split_root[2], split_root[3], split_root[4])
				if formprops["pp"] == nil and not form_probably_no_passive(form,
						weakness, rsplit(formprops["pv"] or "", ","), {}) then
					formprops["pp"] = true
				end
				table.insert(formpropslist, formprops)
				index = index + 1
			else
				break
			end
		end
		table.insert(verb_properties, {form, formpropslist})
	end

	-- Go through and create the verb form derivations as necessary, when
	-- they haven't been explicitly given
	for _, vplist in ipairs(verb_properties) do
		local form = vplist[1]
		for _, props in ipairs(vplist[2]) do
			local args = {}
			function append_form_and_root()
				table.insert(args, form)
				if form == "I" then
					table.insert(args, props["pv"]) -- past vowel
					table.insert(args, "")
				end
				append_array(args, split_root)
			end
			if props["verb"] == true then
				args = {}
				append_form_and_root()
				props["verb"] = past3sm(args, true)
			end
			for _, deriv in ipairs({"vn", "ap", "pp"}) do
				if props[deriv] == true then
					args = {deriv}
					append_form_and_root()
					props[deriv] = verb_part(args, true)
				end
			end
		end
	end

    -- Go through and output the result
	local formtextarr = {}
	for _, vplist in ipairs(verb_properties) do
		local form = vplist[1]
		for _, props in ipairs(vplist[2]) do
			local textarr = {}
			if props["verb"] then
				local text = "* '''[[Appendix:Arabic verbs#Form " .. form .. "|Form " .. form .. "]]''': "
				local linktext = {}
				local splitheads = rsplit(props["verb"], "[,،]")
				for _, head in ipairs(splitheads) do
					table.insert(linktext, m_links.full_link(head, nil, lang, nil, nil, nil, {gloss = ine(props["verb-gloss"])}, false))
				end
				text = text .. table.concat(linktext, ", ")
				table.insert(textarr, text)
				for _, derivengl in ipairs({{"vn", "Verbal noun"}, {"ap", "Active participle"}, {"pp", "Passive participle"}}) do
					local deriv = derivengl[1]
					local engl = derivengl[2]
					if props[deriv] then
						local text = "** " .. engl .. ": "
						local linktext = {}
						local splitheads = rsplit(props[deriv], "[,،]")
						for _, head in ipairs(splitheads) do
							table.insert(linktext, m_links.full_link(head, nil, lang, nil, nil, nil, {gloss = ine(props[deriv .. "-gloss"])}, false))
						end
						text = text .. table.concat(linktext, ", ")
						table.insert(textarr, text)
					end
				end
				table.insert(formtextarr, table.concat(textarr, "\n"))
			end
		end
	end

	return table.concat(formtextarr, "\n")
end

-- Version of verb_forms entry point meant for calling from the debug console.
-- See export.show2().
function export.verb_forms2(parargs, args)
	return export.verb_forms(debug_frame(parargs, args))
end

-- Guts of conjugation functions. Shared between {{temp|ar-conj}} and
-- {{temp|ar-verb}}, among others. ARGS is the frame parent arguments,
-- as returned by get_frame_args() (i.e. with arguments passed through ine()).
-- ARGIND is the numbered argument holding the verb form (either 1 or 2);
-- if form is I, the next two arguments are the past and non-past vowels;
-- afterwards are the (optional) radicals. Return five values:
-- DATA, FORM, WEAKNESS, PAST_VOWEL, NONPAST_VOWEL. The last two are
-- arrays of vowels (each one 'a', 'i' or 'u'), since there may be more
-- than one, or none in the case of non-form-I verbs.
function conjugate(args, argind)
	local data = {forms = {}, categories = {}, headword_categories = {}}

	PAGENAME = mw.title.getCurrentTitle().text

	local conj_type = args[argind] or
		error("Form (conjugation type) has not been specified. " ..
			"Please pass a value to parameter " .. argind ..
			" in the template invocation.")

	-- derive form and weakness from conj type
	local form, weakness
	if rfind(conj_type, "%-") then
		local form_weakness = rsplit(conj_type, "%-")
		form = form_weakness[1]
		table.remove(form_weakness, 1)
		weakness = table.concat(form_weakness, "-")
	else
		form = conj_type
		weakness = nil
	end

	-- convert numeric forms to Roman numerals
	-- no longer supported
	-- form = canonicalize_form(form)

	-- check for quadriliteral form (Iq, IIq, IIIq, IVq)
	local quadlit = rmatch(form, "q$")

	-- get radicals and past/non-past vowel
	local rad1, rad2, rad3, rad4, past_vowel, nonpast_vowel
	if form == "I" then
		past_vowel = args[argind + 1]
		nonpast_vowel = args[argind + 2]
		local function splitvowel(vowelspec)
			if vowelspec == nil then
				vowelspec = {}
			else
				vowelspec = rsplit(vowelspec, ",")
			end
			return vowelspec
		end
		-- allow multiple past or non-past vowels separated by commas, e.g.
		-- in farada/faruda yafrudu "to be single"
		past_vowel = splitvowel(past_vowel)
		nonpast_vowel = splitvowel(nonpast_vowel)
		rad1 = args[argind + 3] or args["I"]
		rad2 = args[argind + 4] or args["II"]
		rad3 = args[argind + 5] or args["III"]
	else
		rad1 = args[argind + 1] or args["I"]
		rad2 = args[argind + 2] or args["II"]
		rad3 = args[argind + 3] or args["III"]
		if quadlit then
			rad4 = args[argind + 4] or args["IV"]
		end
	end

	-- Default any unspecified radicals to radicals determined from the
	-- headword. The return radicals may have Latin letters in them (w, t, y)
	-- to indicate ambiguous radicals that should be converted to the
	-- corresponding Arabic letters.
	--
	-- Only call infer_radicals() if at least one radical unspecified,
	-- because infer_radicals() will throw an error if the headword is
	-- malformed for the form, and we don't want that to happen (e.g. we might
	-- be called from a test page).
	if not rad1 or not rad2 or not rad3 or quadlit and not rad4 then
		local wkness, r1, r2, r3, r4 =
			export.infer_radicals(PAGENAME, form)
		-- Use the inferred weakness if we don't override any of the inferred
		-- radicals with something else, i.e. for each user-specified radical,
		-- either it's nil (was not specified) or same as inferred radical.
		-- That way we will correctly set the weakness to sound in cases like
		-- layisa "to be valiant", 'aḥwaja "to need", istahwana "to consider easy",
		-- izdawaja "to be in pairs", etc.
		local use_wkness = (not rad1 or rad1 == r1) and (not rad2 or rad2 == r2) and
			(not rad3 or rad3 == r3) and (not rad4 or rad4 == r4)
		rad1 = rad1 or r1
		rad2 = rad2 or r2
		rad3 = rad3 or r3
		rad4 = rad4 or r4

		-- For most ambiguous radicals, the choice of radical doesn't matter
		-- because it doesn't affect the conjugation one way or another.
		-- For form I hollow verbs, however, it definitely does. In fact, the
		-- choice of radical is critical even beyond the past and non-past
		-- vowels because it affects the form of the passive participle.
		-- So, check for this, try to guess if necessary from non-past vowel,
		-- else signal an error, requiring that the radical be specified
		-- explicitly. This will happen when the non-past vowel isn't specified
		-- and also when it's "a", from which the radical cannot be inferred.
		-- Do this check here rather than in infer_radicals() so that we don't
		-- get an error if the appropriate radical is given but not others.
		if form == "I" and (rad2 == "w" or rad2 == "y") then
			if contains(nonpast_vowel, "i") then
				rad2 = Y
			elseif contains(nonpast_vowel, "u") then
				rad2 = W
			else
				error("Unable to guess middle radical of hollow form I verb; " ..
					"need to specify radical explicitly")
			end
		end

		-- If weakness unspecified, then maybe default to weakness determined
		-- from headword. We do this specifically when some radicals are
		-- unspecified and all specified radicals are the same as the
		-- corresponding inferred radicals, i.e. the specified radicals (if any)
		-- don't provide any new information. When this isn't the case, and
		-- the specified radicals override the inferred radicals with something
		-- else, the inferred weakness may be wrong, so we figure out
		-- the weakness below by ourselves, based on the combination of any
		-- user-specified and inferred radicals.
		--
		-- The reason for using the inferred weakness when possible is that
		-- it may be more accurate than the weakness we derive below, in
		-- particular with verbs like layisa "to be courageous",
		-- `awira "to be one-eyed", 'aḥwaja "to need", istajwaba "to interrogate",
		-- izdawaja "to be in pairs", with a weak vowel in a sound conjugation.
		-- The weakness derived below from the radicals would be hollow but the
		-- weakness inferred in infer_radicals() is (correctly) sound.
		if use_wkness then
			weakness = weakness or wkness
		end
	end

	-- Store unregularized radicals for later use in creating categories
	data.unreg_rad1 = rad1
	data.unreg_rad2 = rad2
	data.unreg_rad3 = rad3
	data.unreg_rad4 = rad4

	-- Convert the Latin radicals indicating ambiguity into the corresponding
	-- Arabic radicals.
	local function regularize_inferred_radical(rad)
		if rad == "t" then
			return T
		elseif rad == "w" then
			return W
		elseif rad == "y" then
			return Y
		else
			return rad
		end
	end

	rad1 = regularize_inferred_radical(rad1)
	rad2 = regularize_inferred_radical(rad2)
	rad3 = regularize_inferred_radical(rad3)
	rad4 = regularize_inferred_radical(rad4)

	data.rad1 = rad1
	data.rad2 = rad2
	data.rad3 = rad3
	data.rad4 = rad4

	-- Old code, default radicals to ف-ع-ل or variants.

	--if not quadlit then
	--	-- default radicals to ف-ع-ل (or ف-ل-ل for geminate, or with the
	--	-- appropriate radical replaced by wāw for assimilated/hollow/final-weak)
	--	rad1 = rad1 or
	--		(weakness == "assimilated" or weakness == "assimilated+final-weak") and W or "ف"
	--	rad2 = rad2 or weakness == "hollow" and W or
	--		weakness == "geminate" and "ل" or "ع"
	--	rad3 = rad3 or (weakness == "final-weak" or weakness == "assimilated+final-weak") and W or
	--		weakness == "geminate" and rad2 or "ل"
	--else
	--	-- default to ف-ع-ل-ق (or ف-ع-ل-و for final-weak)
	--	rad1 = rad1 or "ف"
	--	rad2 = rad2 or "ع"
	--	rad3 = rad3 or "ل"
	--	rad4 = rad4 or weakness == "final-weak" and W or "ق"
	--end

	if weakness == nil then
		weakness = weakness_from_radicals(form, rad1, rad2, rad3, rad4)
	end
	
	-- Error if radicals are wrong given the weakness. More likely to happen
	-- if the weakness is explicitly given rather than inferred. Will also
	-- happen if certain incorrect letters are included as radicals e.g.
	-- hamza on top of various letters, alif maqṣūra, tā' marbūṭa.
	check_radicals(form, weakness, rad1, rad2, rad3, rad4)

	-- check to see if an argument ends in a ?. If so, strip the ? and return
	-- true. Otherwise, return false.
	local function check_for_uncertainty(arg)
		if args[arg] and rfind(args[arg], "%?$") then
			args[arg] = rsub(args[arg], "%?$", "")
			if args[arg] == "" then
				args[arg] = nil
			end
			return true
		else
			return false
		end
	end

	-- allow a ? at the end of vn= and passive=; if so, putting the page into
	-- special categories indicating the need to check the property in
	-- question, and remove the ?. Also put into category for vn= if empty
	-- and form is I.
	if check_for_uncertainty("vn") or form == "I" and not args["vn"] then
		table.insert(data.categories, "Arabic verbs needing verbal noun checked")
		data.vn_uncertain = true
	end
	if check_for_uncertainty("passive") then
		table.insert(data.categories, "Arabic verbs needing passive checked")
		data.passive_uncertain = true
	end

	-- parse value of passive.
	--
	-- if the value is "impers", the verb has only impersonal passive;
	-- if the value is "only", the verb has only a passive, no active;
	-- if the value is "only-impers", the verb has only an impersonal passive;
	-- if the value is "yes" or variants, verb has a passive;
	-- if the value is "no" or variants, the verb has no passive.
	-- If not specified, default is yes, but no for forms VII, IX,
	-- XII - XV and IIq - IVq, and "impers" for form VI.
	local passive = args["passive"]
	if passive == "impers" or passive == "only" or passive == "only-impers" then
	elseif not passive then
		passive = form_probably_impersonal_passive(form) and "impers" or
			not form_probably_no_passive(form, weakness, past_vowel,
				nonpast_vowel) and true or false
	else
		passive = yesno(passive, "unknown")
		if passive == "unknown" then
			error("Unrecognized value '" .. args["passive"] ..
				"' to argument passive=; use 'impers', 'only', 'only-impers', " ..
				"'yes'/'y'/'true'/'1' or 'no'/'n'/'false'/'0'")
		end
	end
	data.passive = passive

	-- Initialize categories related to form and weakness.
	initialize_categories(data,	form, weakness, rad1, rad2, rad3, rad4)

	-- Reconstruct conjugation type from form and (possibly inferred) weakness.
	conj_type = form .. "-" .. weakness

	-- Check that the conjugation type is recognized.
	if not conjugations[conj_type] then
		error("Unknown conjugation type '" .. conj_type .. "'")
	end

	-- Actually conjugate the verb. The signature of the conjugation function
	-- is different for form-I verbs, non-form-I triliteral verbs, and
	-- quadriliteral verbs.
	--
	-- The way the conjugation functions work is they always add entries to the
	-- appropriate parts of the paradigm (each of which is an array), rather
	-- than setting the values. This makes it possible to call more than one
	-- conjugation function and essentially get a paradigm of the "either
	-- A or B" kind. Doing this may insert duplicate entries into a particular
	-- paradigm part, but this is not a problem because we remove duplicate
	-- entries (in get_spans()) before generating the actual table.
	if quadlit then
		conjugations[conj_type](data, args, rad1, rad2, rad3, rad4)
	elseif form ~= "I" then
		conjugations[conj_type](data, args, rad1, rad2, rad3)
	else
		-- For Form-I verbs, we also pass in the past and non-past vowels.
		-- There may be more than one of each in case of alternative possible
		-- conjugations. In such cases, we loop over the sets of vowels,
		-- calling the appropriate conjugation function for each combination
		-- of past and non-past vowel.

		-- If the past or non-past vowel is unspecified, its value will be
		-- an empty array. In such a case, we still want to iterate once,
		-- passing in nil. Ideally, we'd convert empty arrays into one-element
		-- arrays holding the value nil, but Lua doesn't let you put the
		-- value nil into an array. To work around this we convert each array
		-- to an array of one-element arrays and fetch the first item of the
		-- inner array when we encounter it. Corresponding to nil will
		-- be an empty array, and fetching its first item will indeed
		-- return nil.
		local function convert_to_nested_array(array)
			if #array == 0 then
				return {{}}
			else
				local retval = {}
				for _, el in ipairs(array) do
					table.insert(retval, {el})
				end
				return retval
			end
		end
		local pv_nested = convert_to_nested_array(past_vowel)
		local npv_nested = convert_to_nested_array(nonpast_vowel)
		for i, pv in ipairs(pv_nested) do
			for j, npv in ipairs(npv_nested) do
				-- items were made into 1-element arrays so undo this
				conjugations[conj_type](data, args, rad1, rad2, rad3, pv[1], npv[1])
			end
		end
	end

	return data, form, weakness, past_vowel, nonpast_vowel
end

-- Determine weakness from radicals
function weakness_from_radicals(form, rad1, rad2, rad3, rad4)
	local weakness = nil
	local quadlit = rmatch(form, "q$")
	-- If weakness unspecified, derive from radicals.
	if not quadlit then
		if is_waw_ya(rad3) and rad1 == W and form == "I" then
			weakness = "assimilated+final-weak"
		elseif is_waw_ya(rad3) and form_supports_final_weak(form) then
			weakness = "final-weak"
		elseif rad2 == rad3 and form_supports_geminate(form) then
			weakness = "geminate"
		elseif is_waw_ya(rad2) and form_supports_hollow(form) then
			weakness = "hollow"
		elseif rad1 == W and form == "I" then
			weakness = "assimilated"
		else
			weakness = "sound"
		end
	else
		if is_waw_ya(rad4) then
			weakness = "final-weak"
		else
			weakness = "sound"
		end
	end
	return weakness
end

-- Infer radicals from headword and form. Throw an error if headword is
-- malformed. Returned radicals may contain Latin letters "t", "w" or "y"
-- indicating ambiguous radicals guessed to be tāʾ, wāw or yāʾ respectively.
function export.infer_radicals(headword, form)
	local letters = {}
	-- sub out alif-madda for easier processing
	headword = rsub(headword, AMAD, HAMZA .. ALIF)

	local len = ulen(headword)

	-- extract the headword letters into an array
	for i = 1, len do
		table.insert(letters, usub(headword, i, i))
	end

	-- check that the letter at the given index is the given string, or
	-- is one of the members of the given array
	local function check(index, must)
		local letter = letters[index]
		if type(must) == "string" then
			if letter == nil then
				error("Letter " .. index .. " is nil")
			end
			if letter ~= must then
				error("For form " .. form .. ", letter " .. index ..
					" must be " .. must .. ", not " .. letter)
			end
		elseif not contains(must, letter) then
			error("For form " .. form .. ", radical " .. index ..
				" must be one of " .. table.concat(must, " ") .. ", not " .. letter)
		end
	end

	-- Check that length of headword is within [min, max]
	local function check_len(min, max)
		if len < min then
			error("Not enough letters in headword " .. headword ..
				" for form " .. form .. ", expected at least " .. min)
		elseif len > max then
			error("Too many letters in headword " .. headword ..
				" for form " .. form .. ", expected at most " .. max)
		end
	end

	local quadlit = rmatch(form, "q$")

	-- find first radical, start of second/third radicals, check for
	-- required letters
	local radstart, rad1, rad2, rad3, rad4
	local weakness
	if form == "I" or form == "II" then
		rad1 = letters[1]
		radstart = 2
	elseif form == "III" then
		rad1 = letters[1]
		check(2, {ALIF, WAW}) -- WAW occurs in passive-only verbs
		radstart = 3
	elseif form == "IV" then
		-- this would be alif-madda but we replaced it with hamza-alif above.
		if letters[1] == HAMZA and letters[2] == ALIF then
			rad1 = HAMZA
		else
			check(1, HAMZA_ON_ALIF)
			rad1 = letters[2]
		end
		radstart = 3
	elseif form == "V" then
		check(1, T)
		rad1 = letters[2]
		radstart = 3
	elseif form == "VI" then
		check(1, T)
		if letters[2] == AMAD then
			rad1 = HAMZA
			radstart = 3
		else
			rad1 = letters[2]
			check(3, {ALIF, WAW}) -- WAW occurs in passive-only verbs
			radstart = 4
		end
	elseif form == "VII" then
		check(1, ALIF)
		check(2, N)
		rad1 = letters[3]
		radstart = 4
	elseif form == "VIII" then
		check(1, ALIF)
		rad1 = letters[2]
		if rad1 == T or rad1 == "د" or rad1 == "ث" or rad1 == "ذ" or rad1 == "ط" or rad1 == "ظ" then
			radstart = 3
		elseif rad1 == "ز" then
			check(3, "د")
			radstart = 4
		elseif rad1 == "ص" or rad1 == "ض"  then
			check(3, "ط")
			radstart = 4
		else
			check(3, T)
			radstart = 4
		end
		if rad1 == T then
			-- radical is ambiguous, might be ت or و or ي but doesn't affect
			-- conjugation
			rad1 = "t"
		end
	elseif form == "IX" then
		check(1, ALIF)
		rad1 = letters[2]
		radstart = 3
	elseif form == "X" then
		check(1, ALIF)
		check(2, S)
		check(3, T)
		rad1 = letters[4]
		radstart = 5
	elseif form == "Iq" then
		rad1 = letters[1]
		rad2 = letters[2]
		radstart = 3
	elseif form == "IIq" then
		check(1, T)
		rad1 = letters[2]
		rad2 = letters[3]
		radstart = 4
	elseif form == "IIIq" then
		check(1, ALIF)
		rad1 = letters[2]
		rad2 = letters[3]
		check(4, N)
		radstart = 5
	elseif form == "IVq" then
		check(1, ALIF)
		rad1 = letters[2]
		rad2 = letters[3]
		radstart = 4
	elseif form == "XI" then
		check_len(5, 5)
		check(1, ALIF)
		rad1 = letters[2]
		rad2 = letters[3]
		check(4, ALIF)
		rad3 = letters[5]
		weakness = "sound"
	elseif form == "XII" then
		check(1, ALIF)
		rad1 = letters[2]
		if letters[3] ~= letters[5] then
			error("For form XII, letters 3 and 5 of headword " .. headword ..
				" should be the same")
		end
		check(4, W)
		radstart = 5
	elseif form == "XIII" then
		check_len(5, 5)
		check(1, ALIF)
		rad1 = letters[2]
		rad2 = letters[3]
		check(4, W)
		rad3 = letters[5]
		if rad3 == AMAQ then
			weakness = "final-weak"
		else
			weakness = "sound"
		end
	elseif form == "XIV" then
		check_len(6, 6)
		check(1, ALIF)
		rad1 = letters[2]
		rad2 = letters[3]
		check(4, N)
		rad3 = letters[5]
		if letters[6] == AMAQ then
			check_waw_ya(rad3)
			weakness = "final-weak"
		else
			if letters[5] ~= letters[6] then
				error("For form XIV, letters 5 and 6 of headword " .. headword ..
					" should be the same")
			end
			weakness = "sound"
		end
	elseif form == "XV" then
		check_len(6, 6)
		check(1, ALIF)
		rad1 = letters[2]
		rad2 = letters[3]
		check(4, N)
		rad3 = letters[5]
		if rad3 == Y then
			check(6, ALIF)
		else
			check(6, AMAQ)
		end
		weakness = "sound"
	else
		error("Don't recognize form " .. form)
	end

	-- Process the last two radicals. RADSTART is the index of the
	-- first of the two. If it's nil then all radicals have already been
	-- processed above, and we don't do anything.
	if radstart ~= nil then
		-- there must be one or two letters left
		check_len(radstart, radstart + 1)
		if len == radstart then
			-- if one letter left, then it's a geminate verb
			if form_supports_geminate(form) then
				weakness = "geminate"
				rad2 = letters[len]
				rad3 = letters[len]
			else
				-- oops, geminate verbs not allowed in this form; signal
				-- an error
				check_len(radstart + 1, radstart + 1)
			end
		elseif quadlit then
			-- process last two radicals of a quadriliteral form
			rad3 = letters[radstart]
			rad4 = letters[radstart + 1]
			if rad4 == AMAQ or rad4 == ALIF and rad3 == Y or rad4 == Y then
				-- rad4 can be Y in passive-only verbs
				if form_supports_final_weak(form) then
					weakness = "final-weak"
					-- ambiguous radical; randomly pick wāw as radical (but avoid
					-- two wāws in a row); it could be wāw or yāʾ, but doesn't
					-- affect the conjugation
					rad4 = rad3 == W and "y" or "w"
				else
					error("For headword " .. headword ..
						", last radical is " .. rad4 .. " but form " .. form ..
						" doesn't support final-weak verbs")
				end
			else
				weakness = "sound"
			end
		else
			-- process last two radicals of a triliteral form
			rad2 = letters[radstart]
			rad3 = letters[radstart + 1]
			if form == "I" and (is_waw_ya(rad3) or rad3 == ALIF or rad3 == AMAQ) then
				-- check for final-weak form I verb. It can end in tall alif
				-- (rad3 = wāw) or alif maqṣūra (rad3 = yāʾ) or a wāw or yāʾ
				-- (with a past vowel of i or u, e.g. nasiya/yansā "forget"
				-- or with a passive-only verb).
				if rad1 == W then
					weakness = "assimilated+final-weak"
				else
					weakness = "final-weak"
				end
				if rad3 == ALIF then
					rad3 = W
				elseif rad3 == AMAQ then
					rad3 = Y
				else
					-- ambiguous radical; randomly pick wāw as radical (but
					-- avoid two wāws); it could be wāw or yāʾ, but doesn't
					-- affect the conjugation
					rad3 = (rad1 == W or rad2 == W) and "y" or "w" -- ambiguous
				end
		elseif rad3 == AMAQ or rad2 == Y and rad3 == ALIF or rad3 == Y then
				-- rad3 == Y happens in passive-only verbs
				if form_supports_final_weak(form) then
					weakness = "final-weak"
				else
					error("For headword " .. headword ..
						", last radical is " .. rad3 .. " but form " .. form ..
						" doesn't support final-weak verbs")
				end
				-- ambiguous radical; randomly pick wāw as radical (but avoid
				-- two wāws); it could be wāw or yāʾ, but doesn't affect the
				-- conjugation
				rad3 = (rad1 == W or rad2 == W) and "y" or "w"
			elseif rad2 == ALIF then
				if form_supports_hollow(form) then
					weakness = "hollow"
					-- ambiguous radical; could be wāw or yāʾ; if form I,
					-- it's critical to get this right, and the caller checks
					-- for this situation, attempts to infer radical from
					-- non-past vowel, and if that fails, signals an error
					rad2 = "w"
				else
					error("For headword " .. headword ..
						", second radical is alif but form " .. form ..
						" doesn't support hollow verbs")
				end
			elseif form == "I" and rad1 == W then
				weakness = "assimilated"
			elseif rad2 == rad3 and (form == "III" or form == "VI") then
				weakness = "geminate"
			else
				weakness = "sound"
			end
		end
	end

	-- convert radicals to canonical form (handle various hamza varieties and
	-- check for misplaced alif or alif maqṣūra; legitimate cases of these
	-- letters are handled above)
	local function convert(rad, index)
		if rad == HAMZA_ON_ALIF or rad == HAMZA_UNDER_ALIF or
			rad == HAMZA_ON_W or rad == HAMZA_ON_Y then
			return HAMZA
		elseif rad == AMAQ then
			error("For form " .. form .. ", headword " .. headword ..
				", radical " .. index .. " must not be alif maqṣūra")
		elseif rad == ALIF then
			error("For form " .. form .. ", headword " .. headword ..
				", radical " .. index .. " must not be alif")
		else
			return rad
		end
	end
	rad1 = convert(rad1, 1)
	rad2 = convert(rad2, 2)
	rad3 = convert(rad3, 3)
	rad4 = convert(rad4, 4)

	return weakness, rad1, rad2, rad3, rad4
end

-- given form, weakness and radicals, check to make sure the radicals present
-- are allowable for the weakness. Hamzas on alif/wāw/yāʾ seats are never
-- allowed (should always appear as hamza-on-the-line), and various weaknesses
-- have various strictures on allowable consonants.
function check_radicals(form, weakness, rad1, rad2, rad3, rad4)
	local function hamza_check(index, rad)
		if rad == HAMZA_ON_ALIF or rad == HAMZA_UNDER_ALIF or
			rad == HAMZA_ON_W or rad == HAMZA_ON_Y then
			error("Radical " .. index .. " is " .. rad .. " but should be ء (hamza on the line)")
		end
	end
	local function check_waw_ya(index, rad)
		if rad ~= W and rad ~= Y then
			error("Radical " .. index .. " is " .. rad .. " but should be و or ي")
		end
	end
	local function check_not_waw_ya(index, rad)
		if rad == W or rad == Y then
			error("In a sound verb, radical " .. index .. " should not be و or ي")
		end
	end
	hamza_check(rad1)
	hamza_check(rad2)
	hamza_check(rad3)
	hamza_check(rad4)
	if weakness == "assimilated" or weakness == "assimilated+final-weak" then
		if rad1 ~= W then
			error("Radical 1 is " .. rad1 .. " but should be و")
		end
	-- don't check that non-assimilated form I verbs don't have wāw as their
	-- first radical because some form-I verbs exist where a first-radical wāw
	-- behaves as sound, e.g. wajuha yawjuhu "to be distinguished".
	end
	if weakness == "final-weak" or weakness == "assimilated+final-weak" then
		if rad4 then
			check_waw_ya(4, rad4)
		else
			check_waw_ya(3, rad3)
		end
	elseif form_supports_final_weak(form) then
		-- non-final-weak verbs cannot have weak final radical if there's a corresponding
		-- final-weak verb category. I think this is safe. We may have problems with
		-- ḥayya/ḥayiya yaḥyā if we treat it as a geminate verb.
		if rad4 then
			check_not_waw_ya(4, rad4)
		else
			check_not_waw_ya(3, rad3)
		end
	end
	if weakness == "hollow" then
		check_waw_ya(2, rad2)
	-- don't check that non-hollow verbs in forms that support hollow verbs
	-- don't have wāw or yāʾ as their second radical because some verbs exist
	-- where a middle-radical wāw/yāʾ behaves as sound, e.g. form-VIII izdawaja
	-- "to be in pairs".
	end
	if weakness == "geminate" then
		if rad4 then
			error("Internal error. No geminate quadrilaterals, should not be seen.")
		end
		if rad2 ~= rad3 then
			error("Weakness is geminate; radical 3 is " .. rad3 .. " but should be same as radical 2 " .. rad2 .. ".")
		end
	elseif form_supports_geminate(form) then
		-- non-geminate verbs cannot have second and third radical same if there's
		-- a corresponding geminate verb category. I think this is safe. We
		-- don't fuss over double wāw or double yāʾ because this could legitimately
		-- be a final-weak verb with middle wāw/yāʾ, treated as sound.
		if rad4 then
			error("Internal error. No quadrilaterals should support geminate verbs.")
		end
		if rad2 == rad3 and not is_waw_ya(rad2) then
			error("Weakness is '" .. weakness .. "'; radical 2 and 3 are same at " .. rad2 .. " but should not be; consider making weakness 'geminate'")
		end
	end
end

function initialize_categories(data, form, weakness, rad1, rad2, rad3, rad4)
	-- We have to distinguish weakness by form and weakness by conjugation.
	-- Weakness by form merely indicates the presence of weak letters in
	-- certain positions in the radicals. Weakness by conjugation is related
	-- to how the verbs are conjugated. For example, form-II verbs that are
	-- "hollow by form" (middle radical is wāw or yāʾ) are conjugated as sound
	-- verbs. Another example: form-I verbs with initial wāw are "assimilated
	-- by form" and most are assimilated by conjugation as well, but a few
	-- are sound by conjugation, e.g. wajuha yawjuhu "to be distinguished"
	-- (rather than wajuha yajuhu); similarly for some hollow-by-form verbs
	-- in various forms, e.g. form VIII izdawaja yazdawiju "to be in pairs"
	-- (rather than izdāja yazdāju). When most references say just plain
	-- "hollow" or "assimilated" or whatever verbs, they mean by form, so
	-- we name the categories appropriately, where e.g. "Arabic hollow verbs"
	-- means by form, "Arabic hollow verbs by conjugation" means by
	-- conjugation.
	table.insert(data.categories, "Arabic form-" .. form .. " verbs")
	table.insert(data.headword_categories, "Arabic form-" .. form .. " verbs")
	table.insert(data.categories, "Arabic " .. weakness .. " verbs by conjugation")
	table.insert(data.headword_categories, "Arabic " .. weakness .. " verbs by conjugation")
	if form_is_quadriliteral(form) then
		table.insert(data.categories, "Arabic verbs with quadriliteral roots")
		table.insert(data.headword_categories, "Arabic verbs with quadriliteral roots")
	end
	local formweak = {}
	if is_waw_ya(rad1) then
		table.insert(formweak, "assimilated")
	end
	if is_waw_ya(rad2) and rad4 == nil then
		table.insert(formweak, "hollow")
	end
	if is_waw_ya(rad4) or rad4 == nil and is_waw_ya(rad3) then
		table.insert(formweak, "final-weak")
	end
	if rad4 == nil and rad2 == rad3 then
		table.insert(formweak, "geminate")
	end
	if rad1 == HAMZA or rad2 == HAMZA or rad3 == HAMZA or rad4 == HAMZA then
		table.insert(formweak, "hamzated")
	end
	if #formweak == 0 then
		table.insert(formweak, "sound")
	end
	for _, fw in ipairs(formweak) do
		table.insert(data.categories, "Arabic " .. fw .. " form-" .. form .. " verbs")
		table.insert(data.categories, "Arabic " .. fw .. " verbs")
		table.insert(data.headword_categories, "Arabic " .. fw .. " form-" .. form .. " verbs")
		table.insert(data.headword_categories, "Arabic " .. fw .. " verbs")
	end


	local function radical_is_ambiguous(rad)
		return rad == "t" or rad == "w" or rad == "y"
	end
	local function radical_is_weak(rad)
		return rad == W or rad == Y or rad == HAMZA
	end

	local ur1, ur2, ur3, ur4 =
		data.unreg_rad1, data.unreg_rad2, data.unreg_rad3, data.unreg_rad4
	-- Create headword categories based on the radicals. Do the following before
	-- converting the Latin radicals into Arabic ones so we distinguish
	-- between ambiguous and non-ambiguous radicals.
	if radical_is_ambiguous(ur1) or radical_is_ambiguous(ur2) or
			radical_is_ambiguous(ur3) or radical_is_ambiguous(ur4) then
		table.insert(data.headword_categories,
			"Arabic verbs with ambiguous radicals")
	end
	if radical_is_weak(ur1) then
		table.insert(data.headword_categories, "Arabic form-" .. form ..
			" verbs with " .. ur1 .. " as first radical")
	end
	if radical_is_weak(ur2) then
		table.insert(data.headword_categories, "Arabic form-" .. form ..
			" verbs with " .. ur2 .. " as second radical")
	end
	if radical_is_weak(ur3) then
		table.insert(data.headword_categories, "Arabic form-" .. form ..
			" verbs with " .. ur3 .. " as third radical")
	end
	if radical_is_weak(ur4) then
		table.insert(data.headword_categories, "Arabic form-" .. form ..
			" verbs with " .. ur4 .. " as fourth radical")
	end

	if data.passive == "only" then
		table.insert(data.categories, "Arabic passive-only verbs")
		table.insert(data.categories, "Arabic verbs with full passive")
	elseif data.passive == "only-impers" then
		table.insert(data.categories, "Arabic passive-only verbs")
		table.insert(data.categories, "Arabic verbs with impersonal passive")
	elseif data.passive == "impers" then
		table.insert(data.categories, "Arabic verbs with impersonal passive")
	elseif data.passive then
		table.insert(data.categories, "Arabic verbs with full passive")
	else
		table.insert(data.categories, "Arabic verbs with no passive")
	end
end

-------------------------------------------------------
-- Conjugation functions for specific conjugation types
-------------------------------------------------------

-- Check that the past or non-past vowel is a, i, or u. VOWEL is the vowel to
-- check and VTYPE indicates whether it's past or non-past and is used in
-- the error message.
function check_aiu(vtype, vowel)
	if vowel ~= "a" and vowel ~= "i" and vowel ~= "u" then
		error(vtype .. " vowel '" .. vowel .. "' should be a, i, or u")
	end
end

-- Is radical wāw (و) or yāʾ (ي)?
function is_waw_ya(rad)
	return rad == W or rad == Y
end

-- Check that radical is wāw (و) or yāʾ (ي), error if not
function check_waw_ya(rad)
	if not is_waw_ya(rad) then
		error("Expecting weak radical: '" .. rad .. "' should be " .. W .. " or " .. Y)
	end
end

-- Is radical guttural? This favors a non-past vowel of "a"
function is_guttural(rad)
	return rad == HAMZA or rad == "ه" or rad == "ع" or rad == "ح"
end

-- Derive default non-past vowel from past vowel. Most common possibilities are
-- a/u, a/i, a/a if rad2 or rad3 are guttural, i/a, u/u. We choose a/u over a/i.
function nonpast_from_past_vowel(past_vowel, rad2, rad3)
	return past_vowel == "i" and "a" or past_vowel == "u" and "u" or
		(is_guttural(rad2) or is_guttural(rad3)) and "a" or "u"
end

-- determine the imperative vowel based on non-past vowel
function imper_vowel_from_nonpast(nonpast_vowel)
	if nonpast_vowel == "a" or nonpast_vowel == "i" then
		return "i"
	elseif nonpast_vowel == "u" then
		return "u"
	else
		error("Non-past vowel '" .. nonpast_vowel .. "' isn't a, i, or u, should have been caught earlier")
	end
end

-- Convert short vowel to equivalent long vowel (a -> alif, u -> wāw, i -> yāʾ).
function short_to_long_vowel(vowel)
	if vowel == A then return ALIF
	elseif vowel == I then return Y
	elseif vowel == U then return W
	else
		error("Vowel '" .. vowel .. "' isn't a, i, or u, should have been caught earlier")
	end
end

-- Implement form-I sound or assimilated verb. ASSIMILATED is true for
-- assimilated verbs.
local function make_form_i_sound_assimilated_verb(data, args, rad1, rad2, rad3,
		past_vowel, nonpast_vowel, assimilated)
	-- need to provide two vowels - past and non-past
	past_vowel = past_vowel or "a"
	nonpast_vowel = nonpast_vowel or nonpast_from_past_vowel(past_vowel, rad2, rad3)
	check_aiu("past", past_vowel)
	check_aiu("non-past", nonpast_vowel)

	-- Verbal nouns (maṣādir) for form I are unpredictable and have to be supplied
	insert_verbal_noun(data, args, {})

	-- past and non-past stems, active and passive
	local past_stem = rad1 .. A .. rad2 .. dia[past_vowel] .. rad3
	local nonpast_stem = assimilated and rad2 .. dia[nonpast_vowel] .. rad3 or
		rad1 .. SK .. rad2 .. dia[nonpast_vowel] .. rad3
	local ps_past_stem = rad1 .. U .. rad2 .. I .. rad3
	local ps_nonpast_stem = rad1 .. SK .. rad2 .. A .. rad3

	-- determine the imperative vowel based on non-past vowel
	local imper_vowel = imper_vowel_from_nonpast(nonpast_vowel)

	-- imperative stem
	-- check for irregular verb with reduced imperative (أَخَذَ or أَكَلَ or أَمَرَ)
	local reducedimp = reduced_imperative_verb(rad1, rad2, rad3)
	if reducedimp then
		data.irregular = true
	end
	local imper_stem_suffix = rad2 .. dia[nonpast_vowel] .. rad3
	local imper_stem_base = (assimilated or reducedimp) and "" or
		ALIF .. dia[imper_vowel] ..
		(rad1 == HAMZA and short_to_long_vowel(dia[imper_vowel]) or rad1 .. SK)
	local imper_stem = imper_stem_base .. imper_stem_suffix

	-- make parts
	make_sound_verb(data, past_stem, ps_past_stem, nonpast_stem,
		ps_nonpast_stem, imper_stem, "a")

	-- Check for irregular verb سَأَلَ with alternative jussive and imperative.
	-- Calling this after make_sound_verb() adds additional entries to the
	-- paradigm parts.
	if saal_radicals(rad1, rad2, rad3) then
		data.irregular = true
		nonpast_1stem_conj(data, "juss", "a", "سَل")
		nonpast_1stem_conj(data, "ps-juss", "u", "سَل")
		make_1stem_imperative(data, "سَل")
	end

	-- active participle
	insert_part(data, "ap", rad1 .. AA .. rad2 .. I .. rad3 .. UNS)
	-- passive participle
	insert_part(data, "pp", MA .. rad1 .. SK .. rad2 .. U .. "و" .. rad3 .. UNS)
end

conjugations["I-sound"] = function(data, args, rad1, rad2, rad3,
		past_vowel, nonpast_vowel)
	make_form_i_sound_assimilated_verb(data, args, rad1, rad2, rad3,
		past_vowel, nonpast_vowel, false)
end

conjugations["I-assimilated"] = function(data, args, rad1, rad2, rad3,
		past_vowel, nonpast_vowel)
	make_form_i_sound_assimilated_verb(data, args, rad1, rad2, rad3,
		past_vowel, nonpast_vowel, "assimilated")
end

local function make_form_i_hayy_verb(data, args)
	-- Verbal nouns (maṣādir) for form I are unpredictable and have to be supplied
	insert_verbal_noun(data, args, {})

	data.irregular = true

	-- past and non-past stems, active and passive, and imperative stem
	local past_c_stem = "حَيِي"
	local past_v_stem_long = past_c_stem
	local past_v_stem_short = "حَيّ"
	local ps_past_c_stem = "حُيِي"
	local ps_past_v_stem_long = ps_past_c_stem
	local ps_past_v_stem_short = "حُيّ"

	local nonpast_stem = "حْي"
	local ps_nonpast_stem = nonpast_stem
	local imper_stem = "اِ" .. nonpast_stem

	-- make parts

	past_2stem_conj(data, "perf", "-", past_c_stem)
	past_2stem_conj(data, "ps-perf", "-", ps_past_c_stem)
	local variant = args["variant"]
	if variant == "short" or variant == "both" then
		past_2stem_conj(data, "perf", past_v_stem_short, "-")
		past_2stem_conj(data, "ps-perf", ps_past_v_stem_short, "-")
	end
	function inflect_long_variant(tense, long_stem, short_stem)
		inflect_tense_1(data, tense, "",
			{long_stem, long_stem, long_stem, long_stem, short_stem},
			{past_endings[4], past_endings[5], past_endings[7], past_endings[8],
			 past_endings[12]},
			{"3sm", "3sf", "3dm", "3df", "3pm"})
	end
	if variant == "long" or variant == "both" then
		inflect_long_variant("perf", past_v_stem_long, past_v_stem_short)
		inflect_long_variant("ps-perf", ps_past_v_stem_long, ps_past_v_stem_short)
	end

	nonpast_1stem_conj(data, "impf", "a", nonpast_stem, indic_endings_aa)
	nonpast_1stem_conj(data, "subj", "a", nonpast_stem, subj_endings_aa)
	nonpast_1stem_conj(data, "juss", "a", nonpast_stem, juss_endings_aa)
	nonpast_1stem_conj(data, "ps-impf", "u", ps_nonpast_stem, indic_endings_aa)
	nonpast_1stem_conj(data, "ps-subj", "u", ps_nonpast_stem, subj_endings_aa)
	nonpast_1stem_conj(data, "ps-juss", "u", ps_nonpast_stem, juss_endings_aa)
	inflect_tense_impr(data, imper_stem, impr_endings_aa)

	-- active participle apparently does not exist for this verb
	insert_part(data, "ap", {})
	-- passive participle apparently does not exist for this verb
	insert_part(data, "pp", {})
end

-- Implement form-I final-weak assimilated+final-weak verb. ASSIMILATED is true
-- for assimilated verbs.
local function make_form_i_final_weak_verb(data, args, rad1, rad2, rad3,
		past_vowel, nonpast_vowel, assimilated)

	-- حَيَّ or حَيِيَ is weird enough that we handle it as a separate function
	if hayy_radicals(rad1, rad2, rad3) then
		make_form_i_hayy_verb(data, args)
		return
	end

	-- need to provide two vowels - past and non-past
	local past_vowel = past_vowel or "a"
	local nonpast_vowel = nonpast_vowel or past_vowel == "i" and "a" or
		past_vowel == "u" and "u" or rad3 == Y and "i" or "u"
	check_aiu("past", past_vowel)
	check_aiu("non-past", nonpast_vowel)

	-- Verbal nouns (maṣādir) for form I are unpredictable and have to be supplied
	insert_verbal_noun(data, args, {})

	-- past and non-past stems, active and passive, and imperative stem
	local past_stem = rad1 .. A .. rad2
	local ps_past_stem = rad1 .. U .. rad2
	local nonpast_stem, ps_nonpast_stem, imper_stem
	if raa_radicals(rad1, rad2, rad3) then
		data.irregular = true
		nonpast_stem = rad1
		ps_nonpast_stem = rad1
		imper_stem = rad1
	else
		ps_nonpast_stem = rad1 .. SK .. rad2
		if assimilated then
			nonpast_stem = rad2
			imper_stem = rad2
		else
			nonpast_stem = ps_nonpast_stem
			-- determine the imperative vowel based on non-past vowel
			local imper_vowel = imper_vowel_from_nonpast(nonpast_vowel)
			imper_stem =  ALIF .. dia[imper_vowel] ..
				(rad1 == HAMZA and short_to_long_vowel(dia[imper_vowel])
					or rad1 .. SK) ..
				rad2
		end
	end

	-- make parts
	local past_suffs =
		rad3 == Y and past_vowel == "a" and past_endings_ay or
		rad3 == W and past_vowel == "a" and past_endings_aw or
		past_vowel == "i" and past_endings_ii or
		past_endings_uu
	local indic_suffs, subj_suffs, juss_suffs, impr_suffs
	if nonpast_vowel == "a" then
		indic_suffs = indic_endings_aa
		subj_suffs = subj_endings_aa
		juss_suffs = juss_endings_aa
		impr_suffs = impr_endings_aa
	elseif nonpast_vowel == "i" then
		indic_suffs = indic_endings_ii
		subj_suffs = subj_endings_ii
		juss_suffs = juss_endings_ii
		impr_suffs = impr_endings_ii
	else
		assert(nonpast_vowel == "u")
		indic_suffs = indic_endings_uu
		subj_suffs = subj_endings_uu
		juss_suffs = juss_endings_uu
		impr_suffs = impr_endings_uu
	end
	make_final_weak_verb(data, past_stem, ps_past_stem, nonpast_stem,
		ps_nonpast_stem, imper_stem, past_suffs, indic_suffs,
		subj_suffs, juss_suffs, impr_suffs, "a")

	-- active participle
	insert_part(data, "ap", rad1 .. AA .. rad2 .. IN)
	-- passive participle
	insert_part(data, "pp", MA .. rad1 .. SK .. rad2 ..
		(rad3 == Y and II or UU) .. SH .. UNS)
end

conjugations["I-final-weak"] = function(data, args, rad1, rad2, rad3,
		past_vowel, nonpast_vowel)
	make_form_i_final_weak_verb(data, args, rad1, rad2, rad3,
		past_vowel, nonpast_vowel, false)
end

conjugations["I-assimilated+final-weak"] = function(data, args, rad1, rad2, rad3,
		past_vowel, nonpast_vowel)
	make_form_i_final_weak_verb(data, args, rad1, rad2, rad3,
		past_vowel, nonpast_vowel, "assimilated")
end

conjugations["I-hollow"] = function(data, args, rad1, rad2, rad3,
		past_vowel, nonpast_vowel)
	-- need to specify up to two vowels, past and non-past
	-- default past vowel to short equivalent of middle radical
	local past_vowel = past_vowel or rad2 == Y and "i" or "u"
	-- default non-past vowel to past vowel, unless it's "a", in which case
	-- we default to short equivalent of middle radical
	local nonpast_vowel = nonpast_vowel or past_vowel ~= "a" and past_vowel or
		rad2 == Y and "i" or "u"
	check_aiu("past", past_vowel)
	check_aiu("non-past", nonpast_vowel)
	-- Formerly we signaled an error when past_vowel is "a" but that seems
	-- too harsh. We can interpret a past vowel of "a" as meaning to use the
	-- non-past vowel in forms requiring a short vowel. If the non-past vowel
	-- is "a" then the past vowel can only be "i" (e.g. in nāma yanāmu with
	-- first singular past of nimtu).
	if past_vowel == "a" then
		-- error("For form I hollow, past vowel cannot be 'a'")
		past_vowel = nonpast_vowel == "a" and "i" or nonpast_vowel
	end
	local lengthened_nonpast = nonpast_vowel == "u" and UU or
		nonpast_vowel == "i" and II or AA

	-- Verbal nouns (maṣādir) for form I are unpredictable and have to be supplied
	insert_verbal_noun(data, args, {})

	-- active past stems - vowel (v) and consonant (c)
	local past_v_stem = rad1 .. AA .. rad3
	local past_c_stem = rad1 .. dia[past_vowel] .. rad3

	-- active non-past stems - vowel (v) and consonant (c)
	local nonpast_v_stem = rad1 .. lengthened_nonpast .. rad3
	local nonpast_c_stem = rad1 .. dia[nonpast_vowel] .. rad3

	-- passive past stems - vowel (v) and consonant (c)
	-- 'ufīla, 'ufiltu
	local ps_past_v_stem = rad1 .. II .. rad3
	local ps_past_c_stem = rad1 .. I .. rad3

	-- passive non-past stems - vowel (v) and consonant (c)
	-- yufāla/yufalna
	-- stem is built differently but conjugation is identical to sound verbs
	local ps_nonpast_v_stem = rad1 .. AA .. rad3
	local ps_nonpast_c_stem = rad1 .. A .. rad3

	-- imperative stem
	local imper_v_stem = nonpast_v_stem
	local imper_c_stem = nonpast_c_stem

	-- make parts
	make_hollow_geminate_verb(data, past_v_stem, past_c_stem, ps_past_v_stem,
		ps_past_c_stem, nonpast_v_stem, nonpast_c_stem, ps_nonpast_v_stem,
		ps_nonpast_c_stem, imper_v_stem, imper_c_stem, "a", false)

	-- active participle
	insert_part(data, "ap", rad3 == HAMZA and rad1 .. AA .. HAMZA .. IN or
		rad1 .. AA .. HAMZA .. I .. rad3 .. UNS)
	-- passive participle
	insert_part(data, "pp", MA .. rad1 .. (rad2 == Y and II or UU) .. rad3 .. UNS)
end

conjugations["I-geminate"] = function(data, args, rad1, rad2, rad3,
		past_vowel, nonpast_vowel)
	-- need to specify two vowels, past and non-past
	local past_vowel = past_vowel or "a"
	local nonpast_vowel = nonpast_vowel or nonpast_from_past_vowel(past_vowel, rad2, rad3)

	-- Verbal nouns (maṣādir) for form I are unpredictable and have to be supplied
	insert_verbal_noun(data, args, {})

	-- active past stems - vowel (v) and consonant (c)
	local past_v_stem = rad1 .. A .. rad2 .. SH
	local past_c_stem = rad1 .. A .. rad2 .. dia[past_vowel] .. rad2

	-- active non-past stems - vowel (v) and consonant (c)
	local nonpast_v_stem = rad1 .. dia[nonpast_vowel] .. rad2 .. SH
	local nonpast_c_stem = rad1 .. SK .. rad2 .. dia[nonpast_vowel] .. rad2

	-- passive past stems - vowel (v) and consonant (c)
	-- dulla/dulilta
	local ps_past_v_stem = rad1 .. U .. rad2 .. SH
	local ps_past_c_stem = rad1 .. U .. rad2 .. I .. rad2

	-- passive non-past stems - vowel (v) and consonant (c)
	--yudallu/yudlalna
	-- stem is built differently but conjugation is identical to sound verbs
	local ps_nonpast_v_stem = rad1 .. A .. rad2 .. SH
	local ps_nonpast_c_stem = rad1 .. SK .. rad2 .. A .. rad2

	-- determine the imperative vowel based on non-past vowel
	local imper_vowel = imper_vowel_from_nonpast(nonpast_vowel)

	-- imperative stem
	local imper_v_stem = rad1 .. dia[nonpast_vowel] .. rad2 .. SH
	local imper_c_stem = ALIF .. dia[imper_vowel] ..
		(rad1 == HAMZA and short_to_long_vowel(dia[imper_vowel]) or rad1 .. SK) ..
		rad2 .. dia[nonpast_vowel] .. rad2

	-- make parts
	make_hollow_geminate_verb(data, past_v_stem, past_c_stem, ps_past_v_stem,
		ps_past_c_stem, nonpast_v_stem, nonpast_c_stem, ps_nonpast_v_stem,
		ps_nonpast_c_stem, imper_v_stem, imper_c_stem, "a", "geminate")

	-- active participle
	insert_part(data, "ap", rad1 .. AA .. rad2 .. SH .. UNS)
	-- passive participle
	insert_part(data, "pp", MA .. rad1 .. SK .. rad2 .. U .. "و" .. rad2 .. UNS)
end

-- Make form II or V sound or final-weak verb. Final-weak verbs are identified
-- by RAD3 = nil, and FORM distinguishes II from V.
function make_form_ii_v_sound_final_weak_verb(data, args, rad1, rad2, rad3, form)
	local final_weak = rad3 == nil
	local vn = form == "V" and
		"تَ" .. rad1 .. A .. rad2 .. SH ..
			(final_weak and IN or U .. rad3 .. UNS) or
		"تَ" .. rad1 .. SK .. rad2 .. I .. "ي" ..
			(final_weak and AH or rad3) .. UNS
	local ta_pref = form == "V" and "تَ" or ""
	local tu_pref = form == "V" and "تُ" or ""

	-- various stem bases
	local past_stem_base = ta_pref .. rad1 .. A .. rad2 .. SH
	local nonpast_stem_base = past_stem_base
	local ps_past_stem_base = tu_pref .. rad1 .. U .. rad2 .. SH

	-- make parts
	make_augmented_sound_final_weak_verb(data, args, rad3,
		past_stem_base, nonpast_stem_base, ps_past_stem_base, vn, form)
end

conjugations["II-sound"] = function(data, args, rad1, rad2, rad3)
	make_form_ii_v_sound_final_weak_verb(data, args, rad1, rad2, rad3, "II")
end

conjugations["II-final-weak"] = function(data, args, rad1, rad2, rad3)
	make_form_ii_v_sound_final_weak_verb(data, args, rad1, rad2, nil, "II")
end

-- Make form III or VI sound or final-weak verb. Final-weak verbs are identified
-- by RAD3 = nil, and FORM distinguishes III from VI.
function make_form_iii_vi_sound_final_weak_verb(data, args, rad1, rad2, rad3, form)
	local final_weak = rad3 == nil
	local vn = form == "VI" and
		"تَ" .. rad1 .. AA .. rad2 ..
			(final_weak and IN or U .. rad3 .. UNS) or
		{MU .. rad1 .. AA .. rad2 .. (final_weak and AAH or A .. rad3 .. AH) .. UNS,
			rad1 .. I .. rad2 .. AA .. (final_weak and HAMZA or rad3) .. UNS}
	local ta_pref = form == "VI" and "تَ" or ""
	local tu_pref = form == "VI" and "تُ" or ""

	-- various stem bases
	local past_stem_base = ta_pref .. rad1 .. AA .. rad2
	local nonpast_stem_base = past_stem_base
	local ps_past_stem_base = tu_pref .. rad1 .. UU .. rad2

	-- make parts
	make_augmented_sound_final_weak_verb(data, args, rad3,
		past_stem_base, nonpast_stem_base, ps_past_stem_base, vn, form)
end

conjugations["III-sound"] = function(data, args, rad1, rad2, rad3)
	make_form_iii_vi_sound_final_weak_verb(data, args, rad1, rad2, rad3, "III")
end

conjugations["III-final-weak"] = function(data, args, rad1, rad2, rad3)
	make_form_iii_vi_sound_final_weak_verb(data, args, rad1, rad2, nil, "III")
end

-- Make form III or VI geminate verb. FORM distinguishes III from VI.
function make_form_iii_vi_geminate_verb(data, args, rad1, rad2, form)
	-- alternative verbal noun فِعَالٌ will be inserted when we add sound parts below
	local vn = form == "VI" and
		{"تَ" .. rad1 .. AA .. rad2 .. SH .. UNS} or
		{MU .. rad1 .. AA .. rad2 .. SH .. AH .. UNS}
	local ta_pref = form == "VI" and "تَ" or ""
	local tu_pref = form == "VI" and "تُ" or ""

	-- various stem bases
	local past_stem_base = ta_pref .. rad1 .. AA
	local nonpast_stem_base = past_stem_base
	local ps_past_stem_base = tu_pref .. rad1 .. UU

	-- make parts
	make_augmented_geminate_verb(data, args, rad2,
		past_stem_base, nonpast_stem_base, ps_past_stem_base, vn, form)

	-- Also add alternative sound (non-compressed) parts. This will lead to
	-- some duplicate entries, but they are removed in get_spans().
	make_form_iii_vi_sound_final_weak_verb(data, args, rad1, rad2, rad2, form)
end

conjugations["III-geminate"] = function(data, args, rad1, rad2, rad3)
	make_form_iii_vi_geminate_verb(data, args, rad1, rad2, "III")
end

-- Make form IV sound or final-weak verb. Final-weak verbs are identified
-- by RAD3 = nil.
function make_form_iv_sound_final_weak_verb(data, args, rad1, rad2, rad3)
	local final_weak = rad3 == nil

	-- core of stem base, minus stem prefixes
	local stem_core

	-- check for irregular verb أَرَى
	if raa_radicals(rad1, rad2, final_weak and Y or rad3) then
		data.irregular = true
		stem_core = rad1
	else
		stem_core =	rad1 .. SK .. rad2
	end

	-- verbal noun
	local vn = HAMZA .. I .. stem_core .. AA ..
		(final_weak and HAMZA or rad3) .. UNS

	-- various stem bases
	local past_stem_base = HAMZA .. A .. stem_core
	local nonpast_stem_base = stem_core
	local ps_past_stem_base = HAMZA .. U .. stem_core

	-- make parts
	make_augmented_sound_final_weak_verb(data, args, rad3,
		past_stem_base, nonpast_stem_base, ps_past_stem_base, vn, "IV")
end

conjugations["IV-sound"] = function(data, args, rad1, rad2, rad3)
	make_form_iv_sound_final_weak_verb(data, args, rad1, rad2, rad3)
end

conjugations["IV-final-weak"] = function(data, args, rad1, rad2, rad3)
	make_form_iv_sound_final_weak_verb(data, args, rad1, rad2, nil)
end

conjugations["IV-hollow"] = function(data, args, rad1, rad2, rad3)
	-- verbal noun
	local vn = HAMZA .. I .. rad1 .. AA .. rad3 .. AH .. UNS

	-- various stem bases
	local past_stem_base = HAMZA .. A .. rad1
	local nonpast_stem_base = rad1
	local ps_past_stem_base = HAMZA .. U .. rad1

	-- make parts
	make_augmented_hollow_verb(data, args, rad3,
		past_stem_base, nonpast_stem_base, ps_past_stem_base, vn, "IV")
end

conjugations["IV-geminate"] = function(data, args, rad1, rad2, rad3)
	local vn = HAMZA .. I .. rad1 .. SK .. rad2 .. AA .. rad2 .. UNS

	-- various stem bases
	local past_stem_base = HAMZA .. A .. rad1
	local nonpast_stem_base = rad1
	local ps_past_stem_base = HAMZA .. U .. rad1

	-- make parts
	make_augmented_geminate_verb(data, args, rad2,
		past_stem_base, nonpast_stem_base, ps_past_stem_base, vn, "IV")
end

conjugations["V-sound"] = function(data, args, rad1, rad2, rad3)
	make_form_ii_v_sound_final_weak_verb(data, args, rad1, rad2, rad3, "V")
end

conjugations["V-final-weak"] = function(data, args, rad1, rad2, rad3)
	make_form_ii_v_sound_final_weak_verb(data, args, rad1, rad2, nil, "V")
end

conjugations["VI-sound"] = function(data, args, rad1, rad2, rad3)
	make_form_iii_vi_sound_final_weak_verb(data, args, rad1, rad2, rad3, "VI")
end

conjugations["VI-final-weak"] = function(data, args, rad1, rad2, rad3)
	make_form_iii_vi_sound_final_weak_verb(data, args, rad1, rad2, nil, "VI")
end

conjugations["VI-geminate"] = function(data, args, rad1, rad2, rad3)
	make_form_iii_vi_geminate_verb(data, args, rad1, rad2, "VI")
end

-- Make a verbal noun of the general form that applies to forms VII and above.
-- RAD12 is the first consonant cluster (after initial اِ) and RAD34 is the
-- second consonant cluster. RAD5 is the final consonant, or nil for final-weak
-- verbs.
function high_form_verbal_noun(rad12, rad34, rad5)
	return "اِ" .. rad12 .. I .. rad34 .. AA ..
		(rad5 == nil and HAMZA or rad5) .. UNS
end

-- Populate a sound or final-weak verb for any of the various high-numbered
-- augmented forms (form VII and up) that have up to 5 consonants in two
-- clusters in the stem and the same pattern of vowels between.
-- Some of these consonants in certain verb parts are w's, which leads to apparent
-- anomalies in certain stems of these parts, but these anomalies are handled
-- automatically in postprocessing, where we resolve sequences of iwC -> īC,
-- uwC -> ūC, w + sukūn + w -> w + shadda.
--
-- RAD12 is the first consonant cluster (after initial اِ) and RAD34 is the
-- second consonant cluster. RAD5 is the final consonant, or nil for final-weak
-- verbs.
function make_high_form_sound_final_weak_verb(data, args, rad12, rad34, rad5, form)
	local final_weak = rad5 == nil
	local vn = high_form_verbal_noun(rad12, rad34, rad5)

	-- various stem bases
	local nonpast_stem_base = rad12 .. A .. rad34
	local past_stem_base = "اِ" .. nonpast_stem_base
	local ps_past_stem_base = "اُ" .. rad12 .. U .. rad34

	-- make parts
	make_augmented_sound_final_weak_verb(data, args, rad5,
		past_stem_base, nonpast_stem_base, ps_past_stem_base, vn, form)
end

-- Make form VII sound or final-weak verb. Final-weak verbs are identified
-- by RAD3 = nil.
function make_form_vii_sound_final_weak_verb(data, args, rad1, rad2, rad3)
	make_high_form_sound_final_weak_verb(data, args, "نْ" .. rad1, rad2, rad3, "VII")
end

conjugations["VII-sound"] = function(data, args, rad1, rad2, rad3)
	make_form_vii_sound_final_weak_verb(data, args, rad1, rad2, rad3)
end

conjugations["VII-final-weak"] = function(data, args, rad1, rad2, rad3)
	make_form_vii_sound_final_weak_verb(data, args, rad1, rad2, nil)
end

conjugations["VII-hollow"] = function(data, args, rad1, rad2, rad3)
	local nrad1 = "نْ" .. rad1
	local vn = high_form_verbal_noun(nrad1, Y, rad3)

	-- various stem bases
	local nonpast_stem_base = nrad1
	local past_stem_base = "اِ" ..nonpast_stem_base
	local ps_past_stem_base = "اُ" .. nrad1

	-- make parts
	make_augmented_hollow_verb(data, args, rad3,
		past_stem_base, nonpast_stem_base, ps_past_stem_base, vn, "VII")
end

conjugations["VII-geminate"] = function(data, args, rad1, rad2, rad3)
	local nrad1 = "نْ" .. rad1
	local vn = high_form_verbal_noun(nrad1, rad2, rad2)

	-- various stem bases
	local nonpast_stem_base = nrad1 .. A
	local past_stem_base = "اِ" .. nonpast_stem_base
	local ps_past_stem_base = "اُ" .. nrad1 .. U

	-- make parts
	make_augmented_geminate_verb(data, args, rad2,
		past_stem_base, nonpast_stem_base, ps_past_stem_base, vn, "VII")
end

-- Join the infixed tā' (ت) to the first radical in form VIII verbs. This may
-- cause assimilation of the tā' to the radical or in some cases the radical to
-- the tā'.
function join_ta(rad)
	if rad == W or rad == Y or rad == "ت" then return "تّ"
	elseif rad == "د" then return "دّ"
	elseif rad == "ث" then return "ثّ"
	elseif rad == "ذ" then return "ذّ"
	elseif rad == "ز" then return "زْد"
	elseif rad == "ص" then return "صْط"
	elseif rad == "ض" then return "ضْط"
	elseif rad == "ط" then return "طّ"
	elseif rad == "ظ" then return "ظّ"
	else return rad .. SK .. "ت"
	end
end

-- Return Form VIII verbal noun. RAD3 is nil for final-weak verbs. If RAD1 is
-- hamza, there are two alternatives.
function form_viii_verbal_noun(rad1, rad2, rad3)
	local vn = high_form_verbal_noun(join_ta(rad1), rad2, rad3)
	if rad1 == HAMZA then
		return {vn, high_form_verbal_noun(Y .. T, rad2, rad3)}
	else
		return {vn}
	end
end

-- Make form VIII sound or final-weak verb. Final-weak verbs are identified
-- by RAD3 = nil.
function make_form_viii_sound_final_weak_verb(data, args, rad1, rad2, rad3)
	-- check for irregular verb اِتَّخَذَ
	if axadh_radicals(rad1, rad2, rad3) then
		data.irregular = true
		rad1 = T
	end
	make_high_form_sound_final_weak_verb(data, args, join_ta(rad1), rad2, rad3,
		"VIII")

	-- Add alternative parts if verb is first-hamza. Any duplicates are
	-- removed in get_spans().
	if rad1 == HAMZA then
		local vn = form_viii_verbal_noun(rad1, rad2, rad3)
		local past_stem_base2 = "اِيتَ" .. rad2
		local nonpast_stem_base2 = join_ta(rad1) .. A .. rad2
		local ps_past_stem_base2 = "اُوتُ" .. rad2
		make_augmented_sound_final_weak_verb(data, args, rad3,
			past_stem_base2, nonpast_stem_base2, ps_past_stem_base2, vn, "VIII")
	end
end

conjugations["VIII-sound"] = function(data, args, rad1, rad2, rad3)
	make_form_viii_sound_final_weak_verb(data, args, rad1, rad2, rad3)
end

conjugations["VIII-final-weak"] = function(data, args, rad1, rad2, rad3)
	make_form_viii_sound_final_weak_verb(data, args, rad1, rad2, nil)
end

conjugations["VIII-hollow"] = function(data, args, rad1, rad2, rad3)
	local vn = form_viii_verbal_noun(rad1, Y, rad3)

	-- various stem bases
	local nonpast_stem_base = join_ta(rad1)
	local past_stem_base = "اِ" .. nonpast_stem_base
	local ps_past_stem_base = "اُ" .. nonpast_stem_base

	-- make parts
	make_augmented_hollow_verb(data, args, rad3,
		past_stem_base, nonpast_stem_base, ps_past_stem_base, vn, "VIII")

	-- Add alternative parts if verb is first-hamza. Any duplicates are
	-- removed in get_spans().
	if rad1 == HAMZA then
		local past_stem_base2 = "اِيت"
		local nonpast_stem_base2 = nonpast_stem_base
		local ps_past_stem_base2 = "اُوت"
		make_augmented_hollow_verb(data, args, rad3,
			past_stem_base2, nonpast_stem_base2, ps_past_stem_base2, vn, "VIII")
	end
end

conjugations["VIII-geminate"] = function(data, args, rad1, rad2, rad3)
	local vn = form_viii_verbal_noun(rad1, rad2, rad2)

	-- various stem bases
	local nonpast_stem_base = join_ta(rad1) .. A
	local past_stem_base = "اِ" .. nonpast_stem_base
	local ps_past_stem_base = "اُ" .. join_ta(rad1) .. U

	-- make parts
	make_augmented_geminate_verb(data, args, rad2,
		past_stem_base, nonpast_stem_base, ps_past_stem_base, vn, "VIII")

	-- Add alternative parts if verb is first-hamza. Any duplicates are
	-- removed in get_spans().
	if rad1 == HAMZA then
		local past_stem_base2 = "اِيتَ"
		local nonpast_stem_base2 = nonpast_stem_base
		local ps_past_stem_base2 = "اُوتُ"
		make_augmented_geminate_verb(data, args, rad2,
			past_stem_base2, nonpast_stem_base2, ps_past_stem_base2, vn, "VIII")
	end
end

conjugations["IX-sound"] = function(data, args, rad1, rad2, rad3)
	local ipref = "اِ"
	local vn = ipref .. rad1 .. SK .. rad2 .. I .. rad3 .. AA .. rad3 .. UNS

	-- various stem bases
	local nonpast_stem_base = rad1 .. SK .. rad2 .. A
	local past_stem_base = ipref .. nonpast_stem_base
	local ps_past_stem_base = "اُ" .. rad1 .. SK .. rad2 .. U

	-- make parts
	make_augmented_geminate_verb(data, args, rad3,
		past_stem_base, nonpast_stem_base, ps_past_stem_base, vn, "IX")
end

conjugations["IX-final-weak"] = function(data, args, rad1, rad2, rad3)
	error("FIXME: Not yet implemented")
end

-- Populate a sound or final-weak verb for any of the various high-numbered
-- augmented forms that have 5 consonants in the stem and the same pattern of
-- vowels. Some of these consonants in certain verb parts are w's, which leads to
-- apparent anomalies in certain stems of these parts, but these anomalies
-- are handled automatically in postprocessing, where we resolve sequences of
-- iwC -> īC, uwC -> ūC, w + sukūn + w -> w + shadda.
function make_high5_form_sound_final_weak_verb(data, args, rad1, rad2, rad3, rad4, rad5, form)
	make_high_form_sound_final_weak_verb(data, args, rad1 .. SK .. rad2,
		rad3 .. SK .. rad4, rad5, form)
end

-- Make form X sound or final-weak verb. Final-weak verbs are identified
-- by RAD3 = nil.
function make_form_x_sound_final_weak_verb(data, args, rad1, rad2, rad3)
	make_high5_form_sound_final_weak_verb(data, args, S, T, rad1, rad2, rad3, "X")
	-- check for irregular verb اِسْتَحْيَا (also اِسْتَحَى or اِسْتَحَّى)
	if hayy_radicals(rad1, rad2, rad3 or Y) then
		data.irregular = true
		-- Add alternative entries to the verbal paradigms. Any duplicates are
		-- removed in get_spans().
		make_high_form_sound_final_weak_verb(data, args, S .. SK .. T, rad1, rad3, "X")
		make_high_form_sound_final_weak_verb(data, args, S .. SK .. T, rad1 .. SH, rad3, "X")
	end
end

conjugations["X-sound"] = function(data, args, rad1, rad2, rad3)
	make_form_x_sound_final_weak_verb(data, args, rad1, rad2, rad3)
end

conjugations["X-final-weak"] = function(data, args, rad1, rad2, rad3)
	make_form_x_sound_final_weak_verb(data, args, rad1, rad2, nil)
end

conjugations["X-hollow"] = function(data, args, rad1, rad2, rad3)
	local vn = "اِسْتِ" .. rad1 .. AA .. rad3 .. AH .. UNS

	-- various stem bases
	local past_stem_base = "اِسْتَ" .. rad1
	local nonpast_stem_base = "سْتَ" .. rad1
	local ps_past_stem_base = "اُسْتُ" .. rad1

	-- make parts
	make_augmented_hollow_verb(data, args, rad3,
		past_stem_base, nonpast_stem_base, ps_past_stem_base, vn, "X")
end

conjugations["X-geminate"] = function(data, args, rad1, rad2, rad3)
	local vn = "اِسْتِ" .. rad1 .. SK .. rad2 .. AA .. rad2 .. UNS

	-- various stem bases
	local past_stem_base = "اِسْتَ" .. rad1
	local nonpast_stem_base = "سْتَ" .. rad1
	local ps_past_stem_base = "اُسْتُ" .. rad1

	-- make parts
	make_augmented_geminate_verb(data, args, rad2,
		past_stem_base, nonpast_stem_base, ps_past_stem_base, vn, "X")
end

conjugations["XI-sound"] = function(data, args, rad1, rad2, rad3)
	local ipref = "اِ"
	local vn = ipref .. rad1 .. SK .. rad2 .. II .. rad3 .. AA .. rad3 .. UNS

	-- various stem bases
	local nonpast_stem_base = rad1 .. SK .. rad2 .. AA
	local past_stem_base = ipref .. nonpast_stem_base
	local ps_past_stem_base = "اُ" .. rad1 .. SK .. rad2 .. UU

	-- make parts
	make_augmented_geminate_verb(data, args, rad3,
		past_stem_base, nonpast_stem_base, ps_past_stem_base, vn, "XI")
end

-- probably no form XI final-weak, since already geminate in form; would behave as XI-sound

-- Make form XII sound or final-weak verb. Final-weak verbs are identified
-- by RAD3 = nil.
function make_form_xii_sound_final_weak_verb(data, args, rad1, rad2, rad3)
	make_high5_form_sound_final_weak_verb(data, args, rad1, rad2, W, rad2, rad3, "XII")
end

conjugations["XII-sound"] = function(data, args, rad1, rad2, rad3)
	make_form_xii_sound_final_weak_verb(data, args, rad1, rad2, rad3)
end

conjugations["XII-final-weak"] = function(data, args, rad1, rad2, rad3)
	make_form_xii_sound_final_weak_verb(data, args, rad1, rad2, nil)
end

-- Make form XIII sound or final-weak verb. Final-weak verbs are identified
-- by RAD3 = nil.
function make_form_xiii_sound_final_weak_verb(data, args, rad1, rad2, rad3)
	make_high5_form_sound_final_weak_verb(data, args, rad1, rad2, W, W, rad3, "XIII")
end

conjugations["XIII-sound"] = function(data, args, rad1, rad2, rad3)
	make_form_xiii_sound_final_weak_verb(data, args, rad1, rad2, rad3)
end

conjugations["XIII-final-weak"] = function(data, args, rad1, rad2, rad3)
	make_form_xiii_sound_final_weak_verb(data, args, rad1, rad2, nil)
end

-- Make a form XIV or XV sound or final-weak verb. Last radical appears twice
-- (if`anlala / yaf`anlilu) so if it were w or y you'd get if`anwā / yaf`anwī
-- or if`anyā / yaf`anyī, i.e. we need the identity of the radical, so the
-- normal trick of passing nil as rad3 into these types of functions won't work.
-- Instead we pass the full radical as well as a flag indicating whether the
-- verb is final-weak. The last radical need not be w or y; in fact this is
-- exactly what form XV is about.
function make_form_xiv_xv_sound_final_weak_verb(data, args, rad1, rad2, rad3, final_weak, form)
	local lastrad = not final_weak and rad3 or nil
	make_high5_form_sound_final_weak_verb(data, args, rad1, rad2, N, rad3, lastrad, form)
end

conjugations["XIV-sound"] = function(data, args, rad1, rad2, rad3)
	make_form_xiv_xv_sound_final_weak_verb(data, args, rad1, rad2, rad3, false, "XIV")
end

conjugations["XIV-final-weak"] = function(data, args, rad1, rad2, rad3)
	make_form_xiv_xv_sound_final_weak_verb(data, args, rad1, rad2, rad3, true, "XIV")
end

conjugations["XV-sound"] = function(data, args, rad1, rad2, rad3)
	make_form_xiv_xv_sound_final_weak_verb(data, args, rad1, rad2, rad3, true, "XV")
end

-- probably no form XV final-weak, since already final-weak in form; would behave as XV-sound

-- Make form Iq or IIq sound or final-weak verb. Final-weak verbs are identified
-- by RAD4 = nil. FORM distinguishes Iq from IIq.
function make_form_iq_iiq_sound_final_weak_verb(data, args, rad1, rad2, rad3, rad4, form)
	local final_weak = rad4 == nil
	local vn = form == "IIq" and
		"تَ" .. rad1 .. A .. rad2 .. SK .. rad3 ..
			(final_weak and IN or U .. rad4 .. UNS) or
		rad1 .. A .. rad2 .. SK .. rad3 ..
			(final_weak and AAH or A .. rad4 .. AH) .. UNS
	local ta_pref = form == "IIq" and "تَ" or ""
	local tu_pref = form == "IIq" and "تُ" or ""

	-- various stem bases
	local past_stem_base = ta_pref .. rad1 .. A .. rad2 .. SK .. rad3
	local nonpast_stem_base = past_stem_base
	local ps_past_stem_base = tu_pref .. rad1 .. U .. rad2 .. SK .. rad3

	-- make parts
	make_augmented_sound_final_weak_verb(data, args, rad4,
		past_stem_base, nonpast_stem_base, ps_past_stem_base, vn, form)
end

conjugations["Iq-sound"] = function(data, args, rad1, rad2, rad3, rad4)
	make_form_iq_iiq_sound_final_weak_verb(data, args, rad1, rad2, rad3, rad4, "Iq")
end

conjugations["Iq-final-weak"] = function(data, args, rad1, rad2, rad3, rad4)
	make_form_iq_iiq_sound_final_weak_verb(data, args, rad1, rad2, rad3, nil, "Iq")
end

conjugations["IIq-sound"] = function(data, args, rad1, rad2, rad3, rad4)
	make_form_iq_iiq_sound_final_weak_verb(data, args, rad1, rad2, rad3, rad4, "IIq")
end

conjugations["IIq-final-weak"] = function(data, args, rad1, rad2, rad3, rad4)
	make_form_iq_iiq_sound_final_weak_verb(data, args, rad1, rad2, rad3, nil, "IIq")
end

-- Make form IIIq sound or final-weak verb. Final-weak verbs are identified
-- by RAD4 = nil.
function make_form_iiiq_sound_final_weak_verb(data, args, rad1, rad2, rad3, rad4)
	make_high5_form_sound_final_weak_verb(data, args, rad1, rad2, N, rad3, rad4, "IIIq")
end

conjugations["IIIq-sound"] = function(data, args, rad1, rad2, rad3, rad4)
	make_form_iiiq_sound_final_weak_verb(data, args, rad1, rad2, rad3, rad4)
end

conjugations["IIIq-final-weak"] = function(data, args, rad1, rad2, rad3, rad4)
	make_form_iiiq_sound_final_weak_verb(data, args, rad1, rad2, rad3, nil)
end

conjugations["IVq-sound"] = function(data, args, rad1, rad2, rad3, rad4)
	local ipref = "اِ"
	local vn = ipref .. rad1 .. SK .. rad2 .. I .. rad3 .. SK .. rad4 .. AA .. rad4 .. UNS

	-- various stem bases
	local past_stem_base = ipref .. rad1 .. SK .. rad2 .. A .. rad3
	local nonpast_stem_base = rad1 .. SK .. rad2 .. A .. rad3
	local ps_past_stem_base = "اُ" .. rad1 .. SK .. rad2 .. U .. rad3

	-- make parts
	make_augmented_geminate_verb(data, args, rad4,
		past_stem_base, nonpast_stem_base, ps_past_stem_base, vn, "IVq")
end

-- probably no form IVq final-weak, since already geminate in form; would behave as IVq-sound

------------------------------------
-- Basic functions to inflect tenses
------------------------------------

-- Implementation of inflect_tense(). See that function. Also used directly
-- to add the imperative, which has only five parts.
function inflect_tense_1(data, tense, prefixes, stems, endings, pnums)
	if prefixes == nil then
		error("For tense '" .. tense .. "', prefixes = nil")
	end
	if stems == nil then
		error("For tense '" .. tense .. "', stems = nil")
	end
	if endings == nil then
		error("For tense '" .. tense .. "', endings = nil")
	end
	if type(prefixes) == "table" and #pnums ~= #prefixes then
		error("For tense '" .. tense .. "', found " .. #prefixes .. " prefixes but expected " .. #pnums)
	end
	if type(stems) == "table" and #pnums ~= #stems then
		error("For tense '" .. tense .. "', found " .. #stems .. " stems but expected " .. #pnums)
	end
	if #pnums ~= #endings then
		error("For tense '" .. tense .. "', found " .. #endings .. " endings but expected " .. #pnums)
	end

	-- First, initialize any nil entries to sequences.
	for i, pnum in ipairs(pnums) do
		if data.forms[pnum .. "-" .. tense] == nil then
			data.forms[pnum .. "-" .. tense] = {}
		end
	end

	-- Now add entries
	for i = 1, #pnums do
		-- Extract endings for this person-number combo
		local ends = endings[i]
		if type(ends) == "string" then ends = {ends} end
		-- Extract prefix for this person-number combo
		local prefix = prefixes
		if type(prefix) == "table" then prefix = prefix[i] end
		-- Extract stem for this person-number combo
		local stem = stems
		if type(stem) == "table" then stem = stem[i] end
		-- Add entries for stem + endings
		for j, ending in ipairs(ends) do
			-- allow some inflections to be skipped; useful for generating
			-- partly irregular inflections
			if prefix ~= "-" and stem ~= "-" then
				local part = prefix .. stem .. ending
				if ine(part) and part ~= "-"
						-- and (not data.impers or pnums[i] == "3sg")
						then
					table.insert(data.forms[pnums[i] .. "-" .. tense], part)
				end
			end
		end
	end
end

-- Add to DATA the inflections for the tense indicated by TENSE (the suffix
-- in the forms names, e.g. 'perf'), formed by combining the PREFIXES
-- (either a single string or a sequence of 13 strings), STEMS
-- (either a single string or a sequence of 13 strings) with the
-- ENDINGS (a sequence of 13 values, each of which is either a string
-- or a sequence of one or more possible endings). If existing
-- inflections already exist, they will be added to, not overridden.
-- If any value of PREFIXES or STEMS is the string "-", then the corresponding
-- inflection will be skipped.
function inflect_tense(data, tense, prefixes, stems, endings)
	local pnums = {"1s", "2sm", "2sf", "3sm", "3sf",
				   "2d", "3dm", "3df",
				   "1p", "2pm", "2pf", "3pm", "3pf"}
	inflect_tense_1(data, tense, prefixes, stems, endings, pnums)
end

-- Like inflect_tense() but for the imperative, which has only five parts
-- instead of 13.
function inflect_tense_impr(data, stems, endings)
	local pnums = {"2sm", "2sf", "2d", "2pm", "2pf"}
	inflect_tense_1(data, "impr", "", stems, endings, pnums)
end

-- Add VALUE (a string or array) to the end of any entries in DATA.forms[NAME],
-- initializing it to an empty array if needed.
function insert_part(data, name, value)
	if data.forms[name] == nil then
		data.forms[name] = {}
	end
	if type(value) == "table" then
		for _, entry in ipairs(value) do
			table.insert(data.forms[name], entry)
		end
	else
		table.insert(data.forms[name], value)
	end
end

-- Insert verbal noun VN into DATA.forms["vn"], but allow it to be overridden by
-- ARGS["vn"].
function insert_verbal_noun(data, args, vn)
	local vns = args["vn"] and rsplit(args["vn"], "[,،]") or vn
	if type(vns) ~= "table" then
		vns = {vns}
	end

	-------------------- Begin verbal-noun i3rab tracking code ---------------
	-- Examples of what you can find by looking at what links to the given
	-- pages:
	--
	-- Template:tracking/ar-verb/vn/i3rab/un (pages with verbal nouns with -un
	--   i3rab, whether explicitly specified, i.e. using vn=, or auto-generated,
	--   as is normal for augmented forms)
	-- Template:tracking/ar-verb/explicit-vn/i3rab/u (pages with explicitly
	--   specified verbal nouns with -u i3rab)
	-- Template:tracking/ar-verb/explicit-vn/i3rab/an-tall (pages with
	--   explicitly specified verbal nouns with tall-alif -an i3rab)
	-- Template:tracking/ar-verb/auto-vn/i3rab (pages with auto-generated verbal
	--   nouns with any sort of i3rab)
	-- Template:tracking/ar-verb/vn/no-i3rab (pages with verbal nouns without
	--   i3rab)
	-- Template:tracking/ar-verb/vn/would-be-decl/di (pages with verbal nouns
	--   that would be detected as diptote without explicit i3rab)
	function vntrack(pagesuff)
		track("vn/" .. pagesuff)
		if args["vn"] then
			track("explicit-vn/" .. pagesuff)
		else
			track("auto-vn/" .. pagesuff)
		end
	end

	function track_i3rab(entry, arabic, tr)
		if rfind(entry, arabic .. "$") then
			vntrack("i3rab")
			vntrack("i3rab/" .. tr)
		end
	end

	for _, entry in ipairs(vns) do
		-- Need to do this else we will have problems with VN's whose stem ends
		-- in shadda.
		entry = reorder_shadda(entry)
		track_i3rab(entry, UN, "un")
		track_i3rab(entry, U, "u")
		track_i3rab(entry, IN, "in")
		track_i3rab(entry, I, "i")
		track_i3rab(entry, AN .. "[" .. ALIF .. AMAQ .. "]", "an")
		track_i3rab(entry, AN .. ALIF, "an-tall")
		track_i3rab(entry, A, "a")
		track_i3rab(entry, SK, "sk")
		if not rfind(entry, "[" .. A .. I .. U .. AN .. IN .. UN .. DAGGER_ALIF .. "]$") and
				not rfind(entry, AN .. "[" .. ALIF .. AMAQ .. "]") then
			vntrack("no-i3rab")
		end
		entry = rsub(entry, UNU .. "?$", "")
		-- Figure out what the decltype would be without any explicit -un
		-- or -u added, to see whether there are any nouns that would be
		-- detected as diptotes, e.g. of the فَعْلَان pattern.
		local decltype = ar_nominals.detect_type(entry, false, "sg", "noun")
		vntrack("would-be-decl/" .. decltype)
	end
	-------------------- End verbal-noun i3rab tracking code ---------------

	if no_nominal_i3rab then
		vns_no_i3rab = {}
		for _, entry in ipairs(vns) do
			entry = reorder_shadda(entry)
			entry = rsub(entry, UNU .. "?$", "")
			table.insert(vns_no_i3rab, entry)
		end
		insert_part(data, "vn", vns_no_i3rab)
	else
		insert_part(data, "vn", vns)
	end
end

--------------------------------------
-- functions to inflect the past tense
--------------------------------------

--generate past verbs using specified vowel and consonant stems; works for
--sound, assimilated, hollow, and geminate verbs, active and passive
function past_2stem_conj(data, tense, v_stem, c_stem)
	inflect_tense(data, tense, "", {
		-- singular
		c_stem, c_stem, c_stem, v_stem, v_stem,
		--dual
		c_stem, v_stem, v_stem,
		-- plural
		c_stem, c_stem, c_stem, v_stem, c_stem
	}, past_endings)
end

--generate past verbs using single specified stem; works for sound and
--assimilated verbs, active and passive
function past_1stem_conj(data, tense, stem)
	past_2stem_conj(data, tense, stem, stem)
end

---------------------------------------
-- functions to inflect non-past tenses
---------------------------------------

-- Generate non-past conjugation, with two stems, for vowel-initial and
-- consonant-initial endings, respectively. Useful for active and passive;
-- for all forms; for all weaknesses (sound, assimilated, hollow, final-weak
-- and geminate) and for all types of non-past (indicative, subjunctive,
-- jussive) except for the imperative. (There is a separate function below
-- for geminate jussives because they have three alternants.) Both stems may
-- be the same, e.g. for sound verbs.
--
-- PREFIXES will generally be either "a" (= 'nonpast_prefixes_a', for active
-- forms I and V - X) or "u" (= 'nonpast_prefixes_u', for active forms II - IV
-- and Iq and all passive forms). Otherwise, it should be either a single string
-- (often "") or an array (table) of 13 items. ENDINGS should similarly be an
-- array of 13 items. If ENDINGS is nil or omitted, infer the endings from
-- the tense. If JUSSIVE is true, or ENDINGS is nil and TENSE indicatives
-- jussive, use the jussive pattern of vowel/consonant stems (different from the
-- normal ones).
function nonpast_2stem_conj(data, tense, prefixes, v_stem, c_stem, endings, jussive)
	if prefixes == "a" then prefixes = nonpast_prefixes_a
	elseif prefixes == "u" then prefixes = nonpast_prefixes_u
	end
	if endings == nil then
		if tense == "impf" or tense == "ps-impf" then
			endings = indic_endings
		elseif tense == "subj" or tense == "ps-subj" then
			endings = subj_endings
		elseif tense == "juss" or tense == "ps-juss" then
			jussive = true
			endings = juss_endings
		else
			error("Unrecognized tense '" .. tense .."'")
		end
	end
	if not jussive then
		inflect_tense(data, tense, prefixes, {
			-- singular
			v_stem, v_stem, v_stem, v_stem, v_stem,
			--dual
			v_stem, v_stem, v_stem,
			-- plural
			v_stem, v_stem, c_stem, v_stem, c_stem
		}, endings)
	else
		inflect_tense(data, tense, prefixes, {
			-- singular
			-- 'adlul, tadlul, tadullī, yadlul, tadlul
			c_stem, c_stem, v_stem, c_stem, c_stem,
			--dual
			-- tadullā, yadullā, tadullā
			v_stem, v_stem, v_stem,
			-- plural
			-- nadlul, tadullū, tadlulna, yadullū, yadlulna
			c_stem, v_stem, c_stem, v_stem, c_stem
		}, endings)
	end
end

-- Generate non-past conjugation with one stem (no distinct stems for
-- vowel-initial and consonant-initial endings). See nonpast_2stem_conj().
function nonpast_1stem_conj(data, tense, prefixes, stem, endings, jussive)
	nonpast_2stem_conj(data, tense, prefixes, stem, stem, endings, jussive)
end

-- Generate active/passive jussive geminative. There are three alternants, two
-- with terminations -a and -i and one in a null termination with a distinct
-- pattern of vowel/consonant stem usage. See nonpast_2stem_conj() for a
-- description of the arguments.
function jussive_gem_conj(data, tense, prefixes, v_stem, c_stem)
	-- alternative in -a
	nonpast_2stem_conj(data, tense, prefixes, v_stem, c_stem, juss_endings_alt_a)
	-- alternative in -i
	nonpast_2stem_conj(data, tense, prefixes, v_stem, c_stem, juss_endings_alt_i)
	-- alternative in -null; requires different combination of v_stem and
	-- c_stem since the null endings require the c_stem (e.g. "tadlul" here)
	-- whereas the corresponding endings above in -a or -i require the v_stem
	-- (e.g. "tadulla, tadulli" above)
	nonpast_2stem_conj(data, tense, prefixes, v_stem, c_stem, juss_endings, "jussive")
end

--------------------------------------
-- functions to inflect the imperative
--------------------------------------

-- generate imperative parts for sound or assimilated verbs
function make_1stem_imperative(data, stem)
	inflect_tense_impr(data, stem, impr_endings)
end

-- generate imperative parts for two-stem verbs (hollow or geminate)
function make_2stem_imperative(data, v_stem, c_stem)
	inflect_tense_impr(data,
		{c_stem, v_stem, v_stem, v_stem, c_stem}, impr_endings)
end

-- generate imperative parts for geminate verbs form I (also IV, VII, VIII, X)
function make_gem_imperative(data, v_stem, c_stem)
	inflect_tense_impr(data,
		{v_stem, v_stem, v_stem, v_stem, c_stem}, impr_endings_alt_a)
	inflect_tense_impr(data,
		{v_stem, v_stem, v_stem, v_stem, c_stem}, impr_endings_alt_i)
	make_2stem_imperative(data, v_stem, c_stem)
end

------------------------------------
-- functions to inflect entire verbs
------------------------------------

-- generate finite parts of a sound verb (also works for assimilated verbs)
-- from five stems (past and non-past, active and passive, plus imperative)
-- plus the prefix vowel in the active non-past ("a" or "u")
function make_sound_verb(data, past_stem, ps_past_stem, nonpast_stem,
		ps_nonpast_stem, imper_stem, prefix_vowel)
	past_1stem_conj(data, "perf", past_stem)
	past_1stem_conj(data, "ps-perf", ps_past_stem)
	nonpast_1stem_conj(data, "impf", prefix_vowel, nonpast_stem)
	nonpast_1stem_conj(data, "subj", prefix_vowel, nonpast_stem)
	nonpast_1stem_conj(data, "juss", prefix_vowel, nonpast_stem)
	nonpast_1stem_conj(data, "ps-impf", "u", ps_nonpast_stem)
	nonpast_1stem_conj(data, "ps-subj", "u", ps_nonpast_stem)
	nonpast_1stem_conj(data, "ps-juss", "u", ps_nonpast_stem)
	make_1stem_imperative(data, imper_stem)
end

-- generate finite parts of a final-weak verb from five stems (past and
-- non-past, active and passive, plus imperative), five sets of
-- suffixes (past, non-past indicative/subjunctive/jussive, imperative)
-- and the prefix vowel in the active non-past ("a" or "u")
function make_final_weak_verb(data, past_stem, ps_past_stem, nonpast_stem,
		ps_nonpast_stem, imper_stem, past_suffs, indic_suffs,
		subj_suffs, juss_suffs, impr_suffs, prefix_vowel)
	inflect_tense(data, "perf", "", past_stem, past_suffs)
	inflect_tense(data, "ps-perf", "", ps_past_stem, past_endings_ii)
	nonpast_1stem_conj(data, "impf", prefix_vowel, nonpast_stem, indic_suffs)
	nonpast_1stem_conj(data, "subj", prefix_vowel, nonpast_stem, subj_suffs)
	nonpast_1stem_conj(data, "juss", prefix_vowel, nonpast_stem, juss_suffs)
	nonpast_1stem_conj(data, "ps-impf", "u", ps_nonpast_stem, indic_endings_aa)
	nonpast_1stem_conj(data, "ps-subj", "u", ps_nonpast_stem, subj_endings_aa)
	nonpast_1stem_conj(data, "ps-juss", "u", ps_nonpast_stem, juss_endings_aa)
	inflect_tense_impr(data, imper_stem, impr_suffs)
end

-- generate finite parts of an augmented (form II+) final-weak verb from five
-- stems (past and non-past, active and passive, plus imperative) plus the
-- prefix vowel in the active non-past ("a" or "u") and a flag indicating if
-- behave like a form V/VI verb in taking non-past endings in -ā instead of -ī
function make_augmented_final_weak_verb(data, past_stem, ps_past_stem,
		nonpast_stem, ps_nonpast_stem, imper_stem, prefix_vowel, form56)
	make_final_weak_verb(data, past_stem, ps_past_stem, nonpast_stem,
		ps_nonpast_stem, imper_stem, past_endings_ay,
		form56 and indic_endings_aa or indic_endings_ii,
		form56 and subj_endings_aa or subj_endings_ii,
		form56 and juss_endings_aa or juss_endings_ii,
		form56 and impr_endings_aa or impr_endings_ii,
		prefix_vowel)
end

-- generate finite parts of an augmented (form II+) sound or final-weak verb,
-- given the following:
--
-- DATA, ARGS = arguments from conjugation function
-- RAD3 = last radical; should be nil for final-weak verb
-- PAST_STEM_BASE = active past stem minus last syllable (= -al or -ā)
-- NONPAST_STEM_BASE = non-past stem minus last syllable (= -al/-il or -ā/-ī)
-- PS_PAST_STEM_BASE = passive past stem minus last syllable (= -il or -ī)
-- FORM -- form of verb (II to XV, Iq - IVq)
-- VN = verbal noun
function make_augmented_sound_final_weak_verb(data, args, rad3,
		past_stem_base, nonpast_stem_base, ps_past_stem_base, vn, form)

	insert_verbal_noun(data, args, vn)

	local final_weak = rad3 == nil
	local prefix_vowel = prefix_vowel_from_form(form)
	local form56 = form_nonpast_a_vowel(form)
	local a_base_suffix = final_weak and "" or A .. rad3
	local i_base_suffix = final_weak and "" or I .. rad3

	-- past and non-past stems, active and passive
	local past_stem = past_stem_base .. a_base_suffix
	-- In forms 5 and 6, non-past has /a/ as last stem vowel in the non-past
	-- in both active and passive, but /i/ in the active participle and /a/
	-- in the passive participle. Elsewhere, consistent /i/ in active non-past
	-- and participle, consistent /a/ in passive non-past and participle.
	-- Hence, forms 5 and 6 differ only in the non-past active (but not
	-- active participle), so we have to split the finite non-past stem and
	-- active participle stem.
	local nonpast_stem = nonpast_stem_base ..
		(form56 and a_base_suffix or i_base_suffix)
	local ap_stem = nonpast_stem_base .. i_base_suffix
	local ps_past_stem = ps_past_stem_base .. i_base_suffix
	local ps_nonpast_stem = nonpast_stem_base .. a_base_suffix
	-- imperative stem
	local imper_stem = past_stem_base ..
		(form56 and a_base_suffix or i_base_suffix)

	-- make parts
	if final_weak then
		make_augmented_final_weak_verb(data, past_stem, ps_past_stem, nonpast_stem,
			ps_nonpast_stem, imper_stem, prefix_vowel, form56)
	else
		make_sound_verb(data, past_stem, ps_past_stem, nonpast_stem,
			ps_nonpast_stem, imper_stem, prefix_vowel)
	end

	-- active and passive participle
	if final_weak then
		insert_part(data, "ap", MU .. ap_stem .. IN)
		insert_part(data, "pp", MU .. ps_nonpast_stem .. AN .. AMAQ)
	else
		insert_part(data, "ap", MU .. ap_stem .. UNS)
		insert_part(data, "pp", MU .. ps_nonpast_stem .. UNS)
	end
end

-- generate finite parts of a hollow or geminate verb from ten stems (vowel and
-- consonant stems for each of past and non-past, active and passive, plus
-- imperative) plus the prefix vowel in the active non-past ("a" or "u"), plus
-- a flag indicating if we are a geminate verb
function make_hollow_geminate_verb(data, past_v_stem, past_c_stem, ps_past_v_stem,
	ps_past_c_stem, nonpast_v_stem, nonpast_c_stem, ps_nonpast_v_stem,
	ps_nonpast_c_stem, imper_v_stem, imper_c_stem, prefix_vowel, geminate)
	past_2stem_conj(data, "perf", past_v_stem, past_c_stem)
	past_2stem_conj(data, "ps-perf", ps_past_v_stem, ps_past_c_stem)
	nonpast_2stem_conj(data, "impf", prefix_vowel, nonpast_v_stem, nonpast_c_stem)
	nonpast_2stem_conj(data, "subj", prefix_vowel, nonpast_v_stem, nonpast_c_stem)
	nonpast_2stem_conj(data, "ps-impf", "u", ps_nonpast_v_stem, ps_nonpast_c_stem)
	nonpast_2stem_conj(data, "ps-subj", "u", ps_nonpast_v_stem, ps_nonpast_c_stem)
	if geminate then
		jussive_gem_conj(data, "juss", prefix_vowel, nonpast_v_stem, nonpast_c_stem)
		jussive_gem_conj(data, "ps-juss", "u", ps_nonpast_v_stem, ps_nonpast_c_stem)
		make_gem_imperative(data, imper_v_stem, imper_c_stem)
	else
		nonpast_2stem_conj(data, "juss", prefix_vowel, nonpast_v_stem, nonpast_c_stem)
		nonpast_2stem_conj(data, "ps-juss", "u", ps_nonpast_v_stem, ps_nonpast_c_stem)
		make_2stem_imperative(data, imper_v_stem, imper_c_stem)
	end
end

-- generate finite parts of an augmented (form II+) hollow verb,
-- given the following:
--
-- DATA, ARGS = arguments from conjugation function
-- RAD3 = last radical (after the hollowness)
-- PAST_STEM_BASE = invariable part of active past stem
-- NONPAST_STEM_BASE = invariable part of non-past stem
-- PS_PAST_STEM_BASE = invariable part of passive past stem
-- VN = verbal noun
-- FORM = the verb form ("IV", "VII", "VIII", "X")
function make_augmented_hollow_verb(data, args, rad3,
	past_stem_base, nonpast_stem_base, ps_past_stem_base, vn, form)
	insert_verbal_noun(data, args, vn)

	local form410 = form == "IV" or form == "X"
	local prefix_vowel = prefix_vowel_from_form(form)

	local a_base_suffix_v, a_base_suffix_c
	local i_base_suffix_v, i_base_suffix_c

	a_base_suffix_v = AA .. rad3         -- 'af-āl-a, inf-āl-a
	a_base_suffix_c = A .. rad3      -- 'af-al-tu, inf-al-tu
	i_base_suffix_v = II .. rad3         -- 'uf-īl-a, unf-īl-a
	i_base_suffix_c = I .. rad3      -- 'uf-il-tu, unf-il-tu

	-- past and non-past stems, active and passive, for vowel-initial and
	-- consonant-initial endings
	local past_v_stem = past_stem_base .. a_base_suffix_v
	local past_c_stem = past_stem_base .. a_base_suffix_c
	-- yu-f-īl-u, ya-staf-īl-u but yanf-āl-u, yaft-āl-u
	local nonpast_v_stem = nonpast_stem_base ..
		(form410 and i_base_suffix_v or a_base_suffix_v)
	local nonpast_c_stem = nonpast_stem_base ..
		(form410 and i_base_suffix_c or a_base_suffix_c)
	local ps_past_v_stem = ps_past_stem_base .. i_base_suffix_v
	local ps_past_c_stem = ps_past_stem_base .. i_base_suffix_c
	local ps_nonpast_v_stem = nonpast_stem_base .. a_base_suffix_v
	local ps_nonpast_c_stem = nonpast_stem_base .. a_base_suffix_c

	-- imperative stem
	local imper_v_stem = past_stem_base ..
		(form410 and i_base_suffix_v or a_base_suffix_v)
	local imper_c_stem = past_stem_base ..
		(form410 and i_base_suffix_c or a_base_suffix_c)

	-- make parts
	make_hollow_geminate_verb(data, past_v_stem, past_c_stem, ps_past_v_stem,
		ps_past_c_stem, nonpast_v_stem, nonpast_c_stem, ps_nonpast_v_stem,
		ps_nonpast_c_stem, imper_v_stem, imper_c_stem, prefix_vowel, false)

	-- active participle
	insert_part(data, "ap", MU .. nonpast_v_stem .. UNS)
	-- passive participle
	insert_part(data, "pp", MU .. ps_nonpast_v_stem .. UNS)
end

-- generate finite parts of an augmented (form II+) geminate verb,
-- given the following:
--
-- DATA, ARGS = arguments from conjugation function
-- RAD3 = last radical (the one that gets geminated)
-- PAST_STEM_BASE = invariable part of active past stem; this and the stem
--   bases below will end with a consonant for forms IV, X, IVq, and a
--   short vowel for the others
-- NONPAST_STEM_BASE = invariable part of non-past stem
-- PS_PAST_STEM_BASE = invariable part of passive past stem
-- VN = verbal noun
-- FORM = the verb form ("III", "IV", "VI", "VII", "VIII", "IX", "X", "IVq")
function make_augmented_geminate_verb(data, args, rad3,
	past_stem_base, nonpast_stem_base, ps_past_stem_base, vn, form)
	insert_verbal_noun(data, args, vn)

	local prefix_vowel = prefix_vowel_from_form(form)

	local a_base_suffix_v, a_base_suffix_c
	local i_base_suffix_v, i_base_suffix_c

	if form == "IV" or form == "X" or form == "IVq" then
		a_base_suffix_v = A .. rad3 .. SH         -- 'af-all
		a_base_suffix_c = SK .. rad3 .. A .. rad3  -- 'af-lal
		i_base_suffix_v = I .. rad3 .. SH         -- yuf-ill
		i_base_suffix_c = SK .. rad3 .. I .. rad3  -- yuf-lil
	else
		a_base_suffix_v = rad3 .. SH         -- fā-ll, infa-ll
		a_base_suffix_c = rad3 .. A .. rad3  -- fā-lal, infa-lal
		i_base_suffix_v = rad3 .. SH         -- yufā-ll, yanfa-ll
		i_base_suffix_c = rad3 .. I .. rad3  -- yufā-lil, yanfa-lil
	end

	-- past and non-past stems, active and passive, for vowel-initial and
	-- consonant-initial endings
	local past_v_stem = past_stem_base .. a_base_suffix_v
	local past_c_stem = past_stem_base .. a_base_suffix_c
	local nonpast_v_stem = nonpast_stem_base ..
		(form_nonpast_a_vowel(form) and a_base_suffix_v or i_base_suffix_v)
	local nonpast_c_stem = nonpast_stem_base ..
		(form_nonpast_a_vowel(form) and a_base_suffix_c or i_base_suffix_c)
	-- form III and VI passive past do not have contracted parts, only
	-- uncontracted parts, which are added separately by those functions
	local ps_past_v_stem = (form == "III" or form == "VI") and "-" or
		ps_past_stem_base .. i_base_suffix_v
	local ps_past_c_stem = ps_past_stem_base .. i_base_suffix_c
	local ps_nonpast_v_stem = nonpast_stem_base .. a_base_suffix_v
	local ps_nonpast_c_stem = nonpast_stem_base .. a_base_suffix_c

	-- imperative stem
	local imper_v_stem = past_stem_base ..
		(form_nonpast_a_vowel(form) and a_base_suffix_v or i_base_suffix_v)
	local imper_c_stem = past_stem_base ..
		(form_nonpast_a_vowel(form) and a_base_suffix_c or i_base_suffix_c)

	-- make parts
	make_hollow_geminate_verb(data, past_v_stem, past_c_stem, ps_past_v_stem,
		ps_past_c_stem, nonpast_v_stem, nonpast_c_stem, ps_nonpast_v_stem,
		ps_nonpast_c_stem, imper_v_stem, imper_c_stem, prefix_vowel,
		"geminate")

	-- active participle
	insert_part(data, "ap", MU .. nonpast_v_stem .. UNS)
	-- passive participle
	insert_part(data, "pp", MU .. ps_nonpast_v_stem .. UNS)
end

----------------------------------------
-- functions to create inflection tables
----------------------------------------

-- array of substitutions; each element is a 2-entry array FROM, TO; do it
-- this way so the concatenations only get evaluated once
local postprocess_subs = {
	-- reorder short-vowel + shadda -> shadda + short-vowel for easier processing
	{"(" .. AIU .. ")" .. SH, SH .. "%1"},

	----------same letter separated by sukūn should instead use shadda---------
	------------happens e.g. in kun-nā "we were".-----------------
	{"(.)" .. SK .. "%1", "%1" .. SH},

	---------------------------- assimilated verbs ----------------------------
	-- iw, iy -> ī (assimilated verbs)
	{I .. W .. SK, II},
	{I .. Y .. SK, II},
	-- uw, uy -> ū (assimilated verbs)
	{U .. W .. SK, UU},
	{U .. Y .. SK, UU},

    -------------- final -yā uses tall alif not alif maqṣūra ------------------
	{"(" .. Y ..  SH .. "?" .. A .. ")" .. AMAQ, "%1" .. ALIF},

	----------------------- handle hamza assimilation -------------------------
	-- initial hamza + short-vowel + hamza + sukūn -> hamza + long vowel
	{HAMZA .. A .. HAMZA .. SK, HAMZA .. A .. ALIF},
	{HAMZA .. I .. HAMZA .. SK, HAMZA .. I .. Y},
	{HAMZA .. U .. HAMZA .. SK, HAMZA .. U .. W}
}

-- Post-process verb parts to eliminate phonological anomalies. Many of the changes,
-- particularly the tricky ones, involve converting hamza to have the proper
-- seat. The rules for this are complicated and are documented on the
-- [[w:Hamza]] Wikipedia page. In some cases there are alternatives allowed,
-- and we handle them below by returning multiple possibilities.
function postprocess_term(term)
	-- if term is regular hyphen/dash, ndash or mdash, return mdash
	if term == "-" or term == "–" or term == "—" then
		return {"—"} -- mdash
	elseif term == "?" then
		return {"?"}
	end
	-- do the main post-processing, based on the pattern substitutions in
	-- postprocess_subs
	for _, sub in ipairs(postprocess_subs) do
		term = rsub(term, sub[1], sub[2])
	end

	if not rfind(term, HAMZA) then
		return {term}
	end
	term = rsub(term, HAMZA, HAMZA_PH)
	return ar_utilities.process_hamza(term)
end

-- For each paradigm part, postprocess the entries, remove duplicates and
-- return the set of Arabic and transliterated Latin entries as two return
-- values.
function get_spans(part)
	if type(part) == "string" then
		part = {part}
	end
	local part_nondup = {}
	-- for each entry, postprocess it, which may potentially return
	-- multiple entries; insert each into an array, checking and
	-- omitting duplicates
	for _, entry in ipairs(part) do
		for _, e in ipairs(postprocess_term(entry)) do
			insert_if_not(part_nondup, e)
		end
	end
	-- convert each individual entry into Arabic and Latin span
	local arabic_spans = {}
	local latin_spans = {}
	for _, entry in ipairs(part_nondup) do
		table.insert(arabic_spans, entry)
		if entry ~= "—" and entry ~= "?" then
			-- multiple Arabic entries may map to the same Latin entry
			-- (happens particularly with variant ways of spelling hamza)
			insert_if_not(latin_spans, ar_translit.tr(entry, nil, nil, nil))
		end
	end
	return arabic_spans, latin_spans
end

-- Make the conjugation table. Called from export.show().
function make_table(data, title, form, intrans)

	local forms = data.forms
	local arabic_spans_3sm_perf, _
	if data.passive == "only" or data.passive == "only-impers" then
		arabic_spans_3sm_perf, _ = get_spans(forms["3sm-ps-perf"])
	else
		arabic_spans_3sm_perf, _ = get_spans(forms["3sm-perf"])
	end
	-- convert Arabic terms to spans
	for i, entry in ipairs(arabic_spans_3sm_perf) do
		arabic_spans_3sm_perf[i] = "<b lang=\"ar\" class=\"Arab\">" .. entry .. "</b>"
	end
	-- concatenate spans
	local part_3sm_perf = '<div style="display: inline-block">' ..
		table.concat(arabic_spans_3sm_perf, " <small style=\"color: #888\">or</small> ") .. "</div>"

	-- compute # of verbal nouns before we collapse them
	local num_vns = type(forms["vn"]) == "table" and #forms["vn"] or 1

	-- also extract list of verbal nouns and preceding text "verbal noun" or
	-- "verbal nouns", for the conjugation table title
	local vn_spans, _ = get_spans(forms["vn"])
	-- convert verbal nouns to spans
	for i, entry in ipairs(vn_spans) do
		vn_spans[i] = '<span lang="ar" class="Arab" style="font-weight: normal">' .. entry .. '</span>'
	end
	local vn_list = #vn_spans == 0 and "?" or
		table.concat(vn_spans, " <small style=\"color: #888\">or</small> ")
	local vn_prefix = #vn_spans > 1 and "verbal nouns" or "verbal noun"
	local vn_text = vn_prefix .. " " .. vn_list

	-- construct conjugation table title
	local title = 'Conjugation of ' .. part_3sm_perf
		.. (form == "I" and " (" .. title .. ", " .. vn_text .. ")" or " (" .. title .. ")")

	-- Format and add transliterations to all parts
	for key, part in pairs(forms) do
		-- check for nil, empty array, size-one array holding a dash or ?
		if part and #part > 0 then
			local arabic_spans, latin_spans = get_spans(part)
			-- convert Arabic terms to links
			for i, entry in ipairs(arabic_spans) do
				arabic_spans[i] = links(entry)
			end
			-- concatenate spans
			forms[key] = '<div style="display: inline-block">' .. table.concat(arabic_spans, " <small style=\"color: #888\">or</small> ") .. "</div>" .. "<br/>" ..
				"<span style=\"color: #888\">" .. table.concat(latin_spans, " <small>or</small> ") .. "</span>"
		-- if no verbal nouns, it's normally because they're unknown or
		-- unspecified rather than non-existent. For an actually non-existent
		-- verbal noun, put a hyphen or mdash explicitly as the value of the
		-- paradigm part, and it will be handled appropriately (no attempt to
		-- transliterate).
		elseif key == "vn" then
			forms[key] = "?"
		else
			forms[key] = "—"
		end
	end

	local text = [=[<div class="NavFrame" style="width:100%">
<div class="NavHead" style="height:2.5em">]=] .. title  .. [=[</div>
<div class="NavContent">

{| border="1" color="#cdcdcd" style="border-collapse:collapse; border:1px solid #555555; background:#fdfdfd; width:100%; text-align:center" class="inflection-table"
|-
! colspan="6" style="background:#dedede" | verbal noun]=] .. (num_vns > 1 and "s" or "") .. "<br />" .. tag_text(num_vns > 1 and "المَصَادِر" or "المَصْدَر") .. [=[

| colspan="7" | {{{vn}}}
]=]

	if data.passive ~= "only" and data.passive ~= "only-impers" then
		text = text .. [=[
|-
! colspan="6" style="background:#dedede" | active participle<br />{{{اِسْم الفَاعِل}}}
| colspan="7" | {{{ap}}}
]=]
	end

	if data.passive then
		text = text .. [=[
|-
! colspan="6" style="background:#dedede" | passive participle<br />{{{اِسْم المَفْعُول}}}
| colspan="7" | {{{pp}}}
]=]
	end

	if data.passive ~= "only" and data.passive ~= "only-impers" then
		text = text .. [=[
|-
! colspan="12" style="background:#bcbcbc" | active voice<br />{{{الفِعْل المَعْلُوم}}}
|-
! colspan="2" style="background:#cdcdcd" | 
! colspan="3" style="background:#cdcdcd" | singular<br />{{{المُفْرَد}}}
! rowspan="12" style="background:#cdcdcd;width:.5em" | 
! colspan="2" style="background:#cdcdcd" | dual<br />{{{المُثَنَّى}}}
! rowspan="12" style="background:#cdcdcd;width:.5em" | 
! colspan="3" style="background:#cdcdcd" | plural<br />{{{الجَمْع}}}
|-
! colspan="2" style="background:#cdcdcd" | 
! style="background:#cdcdcd" | 1<sup>st</sup> person<br />{{{المُتَكَلِّم}}}
! style="background:#cdcdcd" | 2<sup>nd</sup> person<br />{{{المُخَاطَب}}}
! style="background:#cdcdcd" | 3<sup>rd</sup> person<br />{{{الغَائِب}}}
! style="background:#cdcdcd" | 2<sup>nd</sup> person<br />{{{المُخَاطَب}}}
! style="background:#cdcdcd" | 3<sup>rd</sup> person<br />{{{الغَائِب}}}
! style="background:#cdcdcd" | 1<sup>st</sup> person<br />{{{المُتَكَلِّم}}}
! style="background:#cdcdcd" | 2<sup>nd</sup> person<br />{{{المُخَاطَب}}}
! style="background:#cdcdcd" | 3<sup>rd</sup> person<br />{{{الغَائِب}}}
|-
! rowspan="2" style="background:#cdcdcd" | past (perfect) indicative<br />{{{المَاضِي}}}
! style="background:#dedede" | ''m''
| rowspan="2" | {{{1s-perf}}}
| {{{2sm-perf}}}
| {{{3sm-perf}}}
| rowspan="2" | {{{2d-perf}}}
| {{{3dm-perf}}}
| rowspan="2" | {{{1p-perf}}}
| {{{2pm-perf}}}
| {{{3pm-perf}}}
|-
! style="background:#dedede" | ''f''
| {{{2sf-perf}}}
| {{{3sf-perf}}}
| {{{3df-perf}}}
| {{{2pf-perf}}}
| {{{3pf-perf}}}
|-
! rowspan="2" style="background:#cdcdcd" | non-past (imperfect) indicative<br />{{{المُضَارِع}}}
! style="background:#dedede" | ''m''
| rowspan="2" | {{{1s-impf}}}
| {{{2sm-impf}}}
| {{{3sm-impf}}}
| rowspan="2" | {{{2d-impf}}}
| {{{3dm-impf}}}
| rowspan="2" | {{{1p-impf}}}
| {{{2pm-impf}}}
| {{{3pm-impf}}}
|-
! style="background:#dedede" | ''f''
| {{{2sf-impf}}}
| {{{3sf-impf}}}
| {{{3df-impf}}}
| {{{2pf-impf}}}
| {{{3pf-impf}}}
|-
! rowspan="2" style="background:#cdcdcd" | subjunctive<br />{{{المُضَارِع المَنْصُوب}}}
! style="background:#dedede" | ''m''
| rowspan="2" | {{{1s-subj}}}
| {{{2sm-subj}}}
| {{{3sm-subj}}}
| rowspan="2" | {{{2d-subj}}}
| {{{3dm-subj}}}
| rowspan="2" | {{{1p-subj}}}
| {{{2pm-subj}}}
| {{{3pm-subj}}}
|-
! style="background:#dedede" | ''f''
| {{{2sf-subj}}}
| {{{3sf-subj}}}
| {{{3df-subj}}}
| {{{2pf-subj}}}
| {{{3pf-subj}}}
|-
! rowspan="2" style="background:#cdcdcd" | jussive<br />{{{المُضَارِع المَجْزُوم}}}
! style="background:#dedede" | ''m''
| rowspan="2" | {{{1s-juss}}}
| {{{2sm-juss}}}
| {{{3sm-juss}}}
| rowspan="2" | {{{2d-juss}}}
| {{{3dm-juss}}}
| rowspan="2" | {{{1p-juss}}}
| {{{2pm-juss}}}
| {{{3pm-juss}}}
|-
! style="background:#dedede" | ''f''
| {{{2sf-juss}}}
| {{{3sf-juss}}}
| {{{3df-juss}}}
| {{{2pf-juss}}}
| {{{3pf-juss}}}
|-
! rowspan="2" style="background:#cdcdcd" | imperative<br />{{{الأَمْر}}}
! style="background:#dedede" | ''m''
| rowspan="2" | 
| {{{2sm-impr}}}
| rowspan="2" | 
| rowspan="2" | {{{2d-impr}}}
| rowspan="2" | 
| rowspan="2" | 
| {{{2pm-impr}}}
| rowspan="2" | 
|-
! style="background:#dedede" | ''f''
| {{{2sf-impr}}}
| {{{2pf-impr}}}
]=]
	end

	if data.passive == "impers" or data.passive == "only-impers" then
		text = text .. [=[
|-
! colspan="12" style="background:#bcbcbc" | passive voice<br />{{{الفِعْل المَجْهُول}}}
|-
| colspan="2" style="background:#cdcdcd" | 
! colspan="3" style="background:#cdcdcd" | singular<br />{{{المُفْرَد}}}
| rowspan="10" style="background:#cdcdcd;width:.5em" | 
! colspan="2" style="background:#cdcdcd" | dual<br />{{{المُثَنَّى}}}
| rowspan="10" style="background:#cdcdcd;width:.5em" | 
! colspan="3" style="background:#cdcdcd" | plural<br />{{{الجَمْع}}}
|-
| colspan="2" style="background:#cdcdcd" | 
! style="background:#cdcdcd" | 1<sup>st</sup> person<br />{{{المُتَكَلِّم}}}
! style="background:#cdcdcd" | 2<sup>nd</sup> person<br />{{{المُخَاطَب}}}
! style="background:#cdcdcd" | 3<sup>rd</sup> person<br />{{{الغَائِب}}}
! style="background:#cdcdcd" | 2<sup>nd</sup> person<br />{{{المُخَاطَب}}}
! style="background:#cdcdcd" | 3<sup>rd</sup> person<br />{{{الغَائِب}}}
! style="background:#cdcdcd" | 1<sup>st</sup> person<br />{{{المُتَكَلِّم}}}
! style="background:#cdcdcd" | 2<sup>nd</sup> person<br />{{{المُخَاطَب}}}
! style="background:#cdcdcd" | 3<sup>rd</sup> person<br />{{{الغَائِب}}}
|-
! rowspan="2" style="background:#cdcdcd" | past (perfect) indicative<br />{{{المَاضِي}}}
! style="background:#dedede" | ''m''
| rowspan="2" | &mdash;
| &mdash;
| {{{3sm-ps-perf}}}
| rowspan="2" | &mdash;
| &mdash;
| rowspan="2" | &mdash;
| &mdash;
| &mdash;
|-
! style="background:#dedede" | ''f''
| &mdash;
| &mdash;
| &mdash;
| &mdash;
| &mdash;
|-
! rowspan="2" style="background:#cdcdcd" | non-past (imperfect) indicative<br />{{{المُضَارِع}}}
! style="background:#dedede" | ''m''
| rowspan="2" | &mdash;
| &mdash;
| {{{3sm-ps-impf}}}
| rowspan="2" | &mdash;
| &mdash;
| rowspan="2" | &mdash;
| &mdash;
| &mdash;
|-
! style="background:#dedede" | ''f''
| &mdash;
| &mdash;
| &mdash;
| &mdash;
| &mdash;
|-
! rowspan="2" style="background:#cdcdcd" | subjunctive<br />{{{المُضَارِع المَنْصُوب}}}
! style="background:#dedede" | ''m''
| rowspan="2" | &mdash;
| &mdash;
| {{{3sm-ps-subj}}}
| rowspan="2" | &mdash;
| &mdash;
| rowspan="2" | &mdash;
| &mdash;
| &mdash;
|-
! style="background:#dedede" | ''f''
| &mdash;
| &mdash;
| &mdash;
| &mdash;
| &mdash;
|-
! rowspan="2" style="background:#cdcdcd" | jussive<br />{{{المُضَارِع المَجْزُوم}}}
! style="background:#dedede" | ''m''
| rowspan="2" | &mdash;
| &mdash;
| {{{3sm-ps-juss}}}
| rowspan="2" | &mdash;
| &mdash;
| rowspan="2" | &mdash;
| &mdash;
| &mdash;
|-
! style="background:#dedede" | ''f''
| &mdash;
| &mdash;
| &mdash;
| &mdash;
| &mdash;
]=]

	elseif data.passive then
		text = text .. [=[
|-
! colspan="12" style="background:#bcbcbc" | passive voice<br />{{{الفِعْل المَجْهُول}}}
|-
| colspan="2" style="background:#cdcdcd" | 
! colspan="3" style="background:#cdcdcd" | singular<br />{{{المُفْرَد}}}
| rowspan="10" style="background:#cdcdcd;width:.5em" | 
! colspan="2" style="background:#cdcdcd" | dual<br />{{{المُثَنَّى}}}
| rowspan="10" style="background:#cdcdcd;width:.5em" | 
! colspan="3" style="background:#cdcdcd" | plural<br />{{{الجَمْع}}}
|-
| colspan="2" style="background:#cdcdcd" | 
! style="background:#cdcdcd" | 1<sup>st</sup> person<br />{{{المُتَكَلِّم}}}
! style="background:#cdcdcd" | 2<sup>nd</sup> person<br />{{{المُخَاطَب}}}
! style="background:#cdcdcd" | 3<sup>rd</sup> person<br />{{{الغَائِب}}}
! style="background:#cdcdcd" | 2<sup>nd</sup> person<br />{{{المُخَاطَب}}}
! style="background:#cdcdcd" | 3<sup>rd</sup> person<br />{{{الغَائِب}}}
! style="background:#cdcdcd" | 1<sup>st</sup> person<br />{{{المُتَكَلِّم}}}
! style="background:#cdcdcd" | 2<sup>nd</sup> person<br />{{{المُخَاطَب}}}
! style="background:#cdcdcd" | 3<sup>rd</sup> person<br />{{{الغَائِب}}}
|-
! rowspan="2" style="background:#cdcdcd" | past (perfect) indicative<br />{{{المَاضِي}}}
! style="background:#dedede" | ''m''
| rowspan="2" | {{{1s-ps-perf}}}
| {{{2sm-ps-perf}}}
| {{{3sm-ps-perf}}}
| rowspan="2" | {{{2d-ps-perf}}}
| {{{3dm-ps-perf}}}
| rowspan="2" | {{{1p-ps-perf}}}
| {{{2pm-ps-perf}}}
| {{{3pm-ps-perf}}}
|-
! style="background:#dedede" | ''f''
| {{{2sf-ps-perf}}}
| {{{3sf-ps-perf}}}
| {{{3df-ps-perf}}}
| {{{2pf-ps-perf}}}
| {{{3pf-ps-perf}}}
|-
! rowspan="2" style="background:#cdcdcd" | non-past (imperfect) indicative<br />{{{المُضَارِع}}}
! style="background:#dedede" | ''m''
| rowspan="2" | {{{1s-ps-impf}}}
| {{{2sm-ps-impf}}}
| {{{3sm-ps-impf}}}
| rowspan="2" | {{{2d-ps-impf}}}
| {{{3dm-ps-impf}}}
| rowspan="2" | {{{1p-ps-impf}}}
| {{{2pm-ps-impf}}}
| {{{3pm-ps-impf}}}
|-
! style="background:#dedede" | ''f''
| {{{2sf-ps-impf}}}
| {{{3sf-ps-impf}}}
| {{{3df-ps-impf}}}
| {{{2pf-ps-impf}}}
| {{{3pf-ps-impf}}}
|-
! rowspan="2" style="background:#cdcdcd" | subjunctive<br />{{{المُضَارِع المَنْصُوب}}}
! style="background:#dedede" | ''m''
| rowspan="2" | {{{1s-ps-subj}}}
| {{{2sm-ps-subj}}}
| {{{3sm-ps-subj}}}
| rowspan="2" | {{{2d-ps-subj}}}
| {{{3dm-ps-subj}}}
| rowspan="2" | {{{1p-ps-subj}}}
| {{{2pm-ps-subj}}}
| {{{3pm-ps-subj}}}
|-
! style="background:#dedede" | ''f''
| {{{2sf-ps-subj}}}
| {{{3sf-ps-subj}}}
| {{{3df-ps-subj}}}
| {{{2pf-ps-subj}}}
| {{{3pf-ps-subj}}}
|-
! rowspan="2" style="background:#cdcdcd" | jussive<br />{{{المُضَارِع المَجْزُوم}}}
! style="background:#dedede" | ''m''
| rowspan="2" | {{{1s-ps-juss}}}
| {{{2sm-ps-juss}}}
| {{{3sm-ps-juss}}}
| rowspan="2" | {{{2d-ps-juss}}}
| {{{3dm-ps-juss}}}
| rowspan="2" | {{{1p-ps-juss}}}
| {{{2pm-ps-juss}}}
| {{{3pm-ps-juss}}}
|-
! style="background:#dedede" | ''f''
| {{{2sf-ps-juss}}}
| {{{3sf-ps-juss}}}
| {{{3df-ps-juss}}}
| {{{2pf-ps-juss}}}
| {{{3pf-ps-juss}}}
]=]
	end

	text = text .. [=[
|}
</div>
</div>]=]

	-- Function used as replace arg of call to rsub(). Replace the
	-- specified param with its (HTML) value. The param references appear
	-- as {{{PARAM}}} in the wikicode. Also call tag_text on Arabic text
	-- appearing as {{{ARABIC-TEXT}}}.
	local function repl(param)
		if rfind(param, "^[A-Za-z0-9_-]+$") then
			return data.forms[param]
		else
			return tag_text(param)
		end
	end

	return rsub(text, "{{{([^{}]+)}}}", repl)
end

return export

-- For Vim, so we get 4-space tabs
-- vim: set ts=4 sw=4 noet: