Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ dotnet run --project Jiten.Cli -- --search-lookup "そうする" # Lookups ta
**Key files for parser fixes:**

- `Jiten.Parser/MorphologicalAnalyser.cs`: `SpecialCases2`/`SpecialCases3` (hardcoded token combinations), `MisparsesRemove` (tokens to filter), `Combine*` methods (merging logic), `PreprocessText()` (forced splits), `RepairNTokenisation()` (ん-form fixes)
- `Jiten.Parser/Stages/MorphologicalAnalyser.RewriteRules.cs`: declarative token-rewrite table + engine. **A new single-surface lexical fix (pin a WordId, or split/merge a specific surface into fixed tokens) should be a `RewriteRule` row here, not a hand-rolled `if`-block.** The engine owns offsets, readings-from-templates, cloning, and pin/conjugation recovery. Phases: `Cleanup` (runs before `FilterMisparse`), `Late` (mora-theft position), `Early` (ProcessSpecialCases position). Add a new pipeline **stage** only for a pattern-general mechanism, never a single lexical pattern. Context-heavy logic (window scans, dynamic readings, POS-section edits, order-dependent-on-another-block) legitimately stays as code.
- `Jiten.Parser/Stages/MorphologicalAnalyser.RewriteRules.cs`: declarative token-rewrite table + engine. **A new single-surface lexical fix (pin a WordId, or split/merge a specific surface into fixed tokens) should be a `RewriteRule` row here, not a hand-rolled `if`-block.** The engine owns offsets, readings-from-templates, cloning, and pin/conjugation recovery. Phases: `Cleanup` (runs before `FilterMisparse`), `Late` (mora-theft position), `Early` (ProcessSpecialCases position). A rule pin is soft by default (lookup-time compound matching may absorb the token into a longer attested span); set `HardPin` when the pin is a final word decision that compound matching must not swallow. Add a new pipeline **stage** only for a pattern-general mechanism, never a single lexical pattern. Context-heavy logic (wider/dynamic window scans, dynamic readings, POS-section edits, multi-token mutation, order-dependent-on-another-block) legitimately stays as code.
- `Shared/resources/deconjugator.json`: Rule-based deconjugation (~1500 rules). Types: `stdrule`, `rewriterule`, `onlyfinalrule`, `neverfinalrule`, `contextrule`. Fields: `dec_end`/`con_end` (endings), `dec_tag`/`con_tag` (grammar tags). Search for specific endings or `"detail": "past"` etc.
- `Shared/resources/user_dic.xml`: Custom Sudachi dictionary entries. Format: `surface,leftId,rightId,cost,display,pos1,pos2,pos3,pos4,conjType,conjForm,reading,normalised,dictFormId,splitType,splitA,splitB,unused`. Regenerate after editing: `sudachi ubuild "Y:\CODE\Jiten\Shared\resources\user_dic.xml" -s "S:\Jiten\sudachi.rs\resources\system_full.dic" -o "Y:\CODE\Jiten\Shared\resources\user_dic.dic"`

Expand Down
4 changes: 4 additions & 0 deletions Jiten.Core/Data/PosMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,10 @@ public static bool IsJmDictCompatibleWithSudachi(

// Sudachi 接尾辞 (Suffix) should match JMDict n-suf (NounSuffix) and suf (Suffix).
// E.g. だらけ is n-suf in JMDict but 接尾辞 in Sudachi; 達 (たち) is suf in JMDict.
// Counter (ctr) entries are deliberately NOT compatible here: counter eligibility is
// contextual (needs a preceding numeral), which this context-free check cannot see —
// a suffix-tagged kanji inside a plain compound (結晶|石) must not prefer a counter
// homograph. Numeral contexts select counters in adjacent rescoring instead.
if (sudachiPos == PartOfSpeech.Suffix &&
(convertedPosList.Contains(PartOfSpeech.NounSuffix) || convertedPosList.Contains(PartOfSpeech.Suffix)))
return true;
Expand Down
15 changes: 15 additions & 0 deletions Jiten.Core/JapaneseTextHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ public static bool IsAllKatakana(ReadOnlySpan<char> s)
return true;
}

