local M = {} local records = setmetatable({}, { __mode = "k" }) local record_types = setmetatable({}, { __mode = "k" }) local methods = {} local formatters = {} local equalities = {} local metatables = {} local function fail(message, level) error(message, (level or 1) + 1) end local function is_integer(value) return type(value) == "number" and value > -math.huge and value < math.huge and value == math.floor(value) end local function register_type(type_name, type_methods, formatter, equality) methods[type_name] = type_methods or {} formatters[type_name] = formatter equalities[type_name] = equality local mt = { __index = function(value, key) local field = records[value][key] if field ~= nil then return field end return methods[type_name][key] end, __newindex = function() fail(type_name .. " values are immutable", 2) end, __tostring = function(value) local format = formatters[type_name] if format then return format(records[value]) end return type_name end, __eq = function(left, right) if record_types[left] ~= type_name or record_types[right] ~= type_name then return false end local equal = equalities[type_name] if equal then return equal(records[left], records[right]) end return rawequal(left, right) end, __metatable = "clever_f.domain." .. type_name, } metatables[type_name] = mt end local function new_record(type_name, fields) local value = {} records[value] = fields record_types[value] = type_name return setmetatable(value, metatables[type_name]) end local function is_record(value, type_name) return record_types[value] == type_name end local function require_record(value, type_name, name) if not is_record(value, type_name) then fail((name or "value") .. " must be a " .. type_name, 2) end return value end local function require_string(value, name, allow_empty) if type(value) ~= "string" or (not allow_empty and value == "") then fail(name .. " must be " .. (allow_empty and "a string" or "a nonempty string"), 2) end return value end local function require_boolean(value, name) if type(value) ~= "boolean" then fail(name .. " must be a Boolean", 2) end return value end local function require_nonnegative_integer(value, name) if not is_integer(value) or value < 0 then fail(name .. " must be a nonnegative integer", 2) end return value end function M.type_of(value) return record_types[value] end local function define_enum(type_name, entries) local enum_methods = {} local namespace = {} local by_value = {} register_type(type_name, enum_methods, function(data) return data.value end) for constant, serialized in pairs(entries) do local value = new_record(type_name, { name = constant, value = serialized, }) namespace[constant] = value by_value[serialized] = value end function namespace.from_string(value) if is_record(value, type_name) then return value end local result = by_value[value] if result == nil then fail("value must be a valid " .. type_name, 2) end return result end function namespace.is(value) return is_record(value, type_name) end function enum_methods:to_string() return records[self].value end return namespace end M.Family = define_enum("Family", { FIND = "FIND", TILL = "TILL", }) M.Direction = define_enum("Direction", { FORWARD = "forward", BACKWARD = "backward", }) M.SelectionKind = define_enum("SelectionKind", { NONE = "none", CHARACTER = "character", LINE = "line", BLOCK = "block", }) M.SelectionOption = define_enum("SelectionOption", { INCLUSIVE = "inclusive", EXCLUSIVE = "exclusive", }) M.TargetKind = define_enum("TargetKind", { CHARACTER = "character", SPECIAL_KEY = "special_key", CODE_FALLBACK = "code_fallback", }) M.CaseMode = define_enum("CaseMode", { SENSITIVE = "sensitive", INSENSITIVE = "insensitive", }) M.TargetPlanKind = define_enum("TargetPlanKind", { EMPTY = "empty", LITERAL = "literal", BACKSLASH = "backslash", SYMBOL = "symbol", MIGEMO = "migemo", }) M.SearchScope = define_enum("SearchScope", { BUFFER = "buffer", CURRENT_LINE = "current_line", }) M.EndpointPolicy = define_enum("EndpointPolicy", { REGULAR = "regular", VISUAL_EXCLUSIVE = "visual_exclusive", }) M.SearchStatus = define_enum("SearchStatus", { COMPLETE = "complete", BOUNDARY_AFTER_PARTIAL = "boundary_after_partial", BOUNDARY_BEFORE_ANY = "boundary_before_any", }) M.ActionKind = define_enum("ActionKind", { MOVEMENT = "movement", NEUTRAL = "neutral", ESCAPE = "escape", FAILED_SEARCH = "failed_search", ERROR = "error", EMPTY = "empty", }) local Position = {} M.Position = Position register_type("Position", Position, function(data) return string.format("(%d,%d)", data.line, data.byte_column) end, function(left, right) return left.line == right.line and left.byte_column == right.byte_column end) function Position.new(line, byte_column) if not is_integer(line) or line < 1 then fail("line must be a positive one-based integer", 2) end if not is_integer(byte_column) or byte_column < 1 then fail("byte_column must be a positive one-based integer", 2) end return new_record("Position", { line = line, byte_column = byte_column, }) end function Position.coerce(value) if Position.is(value) then return value end if type(value) ~= "table" then fail("position must be a Position or position table", 2) end return Position.new(value.line, value.byte_column) end function Position.is(value) return is_record(value, "Position") end function Position.compare(left, right) require_record(left, "Position", "left") require_record(right, "Position", "right") if left.line < right.line then return -1 end if left.line > right.line then return 1 end if left.byte_column < right.byte_column then return -1 end if left.byte_column > right.byte_column then return 1 end return 0 end function Position.equal(left, right) return Position.compare(left, right) == 0 end function Position.stationary(left, right) return Position.equal(left, right) end function Position.is_forward(candidate, origin) return Position.compare(candidate, origin) > 0 end function Position.is_backward(candidate, origin) return Position.compare(candidate, origin) < 0 end function Position:to_table() return { line = self.line, byte_column = self.byte_column, } end local Descriptor = {} M.Descriptor = Descriptor register_type("Descriptor", Descriptor, function(data) return data.value end) local descriptors_by_string = {} local descriptors_by_parts = {} local function descriptor_key(family, direction) return family.value .. ":" .. direction.value end local function define_descriptor(name, serialized, family, direction) local descriptor = new_record("Descriptor", { name = name, value = serialized, family = family, direction = direction, uppercase = serialized:match("%u") ~= nil, }) descriptors_by_string[serialized] = descriptor descriptors_by_parts[descriptor_key(family, direction)] = descriptor Descriptor[name] = descriptor Descriptor[serialized] = descriptor return descriptor end Descriptor.FIND_FORWARD = define_descriptor( "FIND_FORWARD", "f", M.Family.FIND, M.Direction.FORWARD ) Descriptor.FIND_BACKWARD = define_descriptor( "FIND_BACKWARD", "F", M.Family.FIND, M.Direction.BACKWARD ) Descriptor.TILL_FORWARD = define_descriptor( "TILL_FORWARD", "t", M.Family.TILL, M.Direction.FORWARD ) Descriptor.TILL_BACKWARD = define_descriptor( "TILL_BACKWARD", "T", M.Family.TILL, M.Direction.BACKWARD ) function Descriptor.is(value) return is_record(value, "Descriptor") end function Descriptor.is_valid(value) return Descriptor.is(value) or descriptors_by_string[value] ~= nil end function Descriptor.from_string(value) if Descriptor.is(value) then return value end local descriptor = descriptors_by_string[value] if descriptor == nil then fail("descriptor must be one of f, F, t, or T", 2) end return descriptor end function Descriptor.try_from_string(value) if Descriptor.is(value) then return value end return descriptors_by_string[value] end function Descriptor.from_parts(family, direction) family = M.Family.from_string(family) direction = M.Direction.from_string(direction) return descriptors_by_parts[descriptor_key(family, direction)] end function Descriptor.to_string(value) return Descriptor.from_string(value).value end function Descriptor.is_uppercase(value) return Descriptor.from_string(value).uppercase end function Descriptor.is_lowercase(value) return not Descriptor.is_uppercase(value) end function Descriptor.swap(value) local descriptor = Descriptor.from_string(value) local direction = descriptor.direction == M.Direction.FORWARD and M.Direction.BACKWARD or M.Direction.FORWARD return Descriptor.from_parts(descriptor.family, direction) end function Descriptor.lowercase(value) local descriptor = Descriptor.from_string(value) return Descriptor.from_parts(descriptor.family, M.Direction.FORWARD) end function Descriptor.uppercase(value) local descriptor = Descriptor.from_string(value) return Descriptor.from_parts(descriptor.family, M.Direction.BACKWARD) end local Count = {} M.Count = Count register_type("Count", Count, function(data) return tostring(data.value) end, function(left, right) return left.value == right.value end) local count_one function Count.new(value) if Count.is(value) then return value end if value == nil then value = 1 end if not is_integer(value) or value < 1 then fail("count must be a positive integer", 2) end if value == 1 and count_one ~= nil then return count_one end local count = new_record("Count", { value = value }) if value == 1 then count_one = count end return count end function Count.is(value) return is_record(value, "Count") end function Count.to_number(value) return Count.new(value).value end Count.ONE = Count.new(1) local ModeContext = {} M.ModeContext = ModeContext register_type("ModeContext", ModeContext, function(data) return data.key end) local mode_contexts = {} local CTRL_V = string.char(0x16) local CTRL_S = string.char(0x13) local operator_modes = { no = true, nov = true, noV = true, ["no" .. CTRL_V] = true, } local function mode_traits(full_mode) local operator = operator_modes[full_mode] == true local visual_kind local select_kind if not operator then local lead = full_mode:sub(1, 1) if lead == "v" then visual_kind = M.SelectionKind.CHARACTER elseif lead == "V" then visual_kind = M.SelectionKind.LINE elseif lead == CTRL_V then visual_kind = M.SelectionKind.BLOCK elseif lead == "s" then select_kind = M.SelectionKind.CHARACTER elseif lead == "S" then select_kind = M.SelectionKind.LINE elseif lead == CTRL_S then select_kind = M.SelectionKind.BLOCK end end return operator, visual_kind, select_kind end function ModeContext.from_full_mode(full_mode) if ModeContext.is(full_mode) then return full_mode end require_string(full_mode, "full_mode", false) local operator, visual_kind, select_kind = mode_traits(full_mode) local key = operator and "no" or full_mode local context = mode_contexts[key] if context ~= nil then return context end context = new_record("ModeContext", { key = key, full_mode = key, operator = operator, visual_kind = visual_kind, select_kind = select_kind, visual = visual_kind ~= nil, select = select_kind ~= nil, command_path = visual_kind == nil, }) mode_contexts[key] = context return context end function ModeContext.is(value) return is_record(value, "ModeContext") end function ModeContext.equal(left, right) require_record(left, "ModeContext", "left") require_record(right, "ModeContext", "right") return left.key == right.key end function ModeContext:to_key() return self.key end local Selection = {} M.Selection = Selection register_type("Selection", Selection, function(data) return data.active and ("selection:" .. data.kind.value) or "selection:none" end, function(left, right) return left.active == right.active and left.kind == right.kind and left.anchor == right.anchor and left.focus == right.focus and left.option == right.option end) function Selection.new(options) if Selection.is(options) then return options end if type(options) ~= "table" then fail("selection options must be a table", 2) end local active = require_boolean(options.active, "selection.active") local kind = M.SelectionKind.from_string(options.kind) local option = M.SelectionOption.from_string(options.option or "inclusive") local anchor = options.anchor local focus = options.focus if active then if kind == M.SelectionKind.NONE then fail("an active selection must have a selection kind", 2) end anchor = Position.coerce(anchor) focus = Position.coerce(focus) else if kind ~= M.SelectionKind.NONE then fail("an inactive selection must use the none kind", 2) end if anchor ~= nil or focus ~= nil then fail("an inactive selection must have empty endpoints", 2) end end return new_record("Selection", { active = active, kind = kind, anchor = anchor, focus = focus, option = option, }) end function Selection.inactive(option) return Selection.new({ active = false, kind = M.SelectionKind.NONE, option = option or M.SelectionOption.INCLUSIVE, }) end function Selection.active(kind, anchor, focus, option) return Selection.new({ active = true, kind = kind, anchor = anchor, focus = focus, option = option or M.SelectionOption.INCLUSIVE, }) end function Selection.is(value) return is_record(value, "Selection") end function Selection:with_focus(focus, kind) if not self.active then fail("selection must be active", 2) end return Selection.active(kind or self.kind, self.anchor, focus, self.option) end function Selection:to_table() return { active = self.active, kind = self.kind.value, anchor = self.anchor and self.anchor:to_table() or nil, focus = self.focus and self.focus:to_table() or nil, option = self.option.value, } end local TextSnapshot = {} M.TextSnapshot = TextSnapshot local text_lines = setmetatable({}, { __mode = "k" }) register_type("TextSnapshot", TextSnapshot, function(data) return "text:" .. tostring(data.line_count) .. " lines" end, function(left, right) if left.line_count ~= right.line_count then return false end local left_lines = text_lines[left.identity] local right_lines = text_lines[right.identity] for index = 1, left.line_count do if left_lines[index] ~= right_lines[index] then return false end end return true end) function TextSnapshot.new(lines) if TextSnapshot.is(lines) then return lines end if type(lines) ~= "table" or #lines < 1 then fail("text lines must be a nonempty list", 2) end local copy = {} for index = 1, #lines do if type(lines[index]) ~= "string" then fail("each text line must be a string", 2) end copy[index] = lines[index] end local identity = {} text_lines[identity] = copy return new_record("TextSnapshot", { identity = identity, line_count = #copy, }) end function TextSnapshot.is(value) return is_record(value, "TextSnapshot") end function TextSnapshot:line(line_number) if not is_integer(line_number) or line_number < 1 or line_number > self.line_count then fail("line_number must identify a line in the text snapshot", 2) end return text_lines[self.identity][line_number] end function TextSnapshot:lines() local result = {} local source = text_lines[self.identity] for index = 1, self.line_count do result[index] = source[index] end return result end function TextSnapshot:to_table() return { lines = self:lines() } end local MacroState = {} M.MacroState = MacroState register_type("MacroState", MacroState, function(data) return data.executing and ("macro:" .. data.register) or "macro:inactive" end, function(left, right) return left.register == right.register end) function MacroState.new(register) if MacroState.is(register) then return register end if register == "" then register = nil end if register ~= nil then require_string(register, "macro register", false) end return new_record("MacroState", { register = register, executing = register ~= nil, }) end function MacroState.is(value) return is_record(value, "MacroState") end local FoldState = {} M.FoldState = FoldState local fold_policies = setmetatable({}, { __mode = "k" }) register_type("FoldState", FoldState, function(data) return "folds:" .. tostring(data.closed_levels) end) function FoldState.new(open_policy, closed_levels) if FoldState.is(open_policy) and closed_levels == nil then return open_policy end if type(open_policy) ~= "table" then fail("fold open policy must be a list", 2) end require_nonnegative_integer(closed_levels, "closed fold levels") local identity = {} local policies = {} local seen = {} for index = 1, #open_policy do local policy = require_string(open_policy[index], "fold policy item", false) if not seen[policy] then seen[policy] = true policies[#policies + 1] = policy end end fold_policies[identity] = { list = policies, set = seen, } return new_record("FoldState", { identity = identity, closed_levels = closed_levels, }) end function FoldState.is(value) return is_record(value, "FoldState") end function FoldState:opens(policy) require_string(policy, "fold policy", false) return fold_policies[self.identity].set[policy] == true end function FoldState:policies() local result = {} local source = fold_policies[self.identity].list for index = 1, #source do result[index] = source[index] end return result end local InputPacket = {} M.InputPacket = InputPacket local packet_bytes = setmetatable({}, { __mode = "k" }) M.InputPacketKind = define_enum("InputPacketKind", { TEXT = "text", RAW_BYTES = "raw_bytes", SPECIAL_KEY = "special_key", ERROR = "error", }) register_type("InputPacket", InputPacket, function(data) return "input:" .. data.kind.value end) function InputPacket.text(text) require_string(text, "input text", false) return new_record("InputPacket", { kind = M.InputPacketKind.TEXT, text = text, }) end function InputPacket.raw_bytes(bytes) if type(bytes) ~= "table" or #bytes < 1 then fail("raw input bytes must be a nonempty list", 2) end local copy = {} for index = 1, #bytes do local byte = bytes[index] if not is_integer(byte) or byte < 0 or byte > 255 then fail("raw input bytes must contain byte values", 2) end copy[index] = byte end local identity = {} packet_bytes[identity] = copy return new_record("InputPacket", { kind = M.InputPacketKind.RAW_BYTES, identity = identity, }) end function InputPacket.special_key(name, encoded) require_string(name, "special key name", false) if encoded ~= nil then require_string(encoded, "encoded special key", false) end return new_record("InputPacket", { kind = M.InputPacketKind.SPECIAL_KEY, name = name, encoded = encoded, }) end function InputPacket.error(message) require_string(message, "input error message", false) return new_record("InputPacket", { kind = M.InputPacketKind.ERROR, message = message, }) end function InputPacket.from_table(packet) if InputPacket.is(packet) then return packet end if type(packet) ~= "table" then fail("input packet must be an InputPacket or packet table", 2) end local kind = M.InputPacketKind.from_string(packet.kind) if kind == M.InputPacketKind.TEXT then return InputPacket.text(packet.text) end if kind == M.InputPacketKind.RAW_BYTES then return InputPacket.raw_bytes(packet.bytes) end if kind == M.InputPacketKind.SPECIAL_KEY then return InputPacket.special_key(packet.name, packet.encoded) end return InputPacket.error(packet.message) end function InputPacket.is(value) return is_record(value, "InputPacket") end function InputPacket:bytes() if self.kind ~= M.InputPacketKind.RAW_BYTES then return nil end local source = packet_bytes[self.identity] local result = {} for index = 1, #source do result[index] = source[index] end return result end function InputPacket:to_table() local result = { kind = self.kind.value } if self.kind == M.InputPacketKind.TEXT then result.text = self.text elseif self.kind == M.InputPacketKind.RAW_BYTES then result.bytes = self:bytes() elseif self.kind == M.InputPacketKind.SPECIAL_KEY then result.name = self.name result.encoded = self.encoded else result.message = self.message end return result end local TargetValue = {} M.TargetValue = TargetValue register_type("TargetValue", TargetValue, function(data) return "target:" .. data.kind.value .. ":" .. tostring(data.first_code) end, function(left, right) return left.kind == right.kind and left.value == right.value and left.first_code == right.first_code end) function TargetValue.character(value, first_code) require_string(value, "target character", false) require_nonnegative_integer(first_code, "target first code") return new_record("TargetValue", { kind = M.TargetKind.CHARACTER, value = value, first_code = first_code, }) end function TargetValue.special_key(value, first_code) require_string(value, "encoded special key", false) first_code = first_code or string.byte(value, 1) require_nonnegative_integer(first_code, "target first code") if first_code ~= 0x80 then fail("an encoded special key must start with hexadecimal 80", 2) end return new_record("TargetValue", { kind = M.TargetKind.SPECIAL_KEY, value = value, first_code = first_code, }) end function TargetValue.code_fallback(first_code) first_code = first_code or 0 require_nonnegative_integer(first_code, "fallback character code") return new_record("TargetValue", { kind = M.TargetKind.CODE_FALLBACK, value = "", first_code = first_code, }) end function TargetValue.from_table(target) if TargetValue.is(target) then return target end if type(target) ~= "table" then fail("target must be a TargetValue or target table", 2) end local kind = M.TargetKind.from_string(target.kind) if kind == M.TargetKind.CHARACTER then return TargetValue.character(target.value, target.first_code) end if kind == M.TargetKind.SPECIAL_KEY then return TargetValue.special_key(target.value, target.first_code) end return TargetValue.code_fallback(target.first_code) end function TargetValue.is(value) return is_record(value, "TargetValue") end function TargetValue:to_table() return { kind = self.kind.value, value = self.value, first_code = self.first_code, } end local TargetPlan = {} M.TargetPlan = TargetPlan register_type("TargetPlan", TargetPlan, function(data) return "target-plan:" .. data.kind.value end) function TargetPlan.new(options) if TargetPlan.is(options) then return options end if type(options) ~= "table" then fail("target plan options must be a table", 2) end local target = require_record(options.target, "TargetValue", "target plan target") local kind = M.TargetPlanKind.from_string(options.kind) local case_mode = M.CaseMode.from_string(options.case_mode) if type(options.matcher) ~= "function" then fail("target plan matcher must be a function", 2) end return new_record("TargetPlan", { target = target, kind = kind, case_mode = case_mode, matcher = options.matcher, }) end function TargetPlan.is(value) return is_record(value, "TargetPlan") end function TargetPlan:matches(...) return self.matcher(...) end function TargetPlan:matches_at(text_view, position) if type(text_view) ~= "table" or type(text_view.character_at) ~= "function" then fail("target plan match requires a text view", 2) end position = Position.coerce(position) return self.matcher(text_view:character_at(position), position, text_view) end function TargetPlan:to_table() return { target = self.target:to_table(), kind = self.kind.value, case_mode = self.case_mode.value, } end local ResolvedMotionPlan = {} M.ResolvedMotionPlan = ResolvedMotionPlan register_type("ResolvedMotionPlan", ResolvedMotionPlan, function(data) return "motion-plan:" .. data.descriptor.value end) function ResolvedMotionPlan.new(options) if ResolvedMotionPlan.is(options) then return options end if type(options) ~= "table" then fail("resolved motion plan options must be a table", 2) end return new_record("ResolvedMotionPlan", { target_plan = require_record(options.target_plan, "TargetPlan", "target plan"), descriptor = Descriptor.from_string(options.descriptor), search_scope = M.SearchScope.from_string(options.search_scope), endpoint_policy = M.EndpointPolicy.from_string(options.endpoint_policy), }) end function ResolvedMotionPlan.is(value) return is_record(value, "ResolvedMotionPlan") end function ResolvedMotionPlan:to_table() return { target_plan = self.target_plan:to_table(), descriptor = self.descriptor.value, search_scope = self.search_scope.value, endpoint_policy = self.endpoint_policy.value, } end local MotionRequest = {} M.MotionRequest = MotionRequest register_type("MotionRequest", MotionRequest, function(data) return "motion-request:" .. data.descriptor.value end) function MotionRequest.new(options) if MotionRequest.is(options) then return options end if type(options) ~= "table" then fail("motion request options must be a table", 2) end if options.policy == nil then fail("motion request policy is required", 2) end return new_record("MotionRequest", { context = require_record(options.context, "ModeContext", "motion context"), origin = Position.coerce(options.origin), descriptor = Descriptor.from_string(options.descriptor), target = require_record(options.target, "TargetValue", "motion target"), count = Count.new(options.count), policy = options.policy, first_move = require_boolean(options.first_move, "first_move"), }) end function MotionRequest.is(value) return is_record(value, "MotionRequest") end local SearchOutcome = {} M.SearchOutcome = SearchOutcome register_type("SearchOutcome", SearchOutcome, function(data) return "search:" .. data.status.value end) local function new_search_outcome(status, endpoint, successful_steps) status = M.SearchStatus.from_string(status) endpoint = Position.coerce(endpoint) require_nonnegative_integer(successful_steps, "successful_steps") if status == M.SearchStatus.COMPLETE and successful_steps < 1 then fail("a complete search must contain a successful step", 3) end if status == M.SearchStatus.BOUNDARY_AFTER_PARTIAL and successful_steps < 1 then fail("a partial search must contain a successful step", 3) end if status == M.SearchStatus.BOUNDARY_BEFORE_ANY and successful_steps ~= 0 then fail("a boundary-before-any search must contain zero successful steps", 3) end return new_record("SearchOutcome", { status = status, endpoint = endpoint, successful_steps = successful_steps, complete = status == M.SearchStatus.COMPLETE, }) end function SearchOutcome.new(options) if SearchOutcome.is(options) then return options end if type(options) ~= "table" then fail("search outcome options must be a table", 2) end return new_search_outcome(options.status, options.endpoint, options.successful_steps) end function SearchOutcome.complete(endpoint, successful_steps) return new_search_outcome(M.SearchStatus.COMPLETE, endpoint, successful_steps) end function SearchOutcome.boundary_after_partial(endpoint, successful_steps) return new_search_outcome( M.SearchStatus.BOUNDARY_AFTER_PARTIAL, endpoint, successful_steps ) end function SearchOutcome.boundary_before_any(origin) return new_search_outcome(M.SearchStatus.BOUNDARY_BEFORE_ANY, origin, 0) end function SearchOutcome.is(value) return is_record(value, "SearchOutcome") end function SearchOutcome:to_table() return { status = self.status.value, endpoint = self.endpoint:to_table(), successful_steps = self.successful_steps, complete = self.complete, } end local DotPayload = {} M.DotPayload = DotPayload register_type("DotPayload", DotPayload, function(data) return "dot:" .. data.descriptor.value end, function(left, right) return left.descriptor == right.descriptor and left.target == right.target end) function DotPayload.new(descriptor, target) if DotPayload.is(descriptor) and target == nil then return descriptor end return new_record("DotPayload", { descriptor = Descriptor.from_string(descriptor), target = require_record(target, "TargetValue", "dot target"), }) end function DotPayload.is(value) return is_record(value, "DotPayload") end function DotPayload:to_table() return { descriptor = self.descriptor.value, target = self.target:to_table(), } end local ActionOutcome = {} M.ActionOutcome = ActionOutcome register_type("ActionOutcome", ActionOutcome, function(data) return "action:" .. data.kind.value end) local function new_action_outcome(options) local kind = M.ActionKind.from_string(options.kind) local position = Position.coerce(options.position) local search_outcome = options.search_outcome local descriptor = options.effective_descriptor local dot_payload = options.dot_payload if search_outcome ~= nil then require_record(search_outcome, "SearchOutcome", "search outcome") end if descriptor ~= nil then descriptor = Descriptor.from_string(descriptor) end if dot_payload ~= nil then require_record(dot_payload, "DotPayload", "dot payload") end if options.diagnostic ~= nil then require_string(options.diagnostic, "diagnostic", false) end if kind == M.ActionKind.MOVEMENT then if search_outcome == nil or not search_outcome.complete then fail("a movement action requires a complete search outcome", 3) end elseif kind == M.ActionKind.FAILED_SEARCH then if search_outcome == nil or search_outcome.complete then fail("a failed-search action requires an incomplete search outcome", 3) end elseif search_outcome ~= nil then fail("only movement and failed-search actions can contain a search outcome", 3) end if kind == M.ActionKind.ERROR and options.diagnostic == nil then fail("an error action requires a diagnostic", 3) end local complete if search_outcome ~= nil then complete = search_outcome.complete end return new_record("ActionOutcome", { kind = kind, position = position, search_outcome = search_outcome, complete = complete, successful_steps = search_outcome and search_outcome.successful_steps or 0, effective_descriptor = descriptor, dot_payload = dot_payload, diagnostic = options.diagnostic, }) end function ActionOutcome.new(options) if ActionOutcome.is(options) then return options end if type(options) ~= "table" then fail("action outcome options must be a table", 2) end return new_action_outcome(options) end function ActionOutcome.from_search(search_outcome, descriptor, dot_payload) require_record(search_outcome, "SearchOutcome", "search outcome") return new_action_outcome({ kind = search_outcome.complete and M.ActionKind.MOVEMENT or M.ActionKind.FAILED_SEARCH, position = search_outcome.endpoint, search_outcome = search_outcome, effective_descriptor = descriptor, dot_payload = dot_payload, }) end local function simple_action(kind, position, diagnostic) return new_action_outcome({ kind = kind, position = position, diagnostic = diagnostic, }) end function ActionOutcome.neutral(position) return simple_action(M.ActionKind.NEUTRAL, position) end function ActionOutcome.escape(position) return simple_action(M.ActionKind.ESCAPE, position) end function ActionOutcome.empty(position) return simple_action(M.ActionKind.EMPTY, position) end function ActionOutcome.error(position, diagnostic) return simple_action(M.ActionKind.ERROR, position, diagnostic) end function ActionOutcome.is(value) return is_record(value, "ActionOutcome") end function ActionOutcome:to_table() return { kind = self.kind.value, position = self.position:to_table(), complete = self.complete, successful_steps = self.successful_steps, effective_descriptor = self.effective_descriptor and self.effective_descriptor.value or nil, dot_payload = self.dot_payload and self.dot_payload:to_table() or nil, diagnostic = self.diagnostic, } end return M