001package gudusoft.gsqlparser.pp2.region;
002
003import gudusoft.gsqlparser.TCustomSqlStatement;
004import gudusoft.gsqlparser.TGSqlParser;
005import gudusoft.gsqlparser.TStatementList;
006import gudusoft.gsqlparser.TSyntaxError;
007
008import java.util.ArrayList;
009import java.util.Collections;
010import java.util.List;
011
012/**
013 * Immutable per-region outcome produced by {@link ParseRecoveryEngine}.
014 *
015 * <p>The outcome carries one of three statuses:
016 *
017 * <ul>
018 *   <li>{@link Status#AST_OK} — the region's source slice parsed cleanly.
019 *       {@link #getStatement()} returns the AST root captured from the
020 *       parser. The {@link #getSyntaxErrors()} list is empty.</li>
021 *   <li>{@link Status#AST_ERROR} — parse failed (or was skipped by the
022 *       {@code maxRegionParseChars} safety valve, or the parser threw).
023 *       {@link #getStatement()} is {@code null}. {@link #getSyntaxErrors()}
024 *       carries the parser's diagnostics (possibly empty if the engine
025 *       could not capture them — e.g., after a {@code Throwable}). The
026 *       optional {@link #getEngineNote()} field carries a short message
027 *       describing why this region was marked {@code AST_ERROR} when the
028 *       parser itself did not report a diagnostic (e.g., region too large,
029 *       parser threw).</li>
030 *   <li>{@link Status#TRIVIA} — the region contains no solid tokens (only
031 *       whitespace and / or comments). No parse was attempted.
032 *       {@link #getStatement()} is {@code null}, {@link #getSyntaxErrors()}
033 *       is empty.</li>
034 * </ul>
035 *
036 * <p><b>AST lifetime contract.</b> Every {@code AST_OK} outcome carries a
037 * reference to the {@link TGSqlParser} that produced it. That parser owns the
038 * captured AST (statement tree, source-token list, error list) and is the
039 * input S13 ({@code GuardedAstDelegate}) and S16 ({@code Pp2Engine}) hand to
040 * {@code FormatterFactory.pp(parser, opt)} via {@link #getParser()}.
041 * Two scenarios:
042 *
043 * <ul>
044 *   <li><b>{@link ParseRecoveryEngine#parseRegion(StatementRange) parseRegion}:</b>
045 *       the parser is the pool's single instance. Its state is overwritten
046 *       on the <i>next</i> {@code parseRegion} / {@code parseAll} call.
047 *       Consume the AST <i>before</i> requesting the next region.</li>
048 *   <li><b>{@link ParseRecoveryEngine#parseAll(List) parseAll}:</b> every
049 *       {@code AST_OK} outcome carries a freshly-allocated parser
050 *       dedicated to that region (the engine probes each range via the
051 *       pool, and on success re-parses the slice into a new
052 *       {@code TGSqlParser} so {@code parser.sqlstatements} holds exactly
053 *       one statement). Outcomes are mutually valid: a caller may iterate
054 *       the returned list in any order and consume each AST without
055 *       invalidating the rest. The contract is broken by the next
056 *       {@code parseAll} / {@code parseRegion} call on the same engine
057 *       only for outcomes produced via {@code parseRegion} (which share
058 *       the pool); {@code parseAll}'s dedicated parsers outlive the engine
059 *       call.</li>
060 * </ul>
061 *
062 * <p>Plan reference: §7.3/S12, §7.4/S12.
063 */
064public final class RegionParseOutcome {
065
066    public enum Status {
067        /** Parse succeeded; {@link #getStatement()} is non-null. */
068        AST_OK,
069        /** Parse failed, was skipped, or the parser threw. */
070        AST_ERROR,
071        /** Region contains no solid tokens. No parse was attempted. */
072        TRIVIA
073    }
074
075    private final StatementRange range;
076    private final Status status;
077    private final String parsedSql;
078    private final TCustomSqlStatement statement;     // nullable
079    private final TGSqlParser parser;                // nullable; non-null iff AST_OK
080    private final List<TSyntaxError> syntaxErrors;
081    private final String engineNote;                 // nullable
082
083    private RegionParseOutcome(StatementRange range,
084                               Status status,
085                               String parsedSql,
086                               TCustomSqlStatement statement,
087                               TGSqlParser parser,
088                               List<TSyntaxError> syntaxErrors,
089                               String engineNote) {
090        if (range == null) throw new NullPointerException("range");
091        if (status == null) throw new NullPointerException("status");
092        if (parsedSql == null) throw new NullPointerException("parsedSql");
093        this.range = range;
094        this.status = status;
095        this.parsedSql = parsedSql;
096        this.statement = statement;
097        this.parser = parser;
098        // Defensive copy: callers (including ParseRecoveryEngine) often
099        // pass the parser's live list; without a copy, a subsequent parse()
100        // that mutates that list would silently change this outcome's
101        // visible diagnostics.
102        this.syntaxErrors = (syntaxErrors == null || syntaxErrors.isEmpty())
103            ? Collections.<TSyntaxError>emptyList()
104            : Collections.unmodifiableList(new ArrayList<TSyntaxError>(syntaxErrors));
105        this.engineNote = engineNote;
106    }
107
108    /**
109     * Build a successful outcome. The first statement of {@code stmts} is
110     * captured as the AST root for this region; the caller's per-region parse
111     * is expected to yield exactly one top-level statement, but the engine
112     * tolerates parsers that wrap output in additional containers by reading
113     * {@code stmts.get(0)} and ignoring extras. {@code stmts} may be
114     * {@code null} or empty, in which case the outcome is treated as
115     * {@code AST_ERROR} (parser claimed success but produced no statement).
116     *
117     * <p>{@code parser} is the {@link TGSqlParser} instance that produced the
118     * statement. It is exposed via {@link #getParser()} for S13's
119     * {@code GuardedAstDelegate} which feeds it to
120     * {@code FormatterFactory.pp(parser, opt)}.
121     */
122    static RegionParseOutcome astOk(StatementRange range, String parsedSql,
123                                    TStatementList stmts, TGSqlParser parser) {
124        int stmtCount = (stmts == null) ? 0 : stmts.size();
125        if (stmtCount == 0) {
126            return new RegionParseOutcome(range, Status.AST_ERROR, parsedSql,
127                null, null, null,
128                "parser reported success but produced no statement");
129        }
130        if (stmtCount > 1) {
131            // Defensive: S13 hands the outcome's parser to
132            // FormatterFactory.pp(parser, opt) which iterates EVERY
133            // statement in parser.sqlstatements. If a single region's
134            // source slice somehow parses into multiple top-level
135            // statements (e.g. the boundary detector's known DECLARE-clause
136            // limitation per the S11 resume), routing this through the AST
137            // path would render siblings the caller did not ask for. Force
138            // the lexical fallback instead.
139            return new RegionParseOutcome(range, Status.AST_ERROR, parsedSql,
140                null, null, null,
141                "region parsed into " + stmtCount + " statements; AST path "
142                    + "expects exactly one — routing to lexical fallback");
143        }
144        if (parser == null) throw new NullPointerException("parser");
145        return new RegionParseOutcome(range, Status.AST_OK, parsedSql,
146            stmts.get(0), parser, null, null);
147    }
148
149    /**
150     * Build an {@code AST_ERROR} outcome. Used by the engine for parse
151     * failures and safety-valve skips; also useful to tests and downstream
152     * stages that need to package a synthetic error outcome (e.g., a
153     * region the boundary detector emitted but parsing was bypassed).
154     *
155     * <p>{@code errors} may be {@code null} or empty.
156     */
157    public static RegionParseOutcome astError(StatementRange range,
158                                              String parsedSql,
159                                              List<TSyntaxError> errors,
160                                              String note) {
161        return new RegionParseOutcome(range, Status.AST_ERROR, parsedSql,
162            null, null, errors, note);
163    }
164
165    /**
166     * Build a {@code TRIVIA} outcome (region with no solid tokens — only
167     * whitespace and/or comments).
168     */
169    public static RegionParseOutcome trivia(StatementRange range,
170                                            String parsedSql) {
171        return new RegionParseOutcome(range, Status.TRIVIA, parsedSql,
172            null, null, null, null);
173    }
174
175    public StatementRange getRange() { return range; }
176    public Status getStatus() { return status; }
177
178    /** The source text that was (or would have been) handed to the parser. */
179    public String getParsedSql() { return parsedSql; }
180
181    /**
182     * The captured AST root for {@code AST_OK} outcomes; {@code null}
183     * otherwise. See the class Javadoc for the AST lifetime contract.
184     */
185    public TCustomSqlStatement getStatement() { return statement; }
186
187    /**
188     * The {@link TGSqlParser} that produced the AST for this outcome.
189     * Non-null only when {@link #getStatus()} is {@link Status#AST_OK};
190     * {@code null} otherwise. See the class Javadoc for the lifetime
191     * contract; in particular, after the next
192     * {@link ParseRecoveryEngine#parseRegion(StatementRange)} or
193     * {@link ParseRecoveryEngine#parseAll(List)} call the parser handed back
194     * here may have been overwritten in place (for outcomes produced via
195     * the pool) and must not be reused.
196     */
197    public TGSqlParser getParser() { return parser; }
198
199    /** Immutable list of syntax errors. Empty for {@code AST_OK} and {@code TRIVIA}. */
200    public List<TSyntaxError> getSyntaxErrors() { return syntaxErrors; }
201
202    /**
203     * Short engine-level note explaining the outcome when the parser itself
204     * did not report a diagnostic. Examples: "region exceeds
205     * maxRegionParseChars", "parser threw: NullPointerException".
206     * {@code null} when not applicable.
207     */
208    public String getEngineNote() { return engineNote; }
209
210    @Override
211    public String toString() {
212        return "RegionParseOutcome{" + status + " " + range
213            + (statement != null ? " ast=" + statement.getClass().getSimpleName() : "")
214            + " errs=" + syntaxErrors.size()
215            + (engineNote != null ? " note='" + engineNote + "'" : "")
216            + "}";
217    }
218}