/// <summary>Katakana letter (ァ–ヺ) or the long-vowel mark ー — the characters a katakana
/// word is spelled with, excluding middle dots and iteration marks.</summary>
public static bool IsKatakanaWordChar(char c) =>
c is (>= 'ァ' and <= 'ヺ') or 'ー';

/// <summary>Fullwidth (A-Z/a-z) or halfwidth (A-Z/a-z) Latin letter.</summary>
public static bool IsLatinLetter(char c) =>
c is (>= '\uFF21' and <= '\uFF3A') or (>= '\uFF41' and <= '\uFF5A') // fullwidth A-Z / a-z
Expand Down Expand Up @@ -86,4 +91,14 @@ public static bool IsKanji(Rune r)
(>= 0xF900 and <= 0xFAFF) or // Compatibility Ideographs
(>= 0x2F800 and <= 0x2FA1F); // Compatibility Supplement
}

/// <summary>
/// A numeral character: an ASCII or full-width digit, a kanji digit (一〜九), a kanji place
/// marker (十百千万億兆), or a kanji zero (〇零). Covers the counting/quantity vocabulary shared by
/// the numeral-context guards; contexts that need a narrower kanji-only set test IsKanji separately.
/// </summary>
public static bool IsNumeralChar(char c) =>
c is (>= '0' and <= '9') or (>= '0' and <= '9')
or '一' or '二' or '三' or '四' or '五' or '六' or '七' or '八' or '九'
or '十' or '百' or '千' or '万' or '億' or '兆' or '〇' or '零';
}
20 changes: 20 additions & 0 deletions Jiten.Parser/Data/WordInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,23 @@ public class WordInfo
/// ProcessWord escalation chain performs.
public bool IsKanaExclamation { get; set; }

/// Set when Sudachi originally tagged this all-katakana token as a noun — a name/loanword
/// shape. Deconjugation of its hiragana conversion must not land on kanji-primary words
/// (ハガナ → はが+な → 剥ぐ fabricates vocabulary out of a name). Survives the POS rewrites
/// that the ProcessWord escalation chain performs.
public bool IsKatakanaNounSurface { get; set; }

/// Set when a rewrite-rule template pinned this token — a deliberate lexical decision the
/// misparse gates must not overrule (え|っつった). PreMatchedWordId alone can't carry this:
/// parser-level machinery (compound matches, fallbacks) reuses it for ordinary tokens.
public bool PinnedByRewriteRule { get; set; }

/// Set when a gate's pin is a final word decision that compound formation must not absorb
/// or override (the ぶん of a fraction frame, the ordinal 目). Soft pins — reading defaults
/// like the する-family — stay absorbable: an attested expression spanning them (そうした,
/// 臆病風に吹かれる) is the better parse.
public bool HardPinned { get; set; }

/// Sudachi lattice segmentation margin: extra cost of the cheapest competing lattice path
/// crossing one of this token's boundaries (clamped to 99999 = no competitor).
/// Null when margin output was not requested. Low values = uncertain segmentation.
Expand Down Expand Up @@ -70,6 +87,9 @@ public WordInfo(WordInfo other)
ResolvedWordId = other.ResolvedWordId;
SudachiBoundaryMargin = other.SudachiBoundaryMargin;
IsKanaExclamation = other.IsKanaExclamation;
IsKatakanaNounSurface = other.IsKatakanaNounSurface;
PinnedByRewriteRule = other.PinnedByRewriteRule;
HardPinned = other.HardPinned;
}

public WordInfo(string sudachiLine)
Expand Down
26 changes: 21 additions & 5 deletions Jiten.Parser/Grammar/TransitionRuleEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,15 +156,20 @@ or PartOfSpeech.PrenounAdjectival or PartOfSpeech.Counter
MatchCondition.NextIsNotQuotative =>
w.Next == null || !w.Next.Text.StartsWith("という"),

