From bde4a1fe3d05cc1dd26755b70810309b12873016 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Mon, 22 Sep 2025 16:14:43 -0700 Subject: [PATCH] move replacement processing to root.zig --- src/main.zig | 138 ++++++++++++++------------------------------------- src/root.zig | 94 +++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 101 deletions(-) diff --git a/src/main.zig b/src/main.zig index fcebca0..2adc149 100644 --- a/src/main.zig +++ b/src/main.zig @@ -284,114 +284,50 @@ pub fn main() !u8 { } fn processCommand(allocator: std.mem.Allocator, sentence: [:0]const u8, parser: *pos.Parser, devices: *std.StringHashMap([]const u8)) !void { - // Try original sentence first, then with word replacements - const replacement_keys = word_replacements.keys(); - const replacement_values = word_replacements.values(); + var tree = parser.adaptiveParse(sentence, word_replacements) catch |err| { + std.log.err("Failed to parse sentence with all replacements: {}\n", .{err}); + return error.UnrecognizedSentence; + }; + defer tree.deinit(); - var sentence_to_try: [:0]const u8 = sentence; - var replaced_sentence: ?[:0]u8 = null; - defer if (replaced_sentence) |s| allocator.free(s); + const action_words = try tree.sentenceAction(); + defer allocator.free(action_words); - var success = false; - for (0..replacement_keys.len + 1) |attempt| { - if (attempt > 0) { - // Create sentence with replacements - if (replaced_sentence) |s| allocator.free(s); - const temp = try std.mem.replaceOwned( - u8, - allocator, - sentence_to_try, - replacement_keys[attempt - 1], - replacement_values[attempt - 1], - ); - replaced_sentence = try allocator.dupeZ(u8, temp); - allocator.free(temp); - if (std.mem.eql(u8, sentence_to_try, replaced_sentence.?)) continue; - sentence_to_try = replaced_sentence.?; - std.log.info("Replaced word '{s}' in sentence with replacement '{s}'. Trying sentence: {s}", .{ - replacement_keys[attempt - 1], - replacement_values[attempt - 1], - sentence_to_try, - }); - } + const object_words = try tree.sentenceObject(); + defer allocator.free(object_words); - var tree = parser.parse(sentence_to_try) catch |err| { - std.log.err("Failed to parse sentence: {}\n", .{err}); - continue; - }; - defer tree.deinit(); + std.log.debug("{f}", .{tree}); + var aw = std.Io.Writer.Allocating.init(allocator); + defer aw.deinit(); + const aw_writer = &aw.writer; + try aw_writer.writeAll("Object words: ["); + var first = true; + for (object_words) |word| { + try aw_writer.print("{s}\"{s}\"", .{ if (!first) ", " else "", word }); + first = false; + } + std.log.debug("{s}]", .{aw.written()}); - // Bracketed words are "null" - // words with [?] are "unknown" - // If we have unknowns, I think we want to replace (or if no replacement - // is available, strip) them. Then re-parse immediately, because we're - // in a bad enough state that we might screw something else up - // - // If there are nulls, then we should walk those nulls and look for - // replacement values. If any replacements have been performed, then - // try re-parsing at that point. - // - // This might all be best done in the library itself. Pass in the - // map of replacement words and let it churn. - // - // For null words, I think we can use this replacement loop - // if (tree.hasUnknowns()) // then what? - // {} + aw.clearRetainingCapacity(); + try aw_writer.writeAll("Action words: ["); + first = true; + for (action_words) |word| { + try aw_writer.print("{s}\"{s}\"", .{ if (!first) ", " else "", word }); + first = false; + } + std.log.debug("{s}]", .{aw.written()}); - const action_words = tree.sentenceAction() catch |err| { - if (!builtin.is_test) - std.log.err("Failed to extract action: {}\nParse tree: {f}", .{ err, tree }); - continue; - }; - defer allocator.free(action_words); - if (action_words.len == 0) { - std.log.info("Failed to extract action from sentence", .{}); - continue; - } - - const object_words = tree.sentenceObject() catch |err| { - std.log.err("Failed to extract object: {}\nParse tree: {f}", .{ err, tree }); - continue; - }; - defer allocator.free(object_words); - if (object_words.len == 0) { - std.log.info("Failed to extract object from sentence", .{}); - continue; - } - - std.log.debug("{f}", .{tree}); - var aw = std.Io.Writer.Allocating.init(allocator); - defer aw.deinit(); - const aw_writer = &aw.writer; - try aw_writer.writeAll("Object words: ["); - var first = true; - for (object_words) |word| { - try aw_writer.print("{s}\"{s}\"", .{ if (!first) ", " else "", word }); - first = false; - } - std.log.debug("{s}]", .{aw.written()}); - - aw.clearRetainingCapacity(); - try aw_writer.writeAll("Action words: ["); - first = true; - for (action_words) |word| { - try aw_writer.print("{s}\"{s}\"", .{ if (!first) ", " else "", word }); - first = false; - } - std.log.debug("{s}]", .{aw.written()}); - - if (parseAction(action_words)) |action| { - if (try extractDevice(allocator, object_words, devices)) |entry| { - if (builtin.is_test) - testSendCommand(entry, action) - else - try sendWemoCommand(allocator, entry, action); - success = true; - break; // Success, exit - } + if (parseAction(action_words)) |action| { + if (try extractDevice(allocator, object_words, devices)) |entry| { + if (builtin.is_test) + testSendCommand(entry, action) + else + try sendWemoCommand(allocator, entry, action); + return; // Success } } - if (!success) return error.UnrecognizedSentence; + + return error.UnrecognizedSentence; } var test_device_entry: std.hash_map.StringHashMap([]const u8).Entry = undefined; diff --git a/src/root.zig b/src/root.zig index 960b5f7..b5ba9e0 100644 --- a/src/root.zig +++ b/src/root.zig @@ -1,3 +1,4 @@ +const builtin = @import("builtin"); const std = @import("std"); const c = @cImport({ @cInclude("link-includes.h"); @@ -275,6 +276,99 @@ pub const Parser = struct { _ = c.dictionary_delete(self.dict); } + /// Parses a sentence with an attempt to "fix" the sentence. If a valid + /// sentence is found, it will be returned, with the guarantee that + /// sentenceObject and sentenceAction will return non-zero results. If that + /// condition cannot be satisfied, error.NoValidParse will be returned + pub fn adaptiveParse(self: *Parser, sentence: [:0]const u8, replacements: std.StaticStringMap([]const u8)) !ParseTree { + var sentence_to_try: [:0]const u8 = sentence; + var replaced_sentence: ?[:0]u8 = null; + defer if (replaced_sentence) |s| self.allocator.free(s); + + const replacement_keys = replacements.keys(); + const replacement_values = replacements.values(); + + for (0..replacement_keys.len + 1) |attempt| { + if (attempt > 0) { + // Create sentence with replacements + if (replaced_sentence) |s| self.allocator.free(s); + const temp = try std.mem.replaceOwned( + u8, + self.allocator, + sentence_to_try, + replacement_keys[attempt - 1], + replacement_values[attempt - 1], + ); + replaced_sentence = try self.allocator.dupeZ(u8, temp); + self.allocator.free(temp); + if (std.mem.eql(u8, sentence_to_try, replaced_sentence.?)) continue; + sentence_to_try = replaced_sentence.?; + std.log.info("Replaced word '{s}' in sentence with replacement '{s}'. Trying sentence: {s}", .{ + replacement_keys[attempt - 1], + replacement_values[attempt - 1], + sentence_to_try, + }); + } + + var tree = self.parse(sentence_to_try) catch |err| { + std.log.err("Failed to parse sentence: {}\n", .{err}); + continue; + }; + + // Bracketed words are "null" + // words with [?] are "unknown" + // If we have unknowns, I think we want to replace (or if no replacement + // is available, strip) them. Then re-parse immediately, because we're + // in a bad enough state that we might screw something else up + // + // If there are nulls, then we should walk those nulls and look for + // replacement values. If any replacements have been performed, then + // try re-parsing at that point. + // + // This might all be best done in the library itself. Pass in the + // map of replacement words and let it churn. + // + // For null words, I think we can use this replacement loop + // if (tree.hasUnknowns()) // then what? + // {} + + // Validate that we can extract action and object before returning + const action_words = tree.sentenceAction() catch |err| { + if (!builtin.is_test) + std.log.err("Failed to extract action: {}\n", .{err}); + tree.deinit(); + continue; + }; + defer self.allocator.free(action_words); + + if (action_words.len == 0) { + if (!builtin.is_test) + std.log.info("Failed to extract action from sentence", .{}); + tree.deinit(); + continue; + } + + const object_words = tree.sentenceObject() catch |err| { + if (!builtin.is_test) + std.log.err("Failed to extract object: {}\n", .{err}); + tree.deinit(); + continue; + }; + defer self.allocator.free(object_words); + + if (object_words.len == 0) { + if (!builtin.is_test) + std.log.info("Failed to extract object from sentence", .{}); + tree.deinit(); + continue; + } + + return tree; + } + + return error.NoValidParse; + } + pub fn parse(self: *Parser, input: []const u8) !ParseTree { const c_input = try self.allocator.dupeZ(u8, input); defer self.allocator.free(c_input);