// Valid hosts for a sentence-final particle: auxiliaries/particles (だな, はね) plus —
// for な only — plain-form verbs/i-adjectives (prohibitive えぐるな, exclamatory
// 欲しいな), which stay valid mid-sentence because run-on speech has no punctuation.
// Other SFPs keep the merge: ない+か must still form ないか.
// Valid hosts for a sentence-final particle: auxiliaries/particles (だな, はね), plain
// predicates before な (prohibitive えぐるな, exclamatory 欲しいな), and plain
// predicates before よ/ね only when the next token looks vocative
// (やべえよ母ちゃん). The vocative gate keeps ordinary run-on text such as
// 食べるよ明日 subject to the existing merge/remove repair.
MatchCondition.PrevIsSfpValidHost =>
w.Prev?.PartOfSpeech is PartOfSpeech.Auxiliary or PartOfSpeech.Particle
|| (w.Current.Text == "な"
&& w.Prev is { PartOfSpeech: PartOfSpeech.Verb or PartOfSpeech.IAdjective }
&& w.Prev.Text == w.Prev.DictionaryForm),
&& w.Prev.Text == w.Prev.DictionaryForm)
|| (w.Current.Text is "よ" or "ね"
&& w.Prev is { PartOfSpeech: PartOfSpeech.Verb or PartOfSpeech.IAdjective }
&& w.Prev.Text == w.Prev.DictionaryForm
&& IsVocativeFollower(w.Next)),

_ => false
};
Expand All @@ -173,6 +178,17 @@ or PartOfSpeech.PrenounAdjectival or PartOfSpeech.Counter
return true;
}

private static bool IsVocativeFollower(WordInfo? next)
{
if (next?.PartOfSpeech is PartOfSpeech.Name or PartOfSpeech.Pronoun)
return true;
if (next?.PartOfSpeech is not (PartOfSpeech.Noun or PartOfSpeech.CommonNoun))
return false;

return TransitionRuleSets.HonorificSuffixes.Any(
suffix => next.Text.EndsWith(suffix, StringComparison.Ordinal));
}

private static void ApplyViolation(
TransitionRule rule,
List<(WordInfo word, int pos, int len)> words,
Expand Down
4 changes: 3 additions & 1 deletion Jiten.Parser/Grammar/TransitionRuleSets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,12 @@ internal static class TransitionRuleSets
[ScoringCondition.NextIsExplanatoryN],
25),

// High enough that a numeral+counter pair outbids a top-priority noun homograph together
// with its ruby prior (第二話: the counter わ must beat はなし).
new("numeral-counter-cohesion",
[ScoringCondition.CandidateIsCounter],
[ScoringCondition.PrevIsNumeral],
40),
60),

new("orphan-counter-penalty",
[ScoringCondition.CandidateIsCounter, ScoringCondition.CandidateIsNotNounLike],
Expand Down
5 changes: 3 additions & 2 deletions Jiten.Parser/Helpers/MorphologicalAnalyser.Diagnostics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@ private static List<SudachiToken> ParseSudachiOutputToDiagnosticTokens(string ra
return tokens;
}

private static List<WordInfo> TrackStage(TokenStage stage, List<WordInfo> input, ParserDiagnostics? diagnostics)
private static List<WordInfo> TrackStage(TokenStage stage, List<WordInfo> input, ParserDiagnostics? diagnostics,
TokenFeatureScan? scan = null)
{
var inputSnapshot = diagnostics != null ? input.Select(TokenSnapshot.From).ToList() : null;
var sw = diagnostics != null ? Stopwatch.StartNew() : null;

var result = stage.Apply(input);
var result = stage.Apply(input, scan);

if (diagnostics == null)
return result;
Expand Down
1 change: 1 addition & 0 deletions Jiten.Parser/Helpers/MorphologicalAnalyser.RuleData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ public partial class MorphologicalAnalyser
("すぐ", "に", PartOfSpeech.Adverb),
("たしか", "に", PartOfSpeech.Adverb),
("確か", "に", PartOfSpeech.Adverb), // 確かに 1205770 — kanji variant of the たしか row above
("直", "に", PartOfSpeech.Adverb), // 直に 1430690 (じかに, "directly/in person") — the bare 直 otherwise reads ちょく
("ゆえ", "に", PartOfSpeech.Conjunction), // 故に 1267130 ("therefore") — literary connective Sudachi splits
("故", "に", PartOfSpeech.Conjunction), // kanji variant of the ゆえ row above
("とう", "に", PartOfSpeech.Adverb), // 疾うに 1633370 ("long ago") — the bare とう otherwise strands on 塔
Expand Down
50 changes: 45 additions & 5 deletions Jiten.Parser/Misparse/MisparseGates.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ internal static class MisparseGates
/// digits, or a long mixed-kana surface — are excluded before the caller assembles flags,
/// neighbour frames and blob context. The character test accepts a superset of the kana the
/// gates accept (full/half-width kana blocks plus stretch marks), so it can only skip tokens
/// no gate would touch.
/// no gate would touch. The ≤4 cap excludes 6-character reduplications such as ギュウギュウ;
/// those overlap attested on-mim adverbs such as ごちゃごちゃとして and require separate
/// mimetic preference handling.
public static bool MayBeKanaFragment(string s)
{
if (s.Length == 0) return false;
Expand Down Expand Up @@ -95,7 +97,14 @@ private static bool IsSfxMimeticFragment(in MisparseGateContext ctx)
? ctx.SelectedWord.OriginalText
: ctx.Token.Text;

if (surface.Length == 0 || surface.Length > 4 || !WanaKana.IsKana(surface)) return false;
// Pure reduplication (コクコク) is mimetic orthography — it extends the local length
// window and counts as a positive marker below. Six-character reduplications
// (ギュウギュウ) only arrive when a shorter resolved token carries the longer
// OriginalText — MayBeKanaFragment's ≤4 cap excludes them as whole tokens upstream.
bool reduplicated = surface.Length is 4 or 6
&& surface[..(surface.Length / 2)] == surface[(surface.Length / 2)..];
if (surface.Length == 0 || (surface.Length > 4 && !reduplicated) || surface.Length > 6
|| !WanaKana.IsKana(surface)) return false;

// A neighbour already discarded as a misparse shred is part of the same burst; so is an
// unresolved kana scrap. Both make the current token frame-transparent on that side.
Expand All @@ -117,17 +126,24 @@ private static bool IsSfxMimeticFragment(in MisparseGateContext ctx)
// unsegmentable blob. A case particle or content word on either side means real syntax,
// which the gate must not touch. When a sokuon is clipped straight onto the token (ず|がんっ),
// a short kana neighbour is part of the same burst even if it happened to resolve.
// A reduplicated surface heading the mimetic-adverb frame 〜と+verb (コクコクと振る) is
// phonetic wherever it sits — the construction itself is the isolation.
bool reduplicatedToFrame = reduplicated
&& ctx.Next is { Text: "と", PartOfSpeech: PartOfSpeech.Particle };

bool frameBefore = ctx.Prev == null || ctx.SymbolsBefore.Length > 0 || ctx.IsSentenceInitial
|| ctx.Prev.PartOfSpeech == PartOfSpeech.Interjection
|| (ctx.Prev.PartOfSpeech == PartOfSpeech.Particle
&& ctx.Prev.Text is "な" or "よ" or "ね" or "ぞ" or "ぜ" or "わ" or "さ")
|| (exclamatoryClip && ctx.Prev.Text.Length <= 2 && WanaKana.IsKana(ctx.Prev.Text))
|| prevShard;
|| prevShard
|| reduplicatedToFrame;
if (!frameBefore) return false;

bool nextIsQuotativeTo = ctx.Next is { Text: "と", PartOfSpeech: PartOfSpeech.Particle }
&& ctx.SymbolsAfter.Length > 0;
bool frameAfter = ctx.Next == null || ctx.SymbolsAfter.Length > 0 || nextShard;
bool frameAfter = ctx.Next == null || ctx.SymbolsAfter.Length > 0 || nextShard
|| reduplicatedToFrame;
if (!frameAfter) return false;

// Require a positive mimetic signal; isolation alone describes any one-word answer (「梨」).
Expand All @@ -138,6 +154,7 @@ private static bool IsSfxMimeticFragment(in MisparseGateContext ctx)
|| ctx.SymbolsAfter.IndexOfAny(GapMimeticChars) >= 0
|| nextIsQuotativeTo
|| prevShard || nextShard
|| reduplicated
// An in-surface sokuon is ordinary orthography when the entry spells the
// surface — literally (おっさん) or through the expressive-deformation check
// in GetWordFlags (バカッ/ほんっと) — but phonetic evidence when it does not
Expand Down Expand Up @@ -174,9 +191,15 @@ private static bool IsSfxMimeticFragment(in MisparseGateContext ctx)
// うぜえ、と, ごくり、と, ハルさんっ): attested as written, usually-kana, or reached
// through conjugation. A non-noun invented by text mutation (うぅ→うん) stays in scope
// as noise.
// An entry with an adverbial sense used in the adverbial と frame is that sense, whatever
// POS happens to be listed first (ぽつぽつ [n, adv, adv-to] + と + 灯す) — first-POS alone
// must not pull an attested mimetic back into the plain-noun scope.
bool adverbialInToFrame = ctx.Next is { Text: "と", PartOfSpeech: PartOfSpeech.Particle }
&& ctx.SelectedWord.PartsOfSpeech.Any(p => p is PartOfSpeech.Adverb or PartOfSpeech.AdverbTo);
if ((ctx.SelectedWord.PartsOfSpeech.Count == 0
|| ctx.SelectedWord.PartsOfSpeech[0] is not (PartOfSpeech.Noun
or PartOfSpeech.CommonNoun or PartOfSpeech.Numeral or PartOfSpeech.NaAdjective))
or PartOfSpeech.CommonNoun or PartOfSpeech.Numeral or PartOfSpeech.NaAdjective)
|| adverbialInToFrame)
&& (ctx.SurfaceAttestsListedForm || ctx.IsUsuallyKana
|| (ctx.SelectedWord.Conjugations.Count > 0
&& ctx.SelectedWord.Conjugations[0] is not ("(stem)" or "(infinitive)"
Expand Down Expand Up @@ -367,6 +390,23 @@ public static (bool isUsuallyKana, bool hasKanjiSpelling, bool readingIsIchi, bo

bool surfaceAttestsLiterally = surfaceAttestsForm;

// Mimetic adverbs and interjections are written in either kana script interchangeably and
// JMdict lists most of them hiragana-only — a katakana spelling (スタスタと) is ordinary
// orthography for the same word, not a deformation. Restricted to the adverbial/
// interjection class so a katakana SFX cannot claim an unrelated hiragana word's identity
// through the script fold.
if (!surfaceAttestsForm && surface is { Length: > 0 }
&& word.PartsOfSpeech.Any(p => p is "adv" or "adv-to" or "int" or "on-mim")
&& JapaneseTextHelper.IsAllKatakana(surface))
{
var folded = WanaKana.ToHiragana(surface);
if (folded != surface && word.Forms.Any(f => f.Text == folded))
{
surfaceAttestsForm = true;
surfaceAttestsLiterally = true;
}
}

// Expressive spelling deforms a word without changing it: emphatic gemination writes a
// sokuon in (ほんっと, バカッ, マジッす), a chōonpu stretches a mora (おーっと), and a
// trailing stretch elongates the final one (そっかー, だってぇ, なんだとぉ). Such a
Expand Down
1 change: 1 addition & 0 deletions Jiten.Parser/MorphologicalAnalyser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public partial class MorphologicalAnalyser
public Func<string, int?>? GetNonNameCompoundWordId { get; set; }
public Func<string, int?>? GetNonNameCompoundFrequencyRank { get; set; }
public Func<string, bool>? HasVerbOrAdjectiveLookup { get; set; }
public Func<string, bool>? HasCounterSenseLookup { get; set; }

// Captured per Parse call for RetokeniseOovBlobs; an instance must not serve two parses with
// different Sudachi configs concurrently (all production paths construct one instance per parse).
Expand Down
Loading