From c8ccb9434ce61630a5282e6ad4074ea0ac61dc63 Mon Sep 17 00:00:00 2001 From: opaopa6969 Date: Fri, 9 Oct 2026 13:47:18 +0900 Subject: [PATCH 1/5] Connect existing P4 LSP to opt-in production embedded Java diagnostics --- .github/workflows/embedded-languages.yml | 41 ++++ .../EMBEDDED-LANGUAGES.md | 17 ++ .../tinyexpression-p4-lsp-vscode/package.json | 5 + tools/tinyexpression-p4-lsp-vscode/pom.xml | 21 +- .../scripts/check-embedded-lsp.py | 63 ++++++ .../p4/ProductionEmbeddedDiagnosticsTest.java | 58 +++++ .../embedded-test/resources/type-error.txt | 200 +++++++++++++++++ .../resources/unicode-crlf-error.txt | 201 ++++++++++++++++++ .../ProductionEmbeddedDiagnostics.java | 55 +++++ .../lsp/p4/embedded/TinyProductionBridge.java | 104 +++++++++ ...ression.lsp.p4.EmbeddedLanguageDiagnostics | 1 + .../src/extension.ts | 5 +- .../lsp/p4/EmbeddedLanguageDiagnostics.java | 9 + .../p4/TinyExpressionP4LanguageServerExt.java | 98 +++++++-- 14 files changed, 861 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/embedded-languages.yml create mode 100644 tools/tinyexpression-p4-lsp-vscode/EMBEDDED-LANGUAGES.md create mode 100644 tools/tinyexpression-p4-lsp-vscode/scripts/check-embedded-lsp.py create mode 100644 tools/tinyexpression-p4-lsp-vscode/src/embedded-test/java/org/unlaxer/tinyexpression/lsp/p4/ProductionEmbeddedDiagnosticsTest.java create mode 100644 tools/tinyexpression-p4-lsp-vscode/src/embedded-test/resources/type-error.txt create mode 100644 tools/tinyexpression-p4-lsp-vscode/src/embedded-test/resources/unicode-crlf-error.txt create mode 100644 tools/tinyexpression-p4-lsp-vscode/src/embedded/java/org/unlaxer/tinyexpression/lsp/p4/embedded/ProductionEmbeddedDiagnostics.java create mode 100644 tools/tinyexpression-p4-lsp-vscode/src/embedded/java/org/unlaxer/tinyexpression/lsp/p4/embedded/TinyProductionBridge.java create mode 100644 tools/tinyexpression-p4-lsp-vscode/src/embedded/resources/META-INF/services/org.unlaxer.tinyexpression.lsp.p4.EmbeddedLanguageDiagnostics create mode 100644 tools/tinyexpression-p4-lsp-vscode/src/main/java/org/unlaxer/tinyexpression/lsp/p4/EmbeddedLanguageDiagnostics.java diff --git a/.github/workflows/embedded-languages.yml b/.github/workflows/embedded-languages.yml new file mode 100644 index 00000000..add41899 --- /dev/null +++ b/.github/workflows/embedded-languages.yml @@ -0,0 +1,41 @@ +name: Embedded language consumer +on: + pull_request: + push: + branches: [master] +jobs: + diagnostics: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: '21.0.9' + - uses: actions/checkout@v6 + with: + repository: opaopa6969/unlaxer-parser + ref: a3d1b131f1509361e94924e64c0f2cf65da9fb5a + path: .integration/unlaxer-parser + - name: Install source-map and diagnostic APIs + working-directory: .integration/unlaxer-parser + run: mvn -B -pl unlaxer-common,unlaxer-dsl -am install -DskipTests -Dgpg.skip=true + - name: Install production Tiny with released default dependencies + run: mvn -B install -DskipTests -Dtinyexpression.skipRailroad=true -Dgpg.skip=true + - name: Released default LSP and DAP compatibility + working-directory: tools/tinyexpression-p4-lsp-vscode + run: mvn -B clean test -Dtest=TinyExpressionP4LanguageServerExtTest,TinyExpressionP4DebugAdapterExtTest + - name: Real javac through existing opt-in LSP consumer + working-directory: tools/tinyexpression-p4-lsp-vscode + run: | + mvn -B clean package -Pembedded-languages -Dgpg.skip=true -Dtest=ProductionEmbeddedDiagnosticsTest,TinyExpressionP4LanguageServerExtTest,TinyExpressionP4DebugAdapterExtTest + python3 scripts/check-embedded-lsp.py + - uses: actions/setup-node@v6 + with: + node-version: '24' + - name: Extension setting and TypeScript checks + working-directory: tools/tinyexpression-p4-lsp-vscode + run: | + npm ci --ignore-scripts + npm run compile + npm test diff --git a/tools/tinyexpression-p4-lsp-vscode/EMBEDDED-LANGUAGES.md b/tools/tinyexpression-p4-lsp-vscode/EMBEDDED-LANGUAGES.md new file mode 100644 index 00000000..447f97fe --- /dev/null +++ b/tools/tinyexpression-p4-lsp-vscode/EMBEDDED-LANGUAGES.md @@ -0,0 +1,17 @@ +# Embedded Java diagnostics in the existing P4 server + +This is an explicit migration profile. The default dependency remains unlaxer 3.1.0; default P4/ubnfc and DAP behavior are unchanged. The profile uses the new source-map/provider API from unlaxer-parser 3.3.0-SNAPSHOT (validated at a3d1b131) and JDK 21.0.9. It never evaluates a Tiny formula or Java body; the external javac adapter disables annotation processing. + +Build unlaxer-parser's `unlaxer-common` and `unlaxer-dsl` modules with `mvn -pl unlaxer-common,unlaxer-dsl -am install -DskipTests -Dgpg.skip=true`. Install this repository's root artifact using its default dependencies with `mvn install -DskipTests -Dtinyexpression.skipRailroad=true -Dgpg.skip=true`. Then, from this directory: + +```sh +mvn clean package -Pembedded-languages -Dgpg.skip=true +``` + +Point `tinyExpressionP4Lsp.server.jarPath` at the resulting server jar, use JDK 21.0.9, and enable `tinyExpressionP4Lsp.embeddedLanguageDiagnostics`. Other LSP clients can set `initializationOptions.embeddedLanguageDiagnostics: true`. Enabling it with a server that lacks the profile fails initialization explicitly. Merely including the profile does not enable analysis. + +The existing `TinyExpressionP4LanguageServerExt` now sends untouched, versioned full documents to an optional diagnostics provider. The production FormulaInfo and P4 parsers construct all section regions; javac diagnostics from each Java body are mapped to original UTF-16 positions. Updating later sections does not reuse first-section cache results. Stale updates are rejected, save revalidates, and close clears diagnostics. Invalid or incomplete region extraction and unavailable providers emit `EMBEDDED_UNAVAILABLE`, not empty success. Existing first-section Tiny completion, formatting and other methods retain their prior behavior; this profile adds Java diagnostics only. + +The bridge is a namespaced port of unlaxer-parser PR #470, commit d247f34a. Its Java/Rust contract is tested there using the same production fixture, independent code-point/UTF-16 coordinates and rejection cases. This repository's existing LSP consumer is Java; no Rust LSP consumer is claimed here. Unclosed Java region completion and foreign dependency diagnostics need a later consumer extension. Workspace ownership is rejected explicitly by this single-document adapter. + +Run `mvn test -Pembedded-languages -Dtest=ProductionEmbeddedDiagnosticsTest,TinyExpressionP4LanguageServerExtTest,TinyExpressionP4DebugAdapterExtTest` here. The integration test runs real javac on production fixture 69 (ten formulas, two Java bodies), checks both error positions, a second-section-only update, Unicode/CRLF, stale updates, save, close and unavailable extraction. Run the 39 existing LSP/DAP tests without the profile as a separate clean build to verify the released dependency path. Use an isolated Maven repository if other checkouts install Tiny artifacts with different generated AST versions. diff --git a/tools/tinyexpression-p4-lsp-vscode/package.json b/tools/tinyexpression-p4-lsp-vscode/package.json index 5991d9ca..5f53ffce 100644 --- a/tools/tinyexpression-p4-lsp-vscode/package.json +++ b/tools/tinyexpression-p4-lsp-vscode/package.json @@ -296,6 +296,11 @@ "type": "object", "title": "TinyExpression P4 LSP", "properties": { + "tinyExpressionP4Lsp.embeddedLanguageDiagnostics": { + "type": "boolean", + "default": false, + "description": "Analyze embedded Java in every FormulaInfo section without executing user code. Requires the explicitly built embedded-languages server profile and JDK 21.0.9. Changing this setting restarts the server." + }, "tinyExpressionP4Lsp.server.javaPath": { "type": "string", "default": "java", diff --git a/tools/tinyexpression-p4-lsp-vscode/pom.xml b/tools/tinyexpression-p4-lsp-vscode/pom.xml index 8be49203..65e75c19 100644 --- a/tools/tinyexpression-p4-lsp-vscode/pom.xml +++ b/tools/tinyexpression-p4-lsp-vscode/pom.xml @@ -14,6 +14,7 @@ 21 UTF-8 0.23.1 + 3.1.0 ${project.build.directory}/generated-sources/tinyexpression-p4 ${project.basedir}/server-dist ${project.build.directory}/tinyexpression-p4-lsp-${project.version}.vsix @@ -23,7 +24,7 @@ org.unlaxer unlaxer-dsl - 3.1.0 + ${unlaxer.version} org.unlaxer @@ -215,6 +216,24 @@ + + + embedded-languages + 3.3.0-SNAPSHOT + org.unlaxerunlaxer-common${unlaxer.version} + + src/embedded/resources + + org.codehaus.mojobuild-helper-maven-plugin + + embedded-sourcesgenerate-sourcesadd-source + src/embedded/java + embedded-testsgenerate-test-sourcesadd-test-source + src/embedded-test/java + + + + p4-smoke diff --git a/tools/tinyexpression-p4-lsp-vscode/scripts/check-embedded-lsp.py b/tools/tinyexpression-p4-lsp-vscode/scripts/check-embedded-lsp.py new file mode 100644 index 00000000..ecd837ba --- /dev/null +++ b/tools/tinyexpression-p4-lsp-vscode/scripts/check-embedded-lsp.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""Verify the packaged existing launcher, opt-in provider service, and actual javac diagnostics.""" +import json +import pathlib +import subprocess +import threading +import queue + +root = pathlib.Path(__file__).resolve().parents[1] +messages = queue.Queue() +with (root / 'target/embedded-stdio.log').open('wb') as errors: + process = subprocess.Popen(['java', '--enable-preview', '-jar', str(root / 'target/tinyexpression-p4-lsp-server.jar')], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=errors) + def read(): + try: + while True: + headers = {} + while True: + line = process.stdout.readline() + if not line: + raise EOFError('server exited') + if line == b'\r\n': + break + key, value = line.decode('ascii').split(':', 1) + headers[key.lower()] = value.strip() + messages.put(json.loads(process.stdout.read(int(headers['content-length'])))) + except BaseException as error: + messages.put(error) + threading.Thread(target=read, daemon=True).start() + def send(method, params, request_id=None): + message = {'jsonrpc': '2.0', 'method': method, 'params': params} + if request_id is not None: + message['id'] = request_id + body = json.dumps(message, ensure_ascii=False).encode('utf-8') + process.stdin.write(f'Content-Length: {len(body)}\r\n\r\n'.encode('ascii') + body) + process.stdin.flush() + def receive(predicate): + while True: + message = messages.get(timeout=40) + if isinstance(message, BaseException): + raise message + if predicate(message): + return message + try: + send('initialize', {'capabilities': {}, 'initializationOptions': {'embeddedLanguageDiagnostics': True}}, 1) + initialize = receive(lambda value: value.get('id') == 1) + assert initialize['result']['capabilities']['experimental']['embeddedLanguageDiagnostics']['enabled'] + send('initialized', {}) + uri = 'file:///production.formulainfo' + send('textDocument/didOpen', {'textDocument': {'uri': uri, 'languageId': 'tinyexpressionP4', 'version': 1, 'text': (root / 'src/embedded-test/resources/type-error.txt').read_text()}}) + published = receive(lambda value: value.get('method') == 'textDocument/publishDiagnostics')['params'] + assert published['version'] == 1, published + diagnostics = [item for item in published['diagnostics'] if item.get('source') == 'tinyexpression-java'] + assert [(item['range']['start']['line'], item['range']['start']['character']) for item in diagnostics] == [(41, 9), (69, 9)], published + assert all(item['code'] == 'compiler.err.prob.found.req' and item['severity'] == 1 for item in diagnostics) + send('textDocument/didClose', {'textDocument': {'uri': uri}}) + assert receive(lambda value: value.get('method') == 'textDocument/publishDiagnostics')['params']['diagnostics'] == [] + send('shutdown', {}, 2) + receive(lambda value: value.get('id') == 2) + send('exit', {}) + print('PASS: packaged Tiny P4 launcher -> real javac -> two original FormulaInfo diagnostic positions -> close clear') + finally: + process.kill() + process.wait(timeout=5) diff --git a/tools/tinyexpression-p4-lsp-vscode/src/embedded-test/java/org/unlaxer/tinyexpression/lsp/p4/ProductionEmbeddedDiagnosticsTest.java b/tools/tinyexpression-p4-lsp-vscode/src/embedded-test/java/org/unlaxer/tinyexpression/lsp/p4/ProductionEmbeddedDiagnosticsTest.java new file mode 100644 index 00000000..6f5ebb24 --- /dev/null +++ b/tools/tinyexpression-p4-lsp-vscode/src/embedded-test/java/org/unlaxer/tinyexpression/lsp/p4/ProductionEmbeddedDiagnosticsTest.java @@ -0,0 +1,58 @@ +package org.unlaxer.tinyexpression.lsp.p4; + +import static org.junit.Assert.*; +import java.nio.file.*; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import org.junit.Test; +import org.eclipse.lsp4j.*; +import org.eclipse.lsp4j.services.LanguageClient; + +public class ProductionEmbeddedDiagnosticsTest { + @Test public void existingConsumerPublishesBothProductionJavaSectionsAndTracksSnapshots() throws Exception { + var server = new TinyExpressionP4LanguageServerExt(); + var notifications = new ArrayList(); + server.connect(new LanguageClient() { + public void publishDiagnostics(PublishDiagnosticsParams params) { notifications.add(params); } + public void telemetryEvent(Object value) {} + public void showMessage(MessageParams value) {} + public CompletableFuture showMessageRequest(ShowMessageRequestParams value) { return CompletableFuture.completedFuture(null); } + public void logMessage(MessageParams value) {} + }); + var defaults = new com.google.gson.Gson().toJsonTree(server.initialize(new InitializeParams()).join().getCapabilities().getExperimental()).getAsJsonObject(); + assertFalse(defaults.getAsJsonObject("embeddedLanguageDiagnostics").get("enabled").getAsBoolean()); + var initialize = new InitializeParams();initialize.setInitializationOptions(Map.of("embeddedLanguageDiagnostics", true)); + var caps = server.initialize(initialize).join().getCapabilities(); + assertNotNull(caps.getExperimental()); + var service = server.getTextDocumentService();String uri="file:///production.formulainfo"; + Path fixtures=Path.of("src/embedded-test/resources"); + String error=Files.readString(fixtures.resolve("type-error.txt")); + service.didOpen(new DidOpenTextDocumentParams(new TextDocumentItem(uri,"tinyexpression",1,error))); + var published=last(notifications);assertEquals(Integer.valueOf(1),published.getVersion()); + var java=javaDiagnostics(published);assertEquals(published.toString(),2,java.size()); + assertEquals(new Range(new Position(41,9),new Position(41,10)),java.get(0).getRange()); + assertEquals(new Range(new Position(69,9),new Position(69,10)),java.get(1).getRange()); + for(var diagnostic:java) { assertEquals("compiler.err.prob.found.req",diagnostic.getCode().getLeft());assertEquals(DiagnosticSeverity.Error,diagnostic.getSeverity()); } + int count=notifications.size(); + service.didChange(new DidChangeTextDocumentParams(new VersionedTextDocumentIdentifier(uri,1),List.of(new TextDocumentContentChangeEvent("stale")))); + assertEquals(count,notifications.size()); + // The first formula remains byte-for-byte identical; the changed second section must be reanalyzed. + String secondFixed = error.substring(0,error.lastIndexOf("return 1;")) + error.substring(error.lastIndexOf("return 1;")).replace("return 1;", "return true;"); + service.didChange(new DidChangeTextDocumentParams(new VersionedTextDocumentIdentifier(uri,2),List.of(new TextDocumentContentChangeEvent(secondFixed)))); + assertEquals(1,javaDiagnostics(last(notifications)).size()); + assertEquals(41,javaDiagnostics(last(notifications)).get(0).getRange().getStart().getLine()); + String unicode=Files.readString(fixtures.resolve("unicode-crlf-error.txt")); + service.didChange(new DidChangeTextDocumentParams(new VersionedTextDocumentIdentifier(uri,3),List.of(new TextDocumentContentChangeEvent(unicode)))); + published=last(notifications);assertEquals(Integer.valueOf(3),published.getVersion());java=javaDiagnostics(published); + assertEquals(published.toString(),1,java.size());assertEquals(new Range(new Position(42,9),new Position(42,10)),java.get(0).getRange()); + service.didSave(new DidSaveTextDocumentParams(new TextDocumentIdentifier(uri)));assertEquals(Integer.valueOf(3),last(notifications).getVersion()); + String valid=Files.readString(Path.of("../../src/test/resources/formulaInfo-test/69/formulaInfo.txt")); + service.didChange(new DidChangeTextDocumentParams(new VersionedTextDocumentIdentifier(uri,4),List.of(new TextDocumentContentChangeEvent(valid)))); + assertTrue(last(notifications).toString(),javaDiagnostics(last(notifications)).isEmpty()); + service.didChange(new DidChangeTextDocumentParams(new VersionedTextDocumentIdentifier(uri,5),List.of(new TextDocumentContentChangeEvent("formula:\ninvalid\n")))); + assertTrue(last(notifications).getDiagnostics().stream().anyMatch(d->d.getCode()!=null && "EMBEDDED_UNAVAILABLE".equals(d.getCode().getLeft()))); + service.didClose(new DidCloseTextDocumentParams(new TextDocumentIdentifier(uri)));assertTrue(last(notifications).getDiagnostics().isEmpty()); + } + private static PublishDiagnosticsParams last(List values) { return values.get(values.size()-1); } + private static List javaDiagnostics(PublishDiagnosticsParams value) { return value.getDiagnostics().stream().filter(d->"tinyexpression-java".equals(d.getSource())).toList(); } +} diff --git a/tools/tinyexpression-p4-lsp-vscode/src/embedded-test/resources/type-error.txt b/tools/tinyexpression-p4-lsp-vscode/src/embedded-test/resources/type-error.txt new file mode 100644 index 00000000..f1e33938 --- /dev/null +++ b/tools/tinyexpression-p4-lsp-vscode/src/embedded-test/resources/type-error.txt @@ -0,0 +1,200 @@ +tags:NORMAL +description:JavaCodeBlockで呼び出しをします +periodStartInclusive:2018-05-01_00:00:00 +periodEndExclusive:2037-05-01_00:00:00 +siteId:69 +# +# calculatorNameが指定できるようになった。この名前はdependsOnで指定できる +# +calculatorName:callJavaCodeBlock + +# +# 非標準項目。ResultConsumerでここに指定された名前でCalculationContextの変数に結果を格納する +# +var:matchNumber + +# +# ここに他の式名(CalculatorName)を指定する事で指定した式を実行した後にこの式を実行する +# 複数指定する場合はカンマ区切りで指定する +# 実際はorder numberで実行順を管理していてdependsOnで指定されたCalculatorのorder numberを +# このCalculatorのorder number-1に設定するだけである。このorder numberでsortされて順番に実行される +# +dependsOn: + +# +# 式の戻り値を指定する +# string,boolean,byte,short,int,long,float,doubleを指定する +# +resultType:float + +# +# JavaCodeBlockを指定できる。 複数記述可能 +# java:の後にfull package class nameを指定する。この例ではdefault packageを指定している +# 同一のクラス名でロジックを変更したい場合classのunloadは出来ないのでpackage名にversionを入れてversion管理をする +# +formula: +```java:CheckDigits +//package sample.v1;//version1. if logic updates then update package. +import org.unlaxer.tinyexpression.CalculationContext; + +public class CheckDigits{ + public boolean check(CalculationContext calculationContext,String target){ + return 1; + } +} +``` +import CheckDigits#check as checkDigits; +var $input as string set if not exists 'not number' description='入力値'; +if(external returning as boolean checkDigits($input)){ + 1 +}else{ + 0 +} +---END_OF_PART--- +tags:NORMAL +description:JavaCodeBlockで呼び出しをします。packageを使用します +periodStartInclusive:2018-05-01_00:00:00 +periodEndExclusive:2037-05-01_00:00:00 +siteId:69 +calculatorName:callJavaCodeBlockWithPackage +var:matchAlphabet +dependsOn: +resultType:float +formula: +```java:sample.v1.CheckAlphabets +package sample.v1;//version1. if logic updates then update package. +import org.unlaxer.tinyexpression.CalculationContext; + +public class CheckAlphabets{ + public boolean check(CalculationContext calculationContext,String target){ + return 1; + } +} +``` +import sample.v1.CheckAlphabets#check as checkAlphabets; +var $inputName as string set if not exists '1234' description='入力値'; +if(external returning as boolean checkAlphabets($inputName)){ + 1 +}else{ + 0 +} +---END_OF_PART--- +tags:NORMAL +description:最終的な点数をcheckKindとして出力します +periodStartInclusive:2018-05-01_00:00:00 +periodEndExclusive:2037-05-01_00:00:00 +siteId:69 +# outputToでcheckKindやfieldやvarなどをしてするけどいらないかな? +calculatorName:setFloatToMintia +checkKind:Mintia + +# +# ここに他の式名(CalculatorName)を指定する事で指定した式を実行した後にこの式を実行する +# 実際はorder numberで実行順を管理していてdependsOnで指定されたCalculatorのorder numberを +# このCalculatorのorder number-1に設定するだけである。このorder numberでsortされて順番に実行される +# 複数指定する場合はカンマ区切りで指定する +# この場合setSqrt2ToVarを実行してからこの式が実行される +# +dependsOn:setSqrt2ToVar +resultType:float +formula: +match{ + $mintia == 2 -> $sqrt , + default -> 3.141592 +} +---END_OF_PART--- +tags:NORMAL +description:$sqrtにsqrt(2)を代入します +periodStartInclusive:2018-05-01_00:00:00 +periodEndExclusive:2037-05-01_00:00:00 +siteId:69 +calculatorName:setSqrt2ToVar +var:sqrt +dependsOn: +resultType:float +formula: +sqrt(2) +---END_OF_PART--- +tags:NORMAL +description:CheckResultのtheScoreに値をセットします +periodStartInclusive:2018-05-01_00:00:00 +periodEndExclusive:2037-05-01_00:00:00 +siteId:69 +calculatorName:setTheScore +field:theScore +dependsOn: +resultType:float +formula: +6969 +---END_OF_PART--- +tags:NORMAL +description:$nameに文字列を代入します +periodStartInclusive:2018-05-01_00:00:00 +periodEndExclusive:2037-05-01_00:00:00 +siteId:69 + +# +# 非標準項目。ResultConsumerでここに指定された名前でCheckResultのfieldに結果を格納する +# +field:theName + +# +# 非標準項目。ResultConsumerでここに指定された名前でCalculationContextの変数に結果を格納する +# +var:name +dependsOn: + +# +# この式ではstringを返す +# +resultType:String +formula: +"opaopa" +---END_OF_PART--- +tags:NORMAL +description:longで計算します +periodStartInclusive:2018-05-01_00:00:00 +periodEndExclusive:2037-05-01_00:00:00 +siteId:69 +calculatorName:calcLong +var:ロング計算結果 +dependsOn: +resultType:Long +formula: +423372036854775807+423372036854775807 +---END_OF_PART--- +tags:NORMAL +description:doubleで計算します +periodStartInclusive:2018-05-01_00:00:00 +periodEndExclusive:2037-05-01_00:00:00 +siteId:69 +calculatorName:calcDouble +var:ダブル計算結果 +dependsOn: +resultType:double +formula: +423372036854775807+423372036854775807 +---END_OF_PART--- +tags:NORMAL +description:booleanで計算します +periodStartInclusive:2018-05-01_00:00:00 +periodEndExclusive:2037-05-01_00:00:00 +siteId:69 +calculatorName:calcBoolean +var:真偽値計算結果 +dependsOn: +resultType:boolean +formula: +(1==1)&true|('肉'!='魚') +---END_OF_PART--- +var:numberType指定真偽値計算結果 +dependsOn: +resultType:boolean + +# +# 数値の型を指定できる +# +numberType:long +formula: +(423372036854775807==423372036854775807)&true|('肉'!='魚') +---END_OF_PART--- \ No newline at end of file diff --git a/tools/tinyexpression-p4-lsp-vscode/src/embedded-test/resources/unicode-crlf-error.txt b/tools/tinyexpression-p4-lsp-vscode/src/embedded-test/resources/unicode-crlf-error.txt new file mode 100644 index 00000000..d197ee7c --- /dev/null +++ b/tools/tinyexpression-p4-lsp-vscode/src/embedded-test/resources/unicode-crlf-error.txt @@ -0,0 +1,201 @@ +tags:NORMAL +description:😀日本語JavaCodeBlockで呼び出しをします +periodStartInclusive:2018-05-01_00:00:00 +periodEndExclusive:2037-05-01_00:00:00 +siteId:69 +# +# calculatorNameが指定できるようになった。この名前はdependsOnで指定できる +# +calculatorName:callJavaCodeBlock + +# +# 非標準項目。ResultConsumerでここに指定された名前でCalculationContextの変数に結果を格納する +# +var:matchNumber + +# +# ここに他の式名(CalculatorName)を指定する事で指定した式を実行した後にこの式を実行する +# 複数指定する場合はカンマ区切りで指定する +# 実際はorder numberで実行順を管理していてdependsOnで指定されたCalculatorのorder numberを +# このCalculatorのorder number-1に設定するだけである。このorder numberでsortされて順番に実行される +# +dependsOn: + +# +# 式の戻り値を指定する +# string,boolean,byte,short,int,long,float,doubleを指定する +# +resultType:float + +# +# JavaCodeBlockを指定できる。 複数記述可能 +# java:の後にfull package class nameを指定する。この例ではdefault packageを指定している +# 同一のクラス名でロジックを変更したい場合classのunloadは出来ないのでpackage名にversionを入れてversion管理をする +# +formula: +```java:CheckDigits +//😀日本語 +//package sample.v1;//version1. if logic updates then update package. +import org.unlaxer.tinyexpression.CalculationContext; + +public class CheckDigits{ + public boolean check(CalculationContext calculationContext,String target){ + return 1; + } +} +``` +import CheckDigits#check as checkDigits; +var $input as string set if not exists 'not number' description='入力値'; +if(external returning as boolean checkDigits($input)){ + 1 +}else{ + 0 +} +---END_OF_PART--- +tags:NORMAL +description:JavaCodeBlockで呼び出しをします。packageを使用します +periodStartInclusive:2018-05-01_00:00:00 +periodEndExclusive:2037-05-01_00:00:00 +siteId:69 +calculatorName:callJavaCodeBlockWithPackage +var:matchAlphabet +dependsOn: +resultType:float +formula: +```java:sample.v1.CheckAlphabets +package sample.v1;//version1. if logic updates then update package. +import org.unlaxer.tinyexpression.CalculationContext; + +public class CheckAlphabets{ + public boolean check(CalculationContext calculationContext,String target){ + return target.matches("[a-zA-Z]+"); + } +} +``` +import sample.v1.CheckAlphabets#check as checkAlphabets; +var $inputName as string set if not exists '1234' description='入力値'; +if(external returning as boolean checkAlphabets($inputName)){ + 1 +}else{ + 0 +} +---END_OF_PART--- +tags:NORMAL +description:最終的な点数をcheckKindとして出力します +periodStartInclusive:2018-05-01_00:00:00 +periodEndExclusive:2037-05-01_00:00:00 +siteId:69 +# outputToでcheckKindやfieldやvarなどをしてするけどいらないかな? +calculatorName:setFloatToMintia +checkKind:Mintia + +# +# ここに他の式名(CalculatorName)を指定する事で指定した式を実行した後にこの式を実行する +# 実際はorder numberで実行順を管理していてdependsOnで指定されたCalculatorのorder numberを +# このCalculatorのorder number-1に設定するだけである。このorder numberでsortされて順番に実行される +# 複数指定する場合はカンマ区切りで指定する +# この場合setSqrt2ToVarを実行してからこの式が実行される +# +dependsOn:setSqrt2ToVar +resultType:float +formula: +match{ + $mintia == 2 -> $sqrt , + default -> 3.141592 +} +---END_OF_PART--- +tags:NORMAL +description:$sqrtにsqrt(2)を代入します +periodStartInclusive:2018-05-01_00:00:00 +periodEndExclusive:2037-05-01_00:00:00 +siteId:69 +calculatorName:setSqrt2ToVar +var:sqrt +dependsOn: +resultType:float +formula: +sqrt(2) +---END_OF_PART--- +tags:NORMAL +description:CheckResultのtheScoreに値をセットします +periodStartInclusive:2018-05-01_00:00:00 +periodEndExclusive:2037-05-01_00:00:00 +siteId:69 +calculatorName:setTheScore +field:theScore +dependsOn: +resultType:float +formula: +6969 +---END_OF_PART--- +tags:NORMAL +description:$nameに文字列を代入します +periodStartInclusive:2018-05-01_00:00:00 +periodEndExclusive:2037-05-01_00:00:00 +siteId:69 + +# +# 非標準項目。ResultConsumerでここに指定された名前でCheckResultのfieldに結果を格納する +# +field:theName + +# +# 非標準項目。ResultConsumerでここに指定された名前でCalculationContextの変数に結果を格納する +# +var:name +dependsOn: + +# +# この式ではstringを返す +# +resultType:String +formula: +"opaopa" +---END_OF_PART--- +tags:NORMAL +description:longで計算します +periodStartInclusive:2018-05-01_00:00:00 +periodEndExclusive:2037-05-01_00:00:00 +siteId:69 +calculatorName:calcLong +var:ロング計算結果 +dependsOn: +resultType:Long +formula: +423372036854775807+423372036854775807 +---END_OF_PART--- +tags:NORMAL +description:doubleで計算します +periodStartInclusive:2018-05-01_00:00:00 +periodEndExclusive:2037-05-01_00:00:00 +siteId:69 +calculatorName:calcDouble +var:ダブル計算結果 +dependsOn: +resultType:double +formula: +423372036854775807+423372036854775807 +---END_OF_PART--- +tags:NORMAL +description:booleanで計算します +periodStartInclusive:2018-05-01_00:00:00 +periodEndExclusive:2037-05-01_00:00:00 +siteId:69 +calculatorName:calcBoolean +var:真偽値計算結果 +dependsOn: +resultType:boolean +formula: +(1==1)&true|('肉'!='魚') +---END_OF_PART--- +var:numberType指定真偽値計算結果 +dependsOn: +resultType:boolean + +# +# 数値の型を指定できる +# +numberType:long +formula: +(423372036854775807==423372036854775807)&true|('肉'!='魚') +---END_OF_PART--- \ No newline at end of file diff --git a/tools/tinyexpression-p4-lsp-vscode/src/embedded/java/org/unlaxer/tinyexpression/lsp/p4/embedded/ProductionEmbeddedDiagnostics.java b/tools/tinyexpression-p4-lsp-vscode/src/embedded/java/org/unlaxer/tinyexpression/lsp/p4/embedded/ProductionEmbeddedDiagnostics.java new file mode 100644 index 00000000..a8ff0bef --- /dev/null +++ b/tools/tinyexpression-p4-lsp-vscode/src/embedded/java/org/unlaxer/tinyexpression/lsp/p4/embedded/ProductionEmbeddedDiagnostics.java @@ -0,0 +1,55 @@ +package org.unlaxer.tinyexpression.lsp.p4.embedded; + +import java.nio.file.Path; +import java.time.Duration; +import java.util.*; +import org.eclipse.lsp4j.*; +import org.unlaxer.source.*; +import org.unlaxer.source.LanguageRegions.Operation; +import org.unlaxer.tinyexpression.lsp.p4.EmbeddedLanguageDiagnostics; + +/** javac parse/analyze only (-proc:none); no class loading or user code evaluation. */ +public final class ProductionEmbeddedDiagnostics implements EmbeddedLanguageDiagnostics { + private final String classpath = System.getProperty("java.class.path"); + private final ProviderProcess provider = new ProviderProcess( + List.of(Path.of(System.getProperty("java.home"), "bin", "java").toString(), "-cp", classpath, + "org.unlaxer.dsl.provider.JavacProvider"), + new ProviderProtocol.Identity("javac", "21.0.9"), Set.of(Operation.VALIDATE), Duration.ofSeconds(30)); + + @Override public List analyze(String uri, int version, String fullContent) { + if (!fullContent.lines().anyMatch(line -> line.stripTrailing().equals("formula:"))) return List.of(); + var host = new DocumentSnapshot(uri, version, fullContent); + var binding = TinyProductionBridge.parse(host); + if (binding.regions().stream().anyMatch(region -> region.parseState() == org.unlaxer.source.LanguageRegions.State.FAILED)) + throw new IllegalArgumentException("incomplete or invalid production region; no speculative Java children retained"); + var project = new LanguageQueries.Project("tiny-lsp", version, Map.of(uri, host), Map.of("classpath", classpath)); + var queries = binding.queries(project, provider); + var diagnostics = new ArrayList(); + for (var result : queries.diagnosticsAll(host, project, Map.of())) { + if (!binding.javaFiles().containsKey(result.region())) continue; + if (result.state() != org.unlaxer.source.LanguageRegions.State.COMPLETE && result.state() != org.unlaxer.source.LanguageRegions.State.PARTIAL) + throw new IllegalArgumentException(result.region() + ": " + result.state()); + for (var item : result.diagnostics()) { + for (var mapping : item.locations()) { + var location = mapping.location(); + if (!location.snapshot().equals(host)) throw new IllegalArgumentException("foreign diagnostic requires workspace consumer"); + var start = host.lsp(location.span().start()); var end = host.lsp(location.span().end()); + var diagnostic = new Diagnostic(new Range(new Position(start.line(), start.character()), new Position(end.line(), end.character())), + item.message(), severity(item.severity()), "tinyexpression-java"); + diagnostic.setCode(item.code()); + diagnostic.setData(Map.of("region", result.region(), "version", Integer.toString(version), "exact", mapping.exact(), "severity", item.severity())); + diagnostics.add(diagnostic); + } + } + } + return diagnostics; + } + private static DiagnosticSeverity severity(String value) { + return switch(value) { + case "ERROR" -> DiagnosticSeverity.Error; + case "WARNING", "MANDATORY_WARNING" -> DiagnosticSeverity.Warning; + case "HINT", "HELP", "SUGGESTION" -> DiagnosticSeverity.Hint; + default -> DiagnosticSeverity.Information; + }; + } +} diff --git a/tools/tinyexpression-p4-lsp-vscode/src/embedded/java/org/unlaxer/tinyexpression/lsp/p4/embedded/TinyProductionBridge.java b/tools/tinyexpression-p4-lsp-vscode/src/embedded/java/org/unlaxer/tinyexpression/lsp/p4/embedded/TinyProductionBridge.java new file mode 100644 index 00000000..aadcab53 --- /dev/null +++ b/tools/tinyexpression-p4-lsp-vscode/src/embedded/java/org/unlaxer/tinyexpression/lsp/p4/embedded/TinyProductionBridge.java @@ -0,0 +1,104 @@ +package org.unlaxer.tinyexpression.lsp.p4.embedded; + +// Port of unlaxer-parser PR #470, d247f34a; shared Java/Rust production fixture contract. +import java.util.*; +import org.unlaxer.*; +import org.unlaxer.context.ParseContext; +import org.unlaxer.tinyexpression.loader.*; +import org.unlaxer.tinyexpression.p4.ubnfc.generated.TinyExpressionP4Parser; +import org.unlaxer.tinyexpression.p4.ubnfc.generated.api.ParseOptions; +import org.unlaxer.source.*; +import org.unlaxer.source.DocumentSnapshot.Span; +import org.unlaxer.source.LanguageRegions.*; + +/** Explicit native integration: production TinyExpression parsers, no evaluation or compilation. */ +public final class TinyProductionBridge { + public static final Language FORMULA = new Language("formulainfo", "tinyexpression", "f86ce8a5ab0ab7d23fdba2cd477187df8aa7607c", "FormulaInfo", "Document"); + public static final Language TINY = new Language("tinyexpression", "tinyexpression", "f86ce8a5ab0ab7d23fdba2cd477187df8aa7607c", "TinyExpressionP4", "Formula"); + public static final Language JAVA = new Language("java", "lang/java", "0.1.0", "Java21", "CompilationUnit"); + public record Binding(DocumentSnapshot host, List regions, Map javaFiles) { + public Binding { regions = List.copyOf(regions); javaFiles = Map.copyOf(javaFiles); } + public LanguageRegions tree() { return new LanguageRegions(host, regions, Set.of()); } + public LanguageQueries queries(LanguageQueries.Project project, LanguageQueries.Provider javaProvider) { + return new LanguageQueries(tree(), project, Map.of(JAVA, new LanguageQueries.Provider() { + public Set capabilities() { return javaProvider.capabilities(); } + public LanguageQueries.Response query(LanguageQueries.Request request) { return javaProvider.query(javaRequest(request)); } + public LanguageQueries.DiagnosticResponse diagnostics(LanguageQueries.Request request) { return javaProvider.diagnostics(javaRequest(request)); } + })); + } + public LanguageQueries.Request javaRequest(LanguageQueries.Request request) { + if (!regions.contains(request.region()) || !JAVA.equals(request.region().language())) throw new IllegalArgumentException("foreign region"); + var parameters = new HashMap<>(request.parameters()); + String file = javaFiles.get(request.region().id()); + if (file == null || parameters.containsKey("fileName") && !file.equals(parameters.get("fileName"))) throw new IllegalArgumentException("foreign Java filename"); + parameters.put("fileName", file); + return new LanguageQueries.Request(request.region(), request.operation(), request.cursor(), request.project(), parameters); + } + } + public static Binding parse(DocumentSnapshot host) { + if (host.length() > 1_048_576) throw new IllegalArgumentException("production document limit"); + Parsed parsed; + try (var context = new ParseContext(StringSource.createRootSource(host.text()))) { + parsed = new FormulaInfoBlocksParser().parse(context); + } + if (!parsed.isSucceeded() || parsed.getConsumed() == null || !parsed.getConsumed().source.sourceAsString().equals(host.text())) + throw new IllegalArgumentException("invalid FormulaInfo document"); + var regions = new ArrayList(); var files = new LinkedHashMap(); + regions.add(region(host, "root", null, FORMULA, new Span(0,host.length()), new Span(0,host.length()), State.COMPLETE)); + int index = 0; + for (var block : parsed.getRootToken().typed(FormulaInfoBlocksParser.class).getChildrenWithParserAsListTyped(FormulaInfoBlockParser.class)) { + Token formula = null; + for (Token token : FormulaInfoElementOrCommentParser.elements(block)) { + if (!(token.parser instanceof FormulaInfoElementParser)) continue; + var element = token.typed(FormulaInfoElementParser.class); + FormulaInfoElementParser.KeyValue pair; + try { pair = element.getParser().extract(element); } + catch (FormulaInfoParseException failure) { throw new IllegalArgumentException("invalid FormulaInfo value", failure); } + if (pair.getKey().equals("formula")) { + if (formula != null) throw new IllegalArgumentException("duplicate formula"); + formula = element.getChild(TokenPredicators.hasTag(FormulaInfoParser.Kind.value.tag())); + } + } + if (formula == null) continue; + if (++index > 256) throw new IllegalArgumentException("production section limit"); + int start = formula.source.offsetFromRoot().value(); + String raw = formula.source.sourceAsString(); + int skip = raw.startsWith("\r\n") ? 2 : raw.startsWith("\n") || raw.startsWith("\r") ? 1 : 0; + start += skip; raw = raw.substring(skip); + Span body = new Span(start, start + raw.codePointCount(0,raw.length())); + if (!host.slice(body).equals(raw)) throw new IllegalArgumentException("FormulaInfo source mismatch"); + String id = "formula/" + index; + String input = maskComments(raw); + var p4 = TinyExpressionP4Parser.parse(input, new ParseOptions(true,true,true,true,org.unlaxer.tinyexpression.p4.ubnfc.P4Scanners.ALL,512)); + regions.add(region(host,id,"root",TINY,body,body,p4.ok() ? State.COMPLETE : State.FAILED)); + if (!p4.ok()) continue; // Never retain children from speculative/failed parses. + var tokens = p4.lexical().stream().filter(t -> t.token() != null && t.ruleId().equals("TinyExpressionP4::CodeBlock")).toList(); + if (tokens.size() % 3 != 0) throw new IllegalArgumentException("P4 lexical contract changed"); + for (int n=0;n out.appendCodePoint(c=='\r'||c=='\n'?c:' ')); + else out.append(line); + } + return out.toString(); + } + private static Region region(DocumentSnapshot host,String id,String parent,Language language,Span full,Span body,State state) { + var virtual=new DocumentSnapshot(host.uri()+"#production/"+id,host.version(),host.slice(body)); + var map=new SegmentSourceMap(virtual,List.of(new SegmentSourceMap.Segment(new Span(0,virtual.length()),SegmentSourceMap.Kind.COPY,new SegmentSourceMap.Location(host,body)))); + return new Region(id,parent,language,full,body,map,state); + } +} diff --git a/tools/tinyexpression-p4-lsp-vscode/src/embedded/resources/META-INF/services/org.unlaxer.tinyexpression.lsp.p4.EmbeddedLanguageDiagnostics b/tools/tinyexpression-p4-lsp-vscode/src/embedded/resources/META-INF/services/org.unlaxer.tinyexpression.lsp.p4.EmbeddedLanguageDiagnostics new file mode 100644 index 00000000..04cada72 --- /dev/null +++ b/tools/tinyexpression-p4-lsp-vscode/src/embedded/resources/META-INF/services/org.unlaxer.tinyexpression.lsp.p4.EmbeddedLanguageDiagnostics @@ -0,0 +1 @@ +org.unlaxer.tinyexpression.lsp.p4.embedded.ProductionEmbeddedDiagnostics diff --git a/tools/tinyexpression-p4-lsp-vscode/src/extension.ts b/tools/tinyexpression-p4-lsp-vscode/src/extension.ts index dd8a08df..db0922b6 100644 --- a/tools/tinyexpression-p4-lsp-vscode/src/extension.ts +++ b/tools/tinyexpression-p4-lsp-vscode/src/extension.ts @@ -112,7 +112,8 @@ export async function activate( // Pass catalog path via initializationOptions so the server can use it // even if system property is not available (e.g. wrapped JVM) const initializationOptions: Record = { - useBundledVariables: catalog.useBundledVariables + useBundledVariables: catalog.useBundledVariables, + embeddedLanguageDiagnostics: vscode.workspace.getConfiguration("tinyExpressionP4Lsp").get("embeddedLanguageDiagnostics", false) }; if (catalog.catalogPath.length > 0) { initializationOptions.catalogPath = catalog.catalogPath; @@ -312,7 +313,7 @@ function registerCatalogFeatures(context: vscode.ExtensionContext, restartClient context.subscriptions.push( vscode.workspace.onDidChangeConfiguration((event) => { - if (event.affectsConfiguration("tinyExpressionP4Lsp.catalog")) { + if (event.affectsConfiguration("tinyExpressionP4Lsp.catalog") || event.affectsConfiguration("tinyExpressionP4Lsp.embeddedLanguageDiagnostics")) { updateCatalogStatus(); void restartClient(); } diff --git a/tools/tinyexpression-p4-lsp-vscode/src/main/java/org/unlaxer/tinyexpression/lsp/p4/EmbeddedLanguageDiagnostics.java b/tools/tinyexpression-p4-lsp-vscode/src/main/java/org/unlaxer/tinyexpression/lsp/p4/EmbeddedLanguageDiagnostics.java new file mode 100644 index 00000000..71fc8160 --- /dev/null +++ b/tools/tinyexpression-p4-lsp-vscode/src/main/java/org/unlaxer/tinyexpression/lsp/p4/EmbeddedLanguageDiagnostics.java @@ -0,0 +1,9 @@ +package org.unlaxer.tinyexpression.lsp.p4; + +import java.util.List; +import org.eclipse.lsp4j.Diagnostic; + +/** Optional analysis of the untouched full document; implementations must never evaluate user code. */ +public interface EmbeddedLanguageDiagnostics { + List analyze(String uri, int version, String fullContent); +} diff --git a/tools/tinyexpression-p4-lsp-vscode/src/main/java/org/unlaxer/tinyexpression/lsp/p4/TinyExpressionP4LanguageServerExt.java b/tools/tinyexpression-p4-lsp-vscode/src/main/java/org/unlaxer/tinyexpression/lsp/p4/TinyExpressionP4LanguageServerExt.java index 0ba46f8b..ebcf9b5c 100644 --- a/tools/tinyexpression-p4-lsp-vscode/src/main/java/org/unlaxer/tinyexpression/lsp/p4/TinyExpressionP4LanguageServerExt.java +++ b/tools/tinyexpression-p4-lsp-vscode/src/main/java/org/unlaxer/tinyexpression/lsp/p4/TinyExpressionP4LanguageServerExt.java @@ -353,6 +353,15 @@ String keywordHover(String name) { // LanguageClientAware // ========================================================================= + private EmbeddedLanguageDiagnostics embeddedDiagnostics; + private final Map embeddedVersions = new java.util.concurrent.ConcurrentHashMap<>(); + + /** Explicit opt-in. The default released server keeps its existing parser/backend behavior. */ + public void useEmbeddedDiagnostics(EmbeddedLanguageDiagnostics provider) { + this.embeddedDiagnostics = java.util.Objects.requireNonNull(provider); + incrementalCaches.clear(); + } + @Override public void connect(LanguageClient client) { super.connect(client); @@ -367,7 +376,22 @@ public void connect(LanguageClient client) { public CompletableFuture initialize(InitializeParams params) { // Call super to trigger initCatalogResolver(params) in the generated class. // We discard the returned capabilities and build our own below. - super.initialize(params); + var initialized = super.initialize(params); + if (initialized.isCompletedExceptionally()) return initialized; + Object raw = params.getInitializationOptions(); + var options = new com.google.gson.Gson().toJsonTree(raw); + if (options.isJsonObject() && options.getAsJsonObject().has("embeddedLanguageDiagnostics")) { + var enabled = options.getAsJsonObject().get("embeddedLanguageDiagnostics"); + if (!enabled.isJsonPrimitive() || !enabled.getAsJsonPrimitive().isBoolean()) + throw new IllegalArgumentException("embeddedLanguageDiagnostics must be a boolean"); + if (enabled.getAsBoolean()) { + var providers = java.util.ServiceLoader.load(EmbeddedLanguageDiagnostics.class).stream().toList(); + if (providers.size() != 1) throw new IllegalArgumentException("embedded-language profile requires exactly one diagnostics provider"); + useEmbeddedDiagnostics(providers.get(0).get()); + } else { + embeddedDiagnostics = null; + } + } ServerCapabilities cap = new ServerCapabilities(); cap.setTextDocumentSync(TextDocumentSyncKind.Full); @@ -398,6 +422,8 @@ public CompletableFuture initialize(InitializeParams params) { cap.setWorkspaceSymbolProvider(true); cap.setDocumentFormattingProvider(true); + cap.setExperimental(Map.of("embeddedLanguageDiagnostics", Map.of( + "enabled", embeddedDiagnostics != null, "fullDocument", true, "executesUserCode", false))); return CompletableFuture.completedFuture(new InitializeResult(cap)); } @@ -445,12 +471,14 @@ static FormulaSection extractFormulaSection(String fullContent) { // ========================================================================= @Override - public ParseResult parseDocument(String uri, String content) { + public synchronized ParseResult parseDocument(String uri, String content) { + // Every full snapshot must reach the provider, including changes outside the first formula. + if (embeddedDiagnostics != null) incrementalCaches.remove(uri); FormulaSection fs = documentFilter.extract(content); if (fs != null) { // FormulaInfo file: suppress the parent's whole-file diagnostics, then parse formula portion. - if (extClient != null) { + if (extClient != null && embeddedDiagnostics == null) { extClient.publishDiagnostics(new PublishDiagnosticsParams(uri, List.of())); } return parseAndEnrich(uri, fs.content(), fs.lineOffset(), content); @@ -812,7 +840,20 @@ private void publishEnrichedDiagnostics(String uri, String content, d.setData(diagnosticData(code, "syntax", start, details)); diagnostics.add(d); } - extClient.publishDiagnostics(new PublishDiagnosticsParams(uri, diagnostics)); + if (embeddedDiagnostics != null) { + try { + diagnostics.addAll(embeddedDiagnostics.analyze(uri, embeddedVersions.getOrDefault(uri, 0), + extDocuments.get(uri).fullContent())); + } catch (RuntimeException failure) { + Diagnostic diagnostic = new Diagnostic(new Range(new Position(0, 0), new Position(0, 0)), + "Embedded language analysis unavailable: " + failure.getMessage(), DiagnosticSeverity.Warning, "tinyexpression-embedded"); + diagnostic.setCode("EMBEDDED_UNAVAILABLE"); + diagnostics.add(diagnostic); + } + } + var published = new PublishDiagnosticsParams(uri, diagnostics); + if (embeddedDiagnostics != null) published.setVersion(embeddedVersions.getOrDefault(uri, 0)); + extClient.publishDiagnostics(published); } private List computeStrictMatchSemanticIssues( @@ -1538,27 +1579,56 @@ class ExtTextDocumentService implements TextDocumentService { @Override public void didOpen(DidOpenTextDocumentParams params) { - server.parseDocument( - params.getTextDocument().getUri(), - params.getTextDocument().getText()); + synchronized (server) { + if (server.embeddedDiagnostics != null) { + String uri = params.getTextDocument().getUri(); + int version = params.getTextDocument().getVersion(); + if (server.embeddedVersions.containsKey(uri) && version <= server.embeddedVersions.get(uri)) return; + server.embeddedVersions.put(uri, version); + } + server.parseDocument(params.getTextDocument().getUri(), params.getTextDocument().getText()); + } } @Override public void didChange(DidChangeTextDocumentParams params) { - server.parseDocument( - params.getTextDocument().getUri(), - params.getContentChanges().get(0).getText()); + synchronized (server) { + if (server.embeddedDiagnostics != null) { + String uri = params.getTextDocument().getUri(); + Integer version = params.getTextDocument().getVersion(); + if (!server.embeddedVersions.containsKey(uri) || version == null || version <= server.embeddedVersions.get(uri) + || params.getContentChanges().isEmpty() || params.getContentChanges().stream().anyMatch(change -> change.getRange() != null)) return; + server.embeddedVersions.put(uri, version); + server.parseDocument(uri, params.getContentChanges().get(params.getContentChanges().size() - 1).getText()); + return; + } + server.parseDocument(params.getTextDocument().getUri(), params.getContentChanges().get(0).getText()); + } } @Override public void didClose(DidCloseTextDocumentParams params) { - String closedUri = params.getTextDocument().getUri(); - server.extDocuments.remove(closedUri); - server.incrementalCaches.remove(closedUri); + synchronized (server) { + String closedUri = params.getTextDocument().getUri(); + server.extDocuments.remove(closedUri); + server.incrementalCaches.remove(closedUri); + if (server.embeddedDiagnostics != null) { + server.embeddedVersions.remove(closedUri); + if (server.extClient != null) server.extClient.publishDiagnostics(new PublishDiagnosticsParams(closedUri, List.of())); + } + } } @Override - public void didSave(DidSaveTextDocumentParams params) {} + public void didSave(DidSaveTextDocumentParams params) { + synchronized (server) { + if (server.embeddedDiagnostics != null) { + String uri = params.getTextDocument().getUri(); + var state = server.extDocuments.get(uri); + if (state != null) server.parseDocument(uri, state.fullContent()); + } + } + } // ── completion (enhanced with method/variable autocomplete + metadata) ── From a979e570376a5c9237694d59af21411961978254 Mon Sep 17 00:00:00 2001 From: opaopa6969 Date: Fri, 9 Oct 2026 13:59:39 +0900 Subject: [PATCH 2/5] Use certified editor regions for embedded Java EOF completion --- .../EMBEDDED-LANGUAGES.md | 7 ++- .../tinyexpression-p4-lsp-vscode/package.json | 2 +- .../scripts/check-embedded-lsp.py | 9 ++- .../p4/ProductionEmbeddedDiagnosticsTest.java | 11 ++++ .../embedded-test/resources/partial-eof.txt | 4 ++ .../ProductionEmbeddedDiagnostics.java | 49 +++++++++++++-- .../lsp/p4/embedded/TinyProductionBridge.java | 63 ++++++++++++++++--- .../lsp/p4/EmbeddedLanguageDiagnostics.java | 5 ++ .../p4/TinyExpressionP4LanguageServerExt.java | 15 +++++ 9 files changed, 147 insertions(+), 18 deletions(-) create mode 100644 tools/tinyexpression-p4-lsp-vscode/src/embedded-test/resources/partial-eof.txt diff --git a/tools/tinyexpression-p4-lsp-vscode/EMBEDDED-LANGUAGES.md b/tools/tinyexpression-p4-lsp-vscode/EMBEDDED-LANGUAGES.md index 447f97fe..c63e0c93 100644 --- a/tools/tinyexpression-p4-lsp-vscode/EMBEDDED-LANGUAGES.md +++ b/tools/tinyexpression-p4-lsp-vscode/EMBEDDED-LANGUAGES.md @@ -10,8 +10,11 @@ mvn clean package -Pembedded-languages -Dgpg.skip=true Point `tinyExpressionP4Lsp.server.jarPath` at the resulting server jar, use JDK 21.0.9, and enable `tinyExpressionP4Lsp.embeddedLanguageDiagnostics`. Other LSP clients can set `initializationOptions.embeddedLanguageDiagnostics: true`. Enabling it with a server that lacks the profile fails initialization explicitly. Merely including the profile does not enable analysis. -The existing `TinyExpressionP4LanguageServerExt` now sends untouched, versioned full documents to an optional diagnostics provider. The production FormulaInfo and P4 parsers construct all section regions; javac diagnostics from each Java body are mapped to original UTF-16 positions. Updating later sections does not reuse first-section cache results. Stale updates are rejected, save revalidates, and close clears diagnostics. Invalid or incomplete region extraction and unavailable providers emit `EMBEDDED_UNAVAILABLE`, not empty success. Existing first-section Tiny completion, formatting and other methods retain their prior behavior; this profile adds Java diagnostics only. +The existing `TinyExpressionP4LanguageServerExt` now sends untouched, versioned full documents to an optional diagnostics provider. The production FormulaInfo and P4 parsers construct all section regions; javac diagnostics from each Java body are mapped to original UTF-16 positions. Updating later sections does not reuse first-section cache results. Stale updates are rejected, save revalidates, and close clears diagnostics. Invalid or incomplete region extraction and unavailable providers emit `EMBEDDED_UNAVAILABLE`, not empty success. Inside an owned Java region, completion now uses the same production editor binding and returns source-mapped javac edits, including at unfinished host EOF. Outside Java, existing first-section Tiny completion, formatting and other methods retain their prior behavior. -The bridge is a namespaced port of unlaxer-parser PR #470, commit d247f34a. Its Java/Rust contract is tested there using the same production fixture, independent code-point/UTF-16 coordinates and rejection cases. This repository's existing LSP consumer is Java; no Rust LSP consumer is claimed here. Unclosed Java region completion and foreign dependency diagnostics need a later consumer extension. Workspace ownership is rejected explicitly by this single-document adapter. +The bridge is a namespaced port of unlaxer-parser production editor bridge commit f2fc6faa (the strict bridge originated in PR #470). Its Java/Rust contract is tested there using the same production fixture, independent code-point/UTF-16 coordinates and rejection cases. This repository's existing LSP consumer is Java; no Rust LSP consumer is claimed here. Foreign dependency diagnostics need a later consumer extension. Workspace ownership is rejected explicitly by this single-document adapter. Run `mvn test -Pembedded-languages -Dtest=ProductionEmbeddedDiagnosticsTest,TinyExpressionP4LanguageServerExtTest,TinyExpressionP4DebugAdapterExtTest` here. The integration test runs real javac on production fixture 69 (ten formulas, two Java bodies), checks both error positions, a second-section-only update, Unicode/CRLF, stale updates, save, close and unavailable extraction. Run the 39 existing LSP/DAP tests without the profile as a separate clean build to verify the released dependency path. Use an isolated Maven repository if other checkouts install Tiny artifacts with different generated AST versions. + +The explicit editor entry certifies leading CodeBlock prefixes using the real production parser. Recognition-only synthetic closing text never enters the snapshot or edits. Incomplete Java diagnostics and EOF completion stay inside the original body; valid siblings remain usable when another formula fails. The strict parser is unchanged and still reports unfinished formulas as FAILED. The packaged stdio smoke additionally verifies the Unicode/CRLF unfinished-body completion edit at line 3, characters 41–44. +The new completion method is optional on the dependency-neutral SPI. The default released build does not load a provider or alter its Tiny completion behavior. Both diagnostics and completion use the same immutable full snapshot under the existing server's lifecycle lock, so an intervening update cannot relabel an old result with a new version. diff --git a/tools/tinyexpression-p4-lsp-vscode/package.json b/tools/tinyexpression-p4-lsp-vscode/package.json index 5f53ffce..64cadf59 100644 --- a/tools/tinyexpression-p4-lsp-vscode/package.json +++ b/tools/tinyexpression-p4-lsp-vscode/package.json @@ -299,7 +299,7 @@ "tinyExpressionP4Lsp.embeddedLanguageDiagnostics": { "type": "boolean", "default": false, - "description": "Analyze embedded Java in every FormulaInfo section without executing user code. Requires the explicitly built embedded-languages server profile and JDK 21.0.9. Changing this setting restarts the server." + "description": "Diagnose and complete embedded Java in every FormulaInfo section without executing user code. Requires the explicitly built embedded-languages server profile and JDK 21.0.9. Changing this setting restarts the server." }, "tinyExpressionP4Lsp.server.javaPath": { "type": "string", diff --git a/tools/tinyexpression-p4-lsp-vscode/scripts/check-embedded-lsp.py b/tools/tinyexpression-p4-lsp-vscode/scripts/check-embedded-lsp.py index ecd837ba..7ea60e0e 100644 --- a/tools/tinyexpression-p4-lsp-vscode/scripts/check-embedded-lsp.py +++ b/tools/tinyexpression-p4-lsp-vscode/scripts/check-embedded-lsp.py @@ -52,12 +52,19 @@ def receive(predicate): diagnostics = [item for item in published['diagnostics'] if item.get('source') == 'tinyexpression-java'] assert [(item['range']['start']['line'], item['range']['start']['character']) for item in diagnostics] == [(41, 9), (69, 9)], published assert all(item['code'] == 'compiler.err.prob.found.req' and item['severity'] == 1 for item in diagnostics) + partial = (root / 'src/embedded-test/resources/partial-eof.txt').read_bytes().decode('utf-8') + send('textDocument/didChange', {'textDocument': {'uri': uri, 'version': 2}, 'contentChanges': [{'text': partial}]}) + assert receive(lambda value: value.get('method') == 'textDocument/publishDiagnostics')['params']['version'] == 2 + send('textDocument/completion', {'textDocument': {'uri': uri}, 'position': {'line': 3, 'character': 44}}, 3) + completions = receive(lambda value: value.get('id') == 3)['result'] + target = next(item for item in completions if item['label'] == 'target') + assert target['textEdit'] == {'range': {'start': {'line': 3, 'character': 41}, 'end': {'line': 3, 'character': 44}}, 'newText': 'target'}, target send('textDocument/didClose', {'textDocument': {'uri': uri}}) assert receive(lambda value: value.get('method') == 'textDocument/publishDiagnostics')['params']['diagnostics'] == [] send('shutdown', {}, 2) receive(lambda value: value.get('id') == 2) send('exit', {}) - print('PASS: packaged Tiny P4 launcher -> real javac -> two original FormulaInfo diagnostic positions -> close clear') + print('PASS: packaged Tiny P4 launcher -> real javac -> two original FormulaInfo diagnostic positions -> partial EOF completion -> close clear') finally: process.kill() process.wait(timeout=5) diff --git a/tools/tinyexpression-p4-lsp-vscode/src/embedded-test/java/org/unlaxer/tinyexpression/lsp/p4/ProductionEmbeddedDiagnosticsTest.java b/tools/tinyexpression-p4-lsp-vscode/src/embedded-test/java/org/unlaxer/tinyexpression/lsp/p4/ProductionEmbeddedDiagnosticsTest.java index 6f5ebb24..aef9bea1 100644 --- a/tools/tinyexpression-p4-lsp-vscode/src/embedded-test/java/org/unlaxer/tinyexpression/lsp/p4/ProductionEmbeddedDiagnosticsTest.java +++ b/tools/tinyexpression-p4-lsp-vscode/src/embedded-test/java/org/unlaxer/tinyexpression/lsp/p4/ProductionEmbeddedDiagnosticsTest.java @@ -51,6 +51,17 @@ public void logMessage(MessageParams value) {} assertTrue(last(notifications).toString(),javaDiagnostics(last(notifications)).isEmpty()); service.didChange(new DidChangeTextDocumentParams(new VersionedTextDocumentIdentifier(uri,5),List.of(new TextDocumentContentChangeEvent("formula:\ninvalid\n")))); assertTrue(last(notifications).getDiagnostics().stream().anyMatch(d->d.getCode()!=null && "EMBEDDED_UNAVAILABLE".equals(d.getCode().getLeft()))); + String partial=Files.readString(fixtures.resolve("partial-eof.txt")); + service.didChange(new DidChangeTextDocumentParams(new VersionedTextDocumentIdentifier(uri,6),List.of(new TextDocumentContentChangeEvent(partial)))); + assertFalse(last(notifications).toString(),javaDiagnostics(last(notifications)).isEmpty()); + var completion=service.completion(new CompletionParams(new TextDocumentIdentifier(uri),new Position(3,44))).join().getLeft(); + var target=completion.stream().filter(item->item.getLabel().equals("target")).findFirst().orElseThrow(); + assertEquals(new Range(new Position(3,41),new Position(3,44)),target.getTextEdit().getLeft().getRange()); + assertEquals("target",target.getTextEdit().getLeft().getNewText()); + var snapshot=new org.unlaxer.source.DocumentSnapshot(uri,7,partial+"get"); + assertTrue(org.unlaxer.tinyexpression.lsp.p4.embedded.TinyProductionBridge.parse(snapshot).regions().stream().anyMatch(region->region.parseState()==org.unlaxer.source.LanguageRegions.State.FAILED)); + service.didChange(new DidChangeTextDocumentParams(new VersionedTextDocumentIdentifier(uri,7),List.of(new TextDocumentContentChangeEvent(snapshot.text())))); + assertEquals(Integer.valueOf(7),last(notifications).getVersion()); service.didClose(new DidCloseTextDocumentParams(new TextDocumentIdentifier(uri)));assertTrue(last(notifications).getDiagnostics().isEmpty()); } private static PublishDiagnosticsParams last(List values) { return values.get(values.size()-1); } diff --git a/tools/tinyexpression-p4-lsp-vscode/src/embedded-test/resources/partial-eof.txt b/tools/tinyexpression-p4-lsp-vscode/src/embedded-test/resources/partial-eof.txt new file mode 100644 index 00000000..632c6c38 --- /dev/null +++ b/tools/tinyexpression-p4-lsp-vscode/src/embedded-test/resources/partial-eof.txt @@ -0,0 +1,4 @@ +formula: +# 😀日本語 +```java:Main +class Main { int target; int f(){ return tar \ No newline at end of file diff --git a/tools/tinyexpression-p4-lsp-vscode/src/embedded/java/org/unlaxer/tinyexpression/lsp/p4/embedded/ProductionEmbeddedDiagnostics.java b/tools/tinyexpression-p4-lsp-vscode/src/embedded/java/org/unlaxer/tinyexpression/lsp/p4/embedded/ProductionEmbeddedDiagnostics.java index a8ff0bef..da3baca1 100644 --- a/tools/tinyexpression-p4-lsp-vscode/src/embedded/java/org/unlaxer/tinyexpression/lsp/p4/embedded/ProductionEmbeddedDiagnostics.java +++ b/tools/tinyexpression-p4-lsp-vscode/src/embedded/java/org/unlaxer/tinyexpression/lsp/p4/embedded/ProductionEmbeddedDiagnostics.java @@ -14,21 +14,23 @@ public final class ProductionEmbeddedDiagnostics implements EmbeddedLanguageDiag private final ProviderProcess provider = new ProviderProcess( List.of(Path.of(System.getProperty("java.home"), "bin", "java").toString(), "-cp", classpath, "org.unlaxer.dsl.provider.JavacProvider"), - new ProviderProtocol.Identity("javac", "21.0.9"), Set.of(Operation.VALIDATE), Duration.ofSeconds(30)); + new ProviderProtocol.Identity("javac", "21.0.9"), Set.of(Operation.VALIDATE, Operation.COMPLETION), Duration.ofSeconds(30)); @Override public List analyze(String uri, int version, String fullContent) { if (!fullContent.lines().anyMatch(line -> line.stripTrailing().equals("formula:"))) return List.of(); var host = new DocumentSnapshot(uri, version, fullContent); - var binding = TinyProductionBridge.parse(host); - if (binding.regions().stream().anyMatch(region -> region.parseState() == org.unlaxer.source.LanguageRegions.State.FAILED)) - throw new IllegalArgumentException("incomplete or invalid production region; no speculative Java children retained"); + var binding = TinyProductionBridge.parseEditor(host); var project = new LanguageQueries.Project("tiny-lsp", version, Map.of(uri, host), Map.of("classpath", classpath)); var queries = binding.queries(project, provider); var diagnostics = new ArrayList(); + for (var region : binding.regions()) if (region.parseState() == org.unlaxer.source.LanguageRegions.State.FAILED) + diagnostics.add(unavailable(host, region.body().start(), "Invalid production region: " + region.id())); for (var result : queries.diagnosticsAll(host, project, Map.of())) { if (!binding.javaFiles().containsKey(result.region())) continue; - if (result.state() != org.unlaxer.source.LanguageRegions.State.COMPLETE && result.state() != org.unlaxer.source.LanguageRegions.State.PARTIAL) - throw new IllegalArgumentException(result.region() + ": " + result.state()); + if (result.state() != org.unlaxer.source.LanguageRegions.State.COMPLETE && result.state() != org.unlaxer.source.LanguageRegions.State.PARTIAL) { + diagnostics.add(unavailable(host, 0, result.region() + ": " + result.state())); + continue; + } for (var item : result.diagnostics()) { for (var mapping : item.locations()) { var location = mapping.location(); @@ -44,6 +46,41 @@ public final class ProductionEmbeddedDiagnostics implements EmbeddedLanguageDiag } return diagnostics; } + @Override public Optional> complete(String uri, int version, String fullContent, Position position) { + if (!fullContent.lines().anyMatch(line -> line.stripTrailing().equals("formula:"))) return Optional.empty(); + var host = new DocumentSnapshot(uri, version, fullContent); + int cursor=host.fromLsp(new DocumentSnapshot.Position(position.getLine(),position.getCharacter())); + var binding=TinyProductionBridge.parseEditor(host);var owner=binding.tree().at(cursor); + if (owner==null || !owner.language().equals(TinyProductionBridge.JAVA)) return Optional.empty(); + int end=host.utf16(cursor),start=end; + while(start>0 && Character.isJavaIdentifierPart(fullContent.codePointBefore(start))) start-=Character.charCount(fullContent.codePointBefore(start)); + var project=new LanguageQueries.Project("tiny-lsp",version,Map.of(uri,host),Map.of("classpath",classpath)); + var result=binding.queries(project,provider).query(host,project,cursor,Operation.COMPLETION,Map.of("prefix",fullContent.substring(start,end))); + if(result.state()!=org.unlaxer.source.LanguageRegions.State.COMPLETE && result.state()!=org.unlaxer.source.LanguageRegions.State.PARTIAL) + throw new IllegalArgumentException(result.state().name()); + var items=new ArrayList(); + for(var candidate:result.items()) { + var item=new CompletionItem(candidate.label());item.setDetail(candidate.detail()); + item.setData(Map.of("region",result.region(),"version",Integer.toString(version),"state",result.state().name())); + var edits=new ArrayList(); + for(var edit:candidate.edits()) edits.add(new TextEdit(range(host,edit.span()),edit.replacement())); + if(edits.isEmpty()) item.setInsertText(""); + else { + item.setTextEdit(org.eclipse.lsp4j.jsonrpc.messages.Either.forLeft(edits.get(0))); + if(edits.size()>1)item.setAdditionalTextEdits(edits.subList(1,edits.size())); + } + items.add(item); + } + return Optional.of(items); + } + private static Range range(DocumentSnapshot host,DocumentSnapshot.Span span) { + var start=host.lsp(span.start());var end=host.lsp(span.end()); + return new Range(new Position(start.line(),start.character()),new Position(end.line(),end.character())); + } + private static Diagnostic unavailable(DocumentSnapshot host,int point,String message) { + var diagnostic=new Diagnostic(range(host,new DocumentSnapshot.Span(point,point)),message,DiagnosticSeverity.Warning,"tinyexpression-embedded"); + diagnostic.setCode("EMBEDDED_UNAVAILABLE");return diagnostic; + } private static DiagnosticSeverity severity(String value) { return switch(value) { case "ERROR" -> DiagnosticSeverity.Error; diff --git a/tools/tinyexpression-p4-lsp-vscode/src/embedded/java/org/unlaxer/tinyexpression/lsp/p4/embedded/TinyProductionBridge.java b/tools/tinyexpression-p4-lsp-vscode/src/embedded/java/org/unlaxer/tinyexpression/lsp/p4/embedded/TinyProductionBridge.java index aadcab53..8aee2ad7 100644 --- a/tools/tinyexpression-p4-lsp-vscode/src/embedded/java/org/unlaxer/tinyexpression/lsp/p4/embedded/TinyProductionBridge.java +++ b/tools/tinyexpression-p4-lsp-vscode/src/embedded/java/org/unlaxer/tinyexpression/lsp/p4/embedded/TinyProductionBridge.java @@ -1,6 +1,6 @@ package org.unlaxer.tinyexpression.lsp.p4.embedded; -// Port of unlaxer-parser PR #470, d247f34a; shared Java/Rust production fixture contract. +// Port of unlaxer-parser production bridge f2fc6faa with explicit editor recovery. import java.util.*; import org.unlaxer.*; import org.unlaxer.context.ParseContext; @@ -16,9 +16,10 @@ public final class TinyProductionBridge { public static final Language FORMULA = new Language("formulainfo", "tinyexpression", "f86ce8a5ab0ab7d23fdba2cd477187df8aa7607c", "FormulaInfo", "Document"); public static final Language TINY = new Language("tinyexpression", "tinyexpression", "f86ce8a5ab0ab7d23fdba2cd477187df8aa7607c", "TinyExpressionP4", "Formula"); public static final Language JAVA = new Language("java", "lang/java", "0.1.0", "Java21", "CompilationUnit"); - public record Binding(DocumentSnapshot host, List regions, Map javaFiles) { - public Binding { regions = List.copyOf(regions); javaFiles = Map.copyOf(javaFiles); } - public LanguageRegions tree() { return new LanguageRegions(host, regions, Set.of()); } + public record Binding(DocumentSnapshot host, List regions, Map javaFiles, Set openEnds) { + public Binding { regions = List.copyOf(regions); javaFiles = Map.copyOf(javaFiles); openEnds = Set.copyOf(openEnds); } + public Binding(DocumentSnapshot host, List regions, Map javaFiles) { this(host,regions,javaFiles,Set.of()); } + public LanguageRegions tree() { return new LanguageRegions(host, regions, openEnds); } public LanguageQueries queries(LanguageQueries.Project project, LanguageQueries.Provider javaProvider) { return new LanguageQueries(tree(), project, Map.of(JAVA, new LanguageQueries.Provider() { public Set capabilities() { return javaProvider.capabilities(); } @@ -35,7 +36,10 @@ public LanguageQueries.Request javaRequest(LanguageQueries.Request request) { return new LanguageQueries.Request(request.region(), request.operation(), request.cursor(), request.project(), parameters); } } - public static Binding parse(DocumentSnapshot host) { + public static Binding parse(DocumentSnapshot host) { return parse(host, false); } + /** Explicit editor recovery retains only CodeBlock prefixes certified by the production entry parser. */ + public static Binding parseEditor(DocumentSnapshot host) { return parse(host, true); } + private static Binding parse(DocumentSnapshot host, boolean editor) { if (host.length() > 1_048_576) throw new IllegalArgumentException("production document limit"); Parsed parsed; try (var context = new ParseContext(StringSource.createRootSource(host.text()))) { @@ -43,7 +47,7 @@ public static Binding parse(DocumentSnapshot host) { } if (!parsed.isSucceeded() || parsed.getConsumed() == null || !parsed.getConsumed().source.sourceAsString().equals(host.text())) throw new IllegalArgumentException("invalid FormulaInfo document"); - var regions = new ArrayList(); var files = new LinkedHashMap(); + var regions = new ArrayList(); var files = new LinkedHashMap(); var openEnds = new HashSet(); regions.add(region(host, "root", null, FORMULA, new Span(0,host.length()), new Span(0,host.length()), State.COMPLETE)); int index = 0; for (var block : parsed.getRootToken().typed(FormulaInfoBlocksParser.class).getChildrenWithParserAsListTyped(FormulaInfoBlockParser.class)) { @@ -71,7 +75,14 @@ public static Binding parse(DocumentSnapshot host) { String input = maskComments(raw); var p4 = TinyExpressionP4Parser.parse(input, new ParseOptions(true,true,true,true,org.unlaxer.tinyexpression.p4.ubnfc.P4Scanners.ALL,512)); regions.add(region(host,id,"root",TINY,body,body,p4.ok() ? State.COMPLETE : State.FAILED)); - if (!p4.ok()) continue; // Never retain children from speculative/failed parses. + if (!p4.ok()) { + if (editor) { + int parentIndex = regions.size()-1; + recoverPrefix(host, id, start, input, regions, files, openEnds); + if (regions.size() > parentIndex+1) regions.set(parentIndex, region(host,id,"root",TINY,body,body,State.PARTIAL)); + } + continue; + } var tokens = p4.lexical().stream().filter(t -> t.token() != null && t.ruleId().equals("TinyExpressionP4::CodeBlock")).toList(); if (tokens.size() % 3 != 0) throw new IllegalArgumentException("P4 lexical contract changed"); for (int n=0;n regions, Map files, Set openEnds) { + int from = 0, number = 0; + var options = new ParseOptions(false,false,true,true,org.unlaxer.tinyexpression.p4.ubnfc.P4Scanners.ALL,512); + while (from < input.length()) { + if (number >= 256) throw new IllegalArgumentException("production code block limit"); + String remaining = input.substring(from), recognized = remaining; + var parsed = TinyExpressionP4Parser.parseEntry("TinyExpressionP4","CodeBlock",recognized,options); + boolean partial = !parsed.ok(); + if (partial) { + recognized = remaining + "\n```\n"; + parsed = TinyExpressionP4Parser.parseEntry("TinyExpressionP4","CodeBlock",recognized,options); + } + if (!parsed.ok()) break; + var tokens = parsed.lexical().stream().filter(t -> t.token()!=null && t.ruleId().equals("TinyExpressionP4::CodeBlock")).toList(); + if (tokens.size()!=3) throw new IllegalArgumentException("P4 CodeBlock lexical contract changed"); + var open=tokens.get(0);var close=tokens.get(2); + int size=remaining.codePointCount(0,remaining.length()); + if (partial && (close.span().start()size)) break; + int consumed=partial ? size : close.span().end(); + if (consumed<=0 || consumed>size) break; + String header=recognized.substring(recognized.offsetByCodePoints(0,open.span().start()),recognized.offsetByCodePoints(0,open.span().end())).strip(); + number++; + if (header.startsWith("```java:")) { + int offset=start+input.codePointCount(0,from); + String child=parent+"/java/"+number; + Span full=new Span(offset+open.span().start(),offset+consumed); + Span body=new Span(offset+open.span().end(),offset+(partial?size:close.span().start())); + regions.add(region(host,child,parent,JAVA,full,body,partial?State.PARTIAL:State.COMPLETE)); + files.put(child,header.substring(Math.max(8,header.lastIndexOf('.')+1))+".java"); + if (partial) openEnds.add(child); + } + if (partial) break; + from += remaining.offsetByCodePoints(0,consumed); + } } private static String maskComments(String raw) { var out = new StringBuilder(); diff --git a/tools/tinyexpression-p4-lsp-vscode/src/main/java/org/unlaxer/tinyexpression/lsp/p4/EmbeddedLanguageDiagnostics.java b/tools/tinyexpression-p4-lsp-vscode/src/main/java/org/unlaxer/tinyexpression/lsp/p4/EmbeddedLanguageDiagnostics.java index 71fc8160..c4ec56b5 100644 --- a/tools/tinyexpression-p4-lsp-vscode/src/main/java/org/unlaxer/tinyexpression/lsp/p4/EmbeddedLanguageDiagnostics.java +++ b/tools/tinyexpression-p4-lsp-vscode/src/main/java/org/unlaxer/tinyexpression/lsp/p4/EmbeddedLanguageDiagnostics.java @@ -6,4 +6,9 @@ /** Optional analysis of the untouched full document; implementations must never evaluate user code. */ public interface EmbeddedLanguageDiagnostics { List analyze(String uri, int version, String fullContent); + /** Empty optional means no owned embedded region; a present empty list is an owned region without candidates. */ + default java.util.Optional> complete( + String uri, int version, String fullContent, org.eclipse.lsp4j.Position position) { + return java.util.Optional.empty(); + } } diff --git a/tools/tinyexpression-p4-lsp-vscode/src/main/java/org/unlaxer/tinyexpression/lsp/p4/TinyExpressionP4LanguageServerExt.java b/tools/tinyexpression-p4-lsp-vscode/src/main/java/org/unlaxer/tinyexpression/lsp/p4/TinyExpressionP4LanguageServerExt.java index ebcf9b5c..258a135c 100644 --- a/tools/tinyexpression-p4-lsp-vscode/src/main/java/org/unlaxer/tinyexpression/lsp/p4/TinyExpressionP4LanguageServerExt.java +++ b/tools/tinyexpression-p4-lsp-vscode/src/main/java/org/unlaxer/tinyexpression/lsp/p4/TinyExpressionP4LanguageServerExt.java @@ -1636,6 +1636,21 @@ public void didSave(DidSaveTextDocumentParams params) { public CompletableFuture, CompletionList>> completion( CompletionParams params) { + synchronized (server) { + String queryUri = params.getTextDocument().getUri(); + var queryState = server.extDocuments.get(queryUri); + if (server.embeddedDiagnostics != null && queryState != null) { + try { + var embedded = server.embeddedDiagnostics.complete(queryUri, + server.embeddedVersions.getOrDefault(queryUri, 0), queryState.fullContent(), params.getPosition()); + if (embedded.isPresent()) return CompletableFuture.completedFuture(Either.forLeft(embedded.get())); + } catch (RuntimeException failure) { + if (server.extClient != null) server.extClient.logMessage(new org.eclipse.lsp4j.MessageParams(org.eclipse.lsp4j.MessageType.Warning, + "Embedded completion unavailable: " + failure.getMessage())); + return CompletableFuture.completedFuture(Either.forLeft(List.of())); + } + } + } List items = new ArrayList<>(); String uri = params.getTextDocument().getUri(); ExtDocumentState state = server.extDocuments.get(uri); From 5a211f7c4b956ba65b0a6f25d19f4c8d36eafc47 Mon Sep 17 00:00:00 2001 From: opaopa6969 Date: Fri, 9 Oct 2026 14:16:04 +0900 Subject: [PATCH 3/5] Support current long code fences in embedded editor regions --- .../EMBEDDED-LANGUAGES.md | 6 +- .../p4/ProductionEmbeddedDiagnosticsTest.java | 13 ++++ .../lsp/p4/embedded/TinyProductionBridge.java | 71 ++++++++++++++++--- 3 files changed, 77 insertions(+), 13 deletions(-) diff --git a/tools/tinyexpression-p4-lsp-vscode/EMBEDDED-LANGUAGES.md b/tools/tinyexpression-p4-lsp-vscode/EMBEDDED-LANGUAGES.md index 17bae1ca..6b7a0a4d 100644 --- a/tools/tinyexpression-p4-lsp-vscode/EMBEDDED-LANGUAGES.md +++ b/tools/tinyexpression-p4-lsp-vscode/EMBEDDED-LANGUAGES.md @@ -12,10 +12,12 @@ Point `tinyExpressionP4Lsp.server.jarPath` at the resulting server jar, use JDK The existing `TinyExpressionP4LanguageServerExt` now sends untouched, versioned full documents to an optional diagnostics provider. The production FormulaInfo and P4 parsers construct all section regions; javac diagnostics from each Java body are mapped to original UTF-16 positions. Updating later sections does not reuse first-section cache results. Stale updates are rejected, save revalidates, and close clears diagnostics. Invalid or incomplete region extraction and unavailable providers emit `EMBEDDED_UNAVAILABLE`, not empty success. Inside an owned Java region, completion now uses the same production editor binding and returns source-mapped javac edits, including at unfinished host EOF. Outside Java, existing first-section Tiny completion, formatting and other methods retain their prior behavior. -The bridge is a namespaced port of unlaxer-parser production editor bridge commit f2fc6faa (the strict bridge originated in PR #470). Its Java/Rust contract is tested there using the same production fixture, independent code-point/UTF-16 coordinates and rejection cases. This repository's existing LSP consumer is Java; no Rust LSP consumer is claimed here. Foreign dependency diagnostics need a later consumer extension. Workspace ownership is rejected explicitly by this single-document adapter. +The bridge is a namespaced port of unlaxer-parser production editor bridge commit f2fc6faa (the strict bridge originated in PR #470), adapted to the current Tiny grammar revision 0d84f0dc. Its Java/Rust contract is tested there using the same production fixture, independent code-point/UTF-16 coordinates and rejection cases. This repository's existing LSP consumer is Java; no Rust LSP consumer is claimed here. Foreign dependency diagnostics need a later consumer extension. Workspace ownership is rejected explicitly by this single-document adapter. -Run `mvn test -Pembedded-languages -Dtest=ProductionEmbeddedDiagnosticsTest,TinyExpressionP4LanguageServerExtTest,TinyExpressionP4DebugAdapterExtTest` here. The integration test runs real javac on production fixture 69 (ten formulas, two Java bodies), checks both error positions, a second-section-only update, Unicode/CRLF, stale updates, save, close and unavailable extraction. Run the 39 existing LSP/DAP tests without the profile as a separate clean build to verify the default dependency path. Use an isolated Maven repository if other checkouts install Tiny artifacts with different generated AST versions. +Run `mvn test -Pembedded-languages -Dtest=ProductionEmbeddedDiagnosticsTest,TinyExpressionP4LanguageServerExtTest,TinyExpressionP4DebugAdapterExtTest` here. The integration test runs real javac on production fixture 69 (ten formulas, two Java bodies), checks both error positions, a second-section-only update, Unicode/CRLF, stale updates, save, close and unavailable extraction. Run the 40 existing LSP/DAP tests without the profile as a separate clean build to verify the default dependency path. Use an isolated Maven repository if other checkouts install Tiny artifacts with different generated AST versions. The explicit editor entry certifies leading CodeBlock prefixes using the real production parser. Recognition-only synthetic closing text never enters the snapshot or edits. Incomplete Java diagnostics and EOF completion stay inside the original body; valid siblings remain usable when another formula fails. The strict parser is unchanged and still reports unfinished formulas as FAILED. The packaged stdio smoke additionally verifies the Unicode/CRLF unfinished-body completion edit at line 3, characters 41–44. The new completion method is optional on the dependency-neutral SPI. The default build does not load a provider or alter its Tiny completion behavior. Both diagnostics and completion use the same immutable full snapshot under the existing server's lifecycle lock, so an intervening update cannot relabel an old result with a new version. + +Current Tiny grammar support includes the retained-source long CodeBlock form: exact-width standalone fences keep shorter triple backticks opaque inside Java text. The adapter uses the production LongCodeFence layout after the production parser certifies the block. FormulaInfo hash-comment masking never rewrites an opaque Java body. The current Tiny grammar revision is carried in FormulaInfo/Tiny region identities; the Java profile identity remains `lang/java@0.1.0`. diff --git a/tools/tinyexpression-p4-lsp-vscode/src/embedded-test/java/org/unlaxer/tinyexpression/lsp/p4/ProductionEmbeddedDiagnosticsTest.java b/tools/tinyexpression-p4-lsp-vscode/src/embedded-test/java/org/unlaxer/tinyexpression/lsp/p4/ProductionEmbeddedDiagnosticsTest.java index aef9bea1..1ad6653d 100644 --- a/tools/tinyexpression-p4-lsp-vscode/src/embedded-test/java/org/unlaxer/tinyexpression/lsp/p4/ProductionEmbeddedDiagnosticsTest.java +++ b/tools/tinyexpression-p4-lsp-vscode/src/embedded-test/java/org/unlaxer/tinyexpression/lsp/p4/ProductionEmbeddedDiagnosticsTest.java @@ -62,6 +62,19 @@ public void logMessage(MessageParams value) {} assertTrue(org.unlaxer.tinyexpression.lsp.p4.embedded.TinyProductionBridge.parse(snapshot).regions().stream().anyMatch(region->region.parseState()==org.unlaxer.source.LanguageRegions.State.FAILED)); service.didChange(new DidChangeTextDocumentParams(new VersionedTextDocumentIdentifier(uri,7),List.of(new TextDocumentContentChangeEvent(snapshot.text())))); assertEquals(Integer.valueOf(7),last(notifications).getVersion()); + String longSource="formula:\n````java:Main\nclass Main { String marker=\"```\"; int f(){return \"bad\";}}\n````\n1\n---END_OF_PART---\n"; + service.didChange(new DidChangeTextDocumentParams(new VersionedTextDocumentIdentifier(uri,100),List.of(new TextDocumentContentChangeEvent(longSource)))); + assertEquals(last(notifications).toString(),1,javaDiagnostics(last(notifications)).size()); + assertEquals("compiler.err.prob.found.req",javaDiagnostics(last(notifications)).get(0).getCode().getLeft()); + assertEquals(new Range(new Position(2,49),new Position(2,54)),javaDiagnostics(last(notifications)).get(0).getRange()); + String longPartial="formula:\n````java:Main\nclass Main { int target; int f(){ return tar"; + service.didChange(new DidChangeTextDocumentParams(new VersionedTextDocumentIdentifier(uri,101),List.of(new TextDocumentContentChangeEvent(longPartial)))); + var longItems=service.completion(new CompletionParams(new TextDocumentIdentifier(uri),new Position(2,44))).join().getLeft(); + assertEquals("target",longItems.stream().filter(item->item.getLabel().equals("target")).findFirst().orElseThrow().getTextEdit().getLeft().getNewText()); + var forged=new org.unlaxer.source.DocumentSnapshot(uri,200,"formula:\n```java:Main\n# ```\nclass Main{}\n```\n1\n---END_OF_PART---\n"); + var rejected=org.unlaxer.tinyexpression.lsp.p4.embedded.TinyProductionBridge.parse(forged); + assertTrue(rejected.javaFiles().isEmpty()); + assertTrue(rejected.regions().stream().anyMatch(region->region.parseState()==org.unlaxer.source.LanguageRegions.State.FAILED)); service.didClose(new DidCloseTextDocumentParams(new TextDocumentIdentifier(uri)));assertTrue(last(notifications).getDiagnostics().isEmpty()); } private static PublishDiagnosticsParams last(List values) { return values.get(values.size()-1); } diff --git a/tools/tinyexpression-p4-lsp-vscode/src/embedded/java/org/unlaxer/tinyexpression/lsp/p4/embedded/TinyProductionBridge.java b/tools/tinyexpression-p4-lsp-vscode/src/embedded/java/org/unlaxer/tinyexpression/lsp/p4/embedded/TinyProductionBridge.java index 8aee2ad7..c20eb3c7 100644 --- a/tools/tinyexpression-p4-lsp-vscode/src/embedded/java/org/unlaxer/tinyexpression/lsp/p4/embedded/TinyProductionBridge.java +++ b/tools/tinyexpression-p4-lsp-vscode/src/embedded/java/org/unlaxer/tinyexpression/lsp/p4/embedded/TinyProductionBridge.java @@ -13,8 +13,8 @@ /** Explicit native integration: production TinyExpression parsers, no evaluation or compilation. */ public final class TinyProductionBridge { - public static final Language FORMULA = new Language("formulainfo", "tinyexpression", "f86ce8a5ab0ab7d23fdba2cd477187df8aa7607c", "FormulaInfo", "Document"); - public static final Language TINY = new Language("tinyexpression", "tinyexpression", "f86ce8a5ab0ab7d23fdba2cd477187df8aa7607c", "TinyExpressionP4", "Formula"); + public static final Language FORMULA = new Language("formulainfo", "tinyexpression", "0d84f0dc5c0331c09f46350cceb21cb57ee25d79", "FormulaInfo", "Document"); + public static final Language TINY = new Language("tinyexpression", "tinyexpression", "0d84f0dc5c0331c09f46350cceb21cb57ee25d79", "TinyExpressionP4", "Formula"); public static final Language JAVA = new Language("java", "lang/java", "0.1.0", "Java21", "CompilationUnit"); public record Binding(DocumentSnapshot host, List regions, Map javaFiles, Set openEnds) { public Binding { regions = List.copyOf(regions); javaFiles = Map.copyOf(javaFiles); openEnds = Set.copyOf(openEnds); } @@ -84,13 +84,27 @@ private static Binding parse(DocumentSnapshot host, boolean editor) { continue; } var tokens = p4.lexical().stream().filter(t -> t.token() != null && t.ruleId().equals("TinyExpressionP4::CodeBlock")).toList(); - if (tokens.size() % 3 != 0) throw new IllegalArgumentException("P4 lexical contract changed"); - for (int n=0;n=tokens.size())throw new IllegalArgumentException("P4 CodeBlock lexical contract changed"); + var close=tokens.get(n+2);n+=3; + String header=input.substring(at,input.offsetByCodePoints(0,open.span().end())).strip(); if (!header.startsWith("```java:")) continue; String file=header.substring(Math.max(8,header.lastIndexOf('.')+1))+".java"; - String child=id+"/java/"+(n/3+1); + String child=id+"/java/"+(number+1); Span full=new Span(start+open.span().start(),start+close.span().end()); Span javaBody=new Span(start+open.span().end(),start+close.span().start()); regions.add(region(host,child,id,JAVA,full,javaBody,State.COMPLETE)); files.put(child,file); @@ -109,11 +123,40 @@ private static void recoverPrefix(DocumentSnapshot host, String parent, int star var parsed = TinyExpressionP4Parser.parseEntry("TinyExpressionP4","CodeBlock",recognized,options); boolean partial = !parsed.ok(); if (partial) { - recognized = remaining + "\n```\n"; - parsed = TinyExpressionP4Parser.parseEntry("TinyExpressionP4","CodeBlock",recognized,options); + var widths=new LinkedHashSet();widths.add(3); + for(String line:remaining.split("[\\r\\n]",-1)) { + int width=0;while(width=4)widths.add(width); + if(widths.size()>=256)break; + } + for(int width:widths) { + recognized = remaining + "\n"+"`".repeat(width)+"\n"; + parsed = TinyExpressionP4Parser.parseEntry("TinyExpressionP4","CodeBlock",recognized,options); + if(parsed.ok())break; + } } if (!parsed.ok()) break; var tokens = parsed.lexical().stream().filter(t -> t.token()!=null && t.ruleId().equals("TinyExpressionP4::CodeBlock")).toList(); + if(tokens.size()==1) { + var token=tokens.get(0);int at=recognized.offsetByCodePoints(0,token.span().start()); + var layout=org.unlaxer.tinyexpression.codeblock.LongCodeFence.scan(recognized,at); + if(layout==null)throw new IllegalArgumentException("P4 long CodeBlock lexical contract changed"); + int size=remaining.codePointCount(0,remaining.length()); + int bodyStart=recognized.codePointCount(0,layout.bodyStart()),bodyEnd=recognized.codePointCount(0,layout.bodyEnd()); + if(partial && (bodyEndsize))break; + int consumed=partial?size:token.span().end(); + if(consumed<=0 || consumed>size)break; + String header=recognized.substring(at+layout.width(),layout.headerEnd());number++; + if(header.startsWith("java:")) { + int offset=start+input.codePointCount(0,from);String child=parent+"/java/"+number; + regions.add(region(host,child,parent,JAVA,new Span(offset+token.span().start(),offset+consumed), + new Span(offset+bodyStart,offset+(partial?size:bodyEnd)),partial?State.PARTIAL:State.COMPLETE)); + String className=header.substring(5);files.put(child,className.substring(className.lastIndexOf('.')+1)+".java"); + if(partial)openEnds.add(child); + } + if(partial)break; + from+=remaining.offsetByCodePoints(0,consumed);continue; + } if (tokens.size()!=3) throw new IllegalArgumentException("P4 CodeBlock lexical contract changed"); var open=tokens.get(0);var close=tokens.get(2); int size=remaining.codePointCount(0,remaining.length()); @@ -136,9 +179,15 @@ private static void recoverPrefix(DocumentSnapshot host, String parent, int star } } private static String maskComments(String raw) { - var out = new StringBuilder(); + var out = new StringBuilder();int fence=0; for (String line : raw.split("(?<=\n)",-1)) { - if (line.stripLeading().startsWith("#")) line.codePoints().forEach(c -> out.appendCodePoint(c=='\r'||c=='\n'?c:' ')); + if(fence>0) { + out.append(line); + if(fence==3 ? line.contains("```") : line.replaceFirst("\\r?\\n$", "").equals("`".repeat(fence)))fence=0; + } else if(line.startsWith("```")) { + while(fence out.appendCodePoint(c=='\r'||c=='\n'?c:' ')); else out.append(line); } return out.toString(); From bf4f409bc0200bd9fbfc80b5b5ddd3a248179d62 Mon Sep 17 00:00:00 2001 From: opaopa6969 Date: Fri, 9 Oct 2026 14:16:04 +0900 Subject: [PATCH 4/5] Adapt embedded diagnostics to current retained-source fences --- .../EMBEDDED-LANGUAGES.md | 6 ++- .../p4/ProductionEmbeddedDiagnosticsTest.java | 9 +++++ .../lsp/p4/embedded/TinyProductionBridge.java | 38 ++++++++++++++----- 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/tools/tinyexpression-p4-lsp-vscode/EMBEDDED-LANGUAGES.md b/tools/tinyexpression-p4-lsp-vscode/EMBEDDED-LANGUAGES.md index a376a623..287255a5 100644 --- a/tools/tinyexpression-p4-lsp-vscode/EMBEDDED-LANGUAGES.md +++ b/tools/tinyexpression-p4-lsp-vscode/EMBEDDED-LANGUAGES.md @@ -12,6 +12,8 @@ Point `tinyExpressionP4Lsp.server.jarPath` at the resulting server jar, use JDK The existing `TinyExpressionP4LanguageServerExt` now sends untouched, versioned full documents to an optional diagnostics provider. The production FormulaInfo and P4 parsers construct all section regions; javac diagnostics from each Java body are mapped to original UTF-16 positions. Updating later sections does not reuse first-section cache results. Stale updates are rejected, save revalidates, and close clears diagnostics. Invalid or incomplete region extraction and unavailable providers emit `EMBEDDED_UNAVAILABLE`, not empty success. Existing first-section Tiny completion, formatting and other methods retain their prior behavior; this profile adds Java diagnostics only. -The bridge is a namespaced port of unlaxer-parser PR #470, commit d247f34a. Its Java/Rust contract is tested there using the same production fixture, independent code-point/UTF-16 coordinates and rejection cases. This repository's existing LSP consumer is Java; no Rust LSP consumer is claimed here. Unclosed Java region completion and foreign dependency diagnostics need a later consumer extension. Workspace ownership is rejected explicitly by this single-document adapter. +The bridge is a namespaced port of unlaxer-parser PR #470, commit d247f34a, adapted to the current Tiny grammar revision 0d84f0dc. Its Java/Rust contract is tested there using the same production fixture, independent code-point/UTF-16 coordinates and rejection cases. This repository's existing LSP consumer is Java; no Rust LSP consumer is claimed here. Unclosed Java region completion and foreign dependency diagnostics need a later consumer extension. Workspace ownership is rejected explicitly by this single-document adapter. -Run `mvn test -Pembedded-languages -Dtest=ProductionEmbeddedDiagnosticsTest,TinyExpressionP4LanguageServerExtTest,TinyExpressionP4DebugAdapterExtTest` here. The integration test runs real javac on production fixture 69 (ten formulas, two Java bodies), checks both error positions, a second-section-only update, Unicode/CRLF, stale updates, save, close and unavailable extraction. Run the 39 existing LSP/DAP tests without the profile as a separate clean build to verify the default dependency path. Use an isolated Maven repository if other checkouts install Tiny artifacts with different generated AST versions. +Run `mvn test -Pembedded-languages -Dtest=ProductionEmbeddedDiagnosticsTest,TinyExpressionP4LanguageServerExtTest,TinyExpressionP4DebugAdapterExtTest` here. The integration test runs real javac on production fixture 69 (ten formulas, two Java bodies), checks both error positions, a second-section-only update, Unicode/CRLF, stale updates, save, close and unavailable extraction. Run the 40 existing LSP/DAP tests without the profile as a separate clean build to verify the default dependency path. Use an isolated Maven repository if other checkouts install Tiny artifacts with different generated AST versions. + +Current Tiny grammar support includes the retained-source long CodeBlock form: exact-width standalone fences keep shorter triple backticks opaque inside Java text. The adapter uses the production LongCodeFence layout after the production parser certifies the block. FormulaInfo hash-comment masking never rewrites an opaque Java body. The current Tiny grammar revision is carried in FormulaInfo/Tiny region identities; the Java profile identity remains `lang/java@0.1.0`. diff --git a/tools/tinyexpression-p4-lsp-vscode/src/embedded-test/java/org/unlaxer/tinyexpression/lsp/p4/ProductionEmbeddedDiagnosticsTest.java b/tools/tinyexpression-p4-lsp-vscode/src/embedded-test/java/org/unlaxer/tinyexpression/lsp/p4/ProductionEmbeddedDiagnosticsTest.java index 6f5ebb24..c5c7977d 100644 --- a/tools/tinyexpression-p4-lsp-vscode/src/embedded-test/java/org/unlaxer/tinyexpression/lsp/p4/ProductionEmbeddedDiagnosticsTest.java +++ b/tools/tinyexpression-p4-lsp-vscode/src/embedded-test/java/org/unlaxer/tinyexpression/lsp/p4/ProductionEmbeddedDiagnosticsTest.java @@ -51,6 +51,15 @@ public void logMessage(MessageParams value) {} assertTrue(last(notifications).toString(),javaDiagnostics(last(notifications)).isEmpty()); service.didChange(new DidChangeTextDocumentParams(new VersionedTextDocumentIdentifier(uri,5),List.of(new TextDocumentContentChangeEvent("formula:\ninvalid\n")))); assertTrue(last(notifications).getDiagnostics().stream().anyMatch(d->d.getCode()!=null && "EMBEDDED_UNAVAILABLE".equals(d.getCode().getLeft()))); + String longSource="formula:\n````java:Main\nclass Main { String marker=\"```\"; int f(){return \"bad\";}}\n````\n1\n---END_OF_PART---\n"; + service.didChange(new DidChangeTextDocumentParams(new VersionedTextDocumentIdentifier(uri,100),List.of(new TextDocumentContentChangeEvent(longSource)))); + assertEquals(last(notifications).toString(),1,javaDiagnostics(last(notifications)).size()); + assertEquals("compiler.err.prob.found.req",javaDiagnostics(last(notifications)).get(0).getCode().getLeft()); + assertEquals(new Range(new Position(2,49),new Position(2,54)),javaDiagnostics(last(notifications)).get(0).getRange()); + var forged=new org.unlaxer.source.DocumentSnapshot(uri,200,"formula:\n```java:Main\n# ```\nclass Main{}\n```\n1\n---END_OF_PART---\n"); + var rejected=org.unlaxer.tinyexpression.lsp.p4.embedded.TinyProductionBridge.parse(forged); + assertTrue(rejected.javaFiles().isEmpty()); + assertTrue(rejected.regions().stream().anyMatch(region->region.parseState()==org.unlaxer.source.LanguageRegions.State.FAILED)); service.didClose(new DidCloseTextDocumentParams(new TextDocumentIdentifier(uri)));assertTrue(last(notifications).getDiagnostics().isEmpty()); } private static PublishDiagnosticsParams last(List values) { return values.get(values.size()-1); } diff --git a/tools/tinyexpression-p4-lsp-vscode/src/embedded/java/org/unlaxer/tinyexpression/lsp/p4/embedded/TinyProductionBridge.java b/tools/tinyexpression-p4-lsp-vscode/src/embedded/java/org/unlaxer/tinyexpression/lsp/p4/embedded/TinyProductionBridge.java index aadcab53..e2299b1e 100644 --- a/tools/tinyexpression-p4-lsp-vscode/src/embedded/java/org/unlaxer/tinyexpression/lsp/p4/embedded/TinyProductionBridge.java +++ b/tools/tinyexpression-p4-lsp-vscode/src/embedded/java/org/unlaxer/tinyexpression/lsp/p4/embedded/TinyProductionBridge.java @@ -13,8 +13,8 @@ /** Explicit native integration: production TinyExpression parsers, no evaluation or compilation. */ public final class TinyProductionBridge { - public static final Language FORMULA = new Language("formulainfo", "tinyexpression", "f86ce8a5ab0ab7d23fdba2cd477187df8aa7607c", "FormulaInfo", "Document"); - public static final Language TINY = new Language("tinyexpression", "tinyexpression", "f86ce8a5ab0ab7d23fdba2cd477187df8aa7607c", "TinyExpressionP4", "Formula"); + public static final Language FORMULA = new Language("formulainfo", "tinyexpression", "0d84f0dc5c0331c09f46350cceb21cb57ee25d79", "FormulaInfo", "Document"); + public static final Language TINY = new Language("tinyexpression", "tinyexpression", "0d84f0dc5c0331c09f46350cceb21cb57ee25d79", "TinyExpressionP4", "Formula"); public static final Language JAVA = new Language("java", "lang/java", "0.1.0", "Java21", "CompilationUnit"); public record Binding(DocumentSnapshot host, List regions, Map javaFiles) { public Binding { regions = List.copyOf(regions); javaFiles = Map.copyOf(javaFiles); } @@ -73,13 +73,27 @@ public static Binding parse(DocumentSnapshot host) { regions.add(region(host,id,"root",TINY,body,body,p4.ok() ? State.COMPLETE : State.FAILED)); if (!p4.ok()) continue; // Never retain children from speculative/failed parses. var tokens = p4.lexical().stream().filter(t -> t.token() != null && t.ruleId().equals("TinyExpressionP4::CodeBlock")).toList(); - if (tokens.size() % 3 != 0) throw new IllegalArgumentException("P4 lexical contract changed"); - for (int n=0;n=tokens.size())throw new IllegalArgumentException("P4 CodeBlock lexical contract changed"); + var close=tokens.get(n+2);n+=3; + String header=input.substring(at,input.offsetByCodePoints(0,open.span().end())).strip(); if (!header.startsWith("```java:")) continue; String file=header.substring(Math.max(8,header.lastIndexOf('.')+1))+".java"; - String child=id+"/java/"+(n/3+1); + String child=id+"/java/"+(number+1); Span full=new Span(start+open.span().start(),start+close.span().end()); Span javaBody=new Span(start+open.span().end(),start+close.span().start()); regions.add(region(host,child,id,JAVA,full,javaBody,State.COMPLETE)); files.put(child,file); @@ -89,9 +103,15 @@ public static Binding parse(DocumentSnapshot host) { var result = new Binding(host,regions,files); result.tree(); return result; } private static String maskComments(String raw) { - var out = new StringBuilder(); + var out = new StringBuilder();int fence=0; for (String line : raw.split("(?<=\n)",-1)) { - if (line.stripLeading().startsWith("#")) line.codePoints().forEach(c -> out.appendCodePoint(c=='\r'||c=='\n'?c:' ')); + if(fence>0) { + out.append(line); + if(fence==3 ? line.contains("```") : line.replaceFirst("\\r?\\n$", "").equals("`".repeat(fence)))fence=0; + } else if(line.startsWith("```")) { + while(fence out.appendCodePoint(c=='\r'||c=='\n'?c:' ')); else out.append(line); } return out.toString(); From 736eb65fbc182f2e7115eecac362eb344218d4c8 Mon Sep 17 00:00:00 2001 From: opaopa6969 Date: Fri, 9 Oct 2026 14:28:46 +0900 Subject: [PATCH 5/5] Clarify fixed bridge parity and current Java LSP consumer validation scopes --- README.en.md | 2 ++ README.md | 2 ++ .../tinyexpression-p4-lsp-vscode/EMBEDDED-LANGUAGES.md | 10 +++++++++- tools/tinyexpression-p4-lsp-vscode/README.md | 2 ++ 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/README.en.md b/README.en.md index 13c82609..0a37c2fd 100644 --- a/README.en.md +++ b/README.en.md @@ -299,6 +299,8 @@ Full specification: [docs/language-guide.md](docs/language-guide.md) ## LSP / DAP +The existing **Java** LSP consumer offers [opt-in embedded Java diagnostics](tools/tinyexpression-p4-lsp-vscode/EMBEDDED-LANGUAGES.md). Its current Tiny grammar adaptation and the fixed-revision Java/Rust bridge fixtures have separate validation scopes; they do not establish a Rust Tiny LSP consumer. + The [tinyexpression-p4-lsp-vscode](tools/tinyexpression-p4-lsp-vscode/README.md) VS Code extension provides: - Syntax highlighting and semantic tokens diff --git a/README.md b/README.md index 2aeb6a69..28cce85c 100644 --- a/README.md +++ b/README.md @@ -329,6 +329,8 @@ VS Code 拡張 [tinyexpression-p4-lsp-vscode](tools/tinyexpression-p4-lsp-vscode - 宣言・import・method・`.tecatalog` を使う補完とホバー - DAP デバッグ(6 バックエンドのパリティ比較) +既存の **Java** LSP consumer へ埋め込み Java 診断を接続する [opt-in 設定と検証範囲](tools/tinyexpression-p4-lsp-vscode/EMBEDDED-LANGUAGES.md) も提供します。現行 Tiny grammar への接続と、固定版の Java / Rust 共通 bridge fixture は別の検証対象です。Rust Tiny LSP consumer の対応を示すものではありません。 + DAP 0.2.33 は生成AST上の停止・ブレークポイント・実ランタイム評価に加え、 人向けの期待値付き構文診断と、LLM/editor向けの構造化診断dataに対応します。 `launch.json` の `variables` は `CalculationContext` に型付きで注入され、選択バックエンド、 diff --git a/tools/tinyexpression-p4-lsp-vscode/EMBEDDED-LANGUAGES.md b/tools/tinyexpression-p4-lsp-vscode/EMBEDDED-LANGUAGES.md index 287255a5..bda598dd 100644 --- a/tools/tinyexpression-p4-lsp-vscode/EMBEDDED-LANGUAGES.md +++ b/tools/tinyexpression-p4-lsp-vscode/EMBEDDED-LANGUAGES.md @@ -12,7 +12,15 @@ Point `tinyExpressionP4Lsp.server.jarPath` at the resulting server jar, use JDK The existing `TinyExpressionP4LanguageServerExt` now sends untouched, versioned full documents to an optional diagnostics provider. The production FormulaInfo and P4 parsers construct all section regions; javac diagnostics from each Java body are mapped to original UTF-16 positions. Updating later sections does not reuse first-section cache results. Stale updates are rejected, save revalidates, and close clears diagnostics. Invalid or incomplete region extraction and unavailable providers emit `EMBEDDED_UNAVAILABLE`, not empty success. Existing first-section Tiny completion, formatting and other methods retain their prior behavior; this profile adds Java diagnostics only. -The bridge is a namespaced port of unlaxer-parser PR #470, commit d247f34a, adapted to the current Tiny grammar revision 0d84f0dc. Its Java/Rust contract is tested there using the same production fixture, independent code-point/UTF-16 coordinates and rejection cases. This repository's existing LSP consumer is Java; no Rust LSP consumer is claimed here. Unclosed Java region completion and foreign dependency diagnostics need a later consumer extension. Workspace ownership is rejected explicitly by this single-document adapter. +The bridge is a namespaced port of unlaxer-parser PR #470, commit d247f34a, adapted to the current Tiny grammar revision 0d84f0dc. The shared Java/Rust bridge contract is independently tested in that repository at the fixed Tiny revision `f86ce8a5ab0ab7d23fdba2cd477187df8aa7607c`; this current-consumer adaptation has a separate validation scope. This repository's existing LSP consumer is Java; no Rust LSP consumer is claimed here. Unclosed Java region completion and foreign dependency diagnostics need a later consumer extension. Workspace ownership is rejected explicitly by this single-document adapter. + +The validation targets are distinct: + +| Tiny source revision | Hosts / consumer | Independent evidence | +| --- | --- | --- | +| `f86ce8a5ab0ab7d23fdba2cd477187df8aa7607c` | Strict Java / Rust production bridge in unlaxer-parser [#470](https://github.com/opaopa6969/unlaxer-parser/pull/470) | 16 shared inputs / 177 independent observations per host, including original code-point / UTF-16 positions and query edits. | +| `f86ce8a5ab0ab7d23fdba2cd477187df8aa7607c` | Editor Java / Rust production bridge in unlaxer-parser [#479](https://github.com/opaopa6969/unlaxer-parser/pull/479) | Nine shared inputs / 29 independent observations per host; edited unfinished inputs still fail strict parsing. These fixed-grammar fixtures do not certify current long-fence consumer parity. | +| `0d84f0dc5c0331c09f46350cceb21cb57ee25d79` | Existing **Java** Tiny P4 LSP consumer, [#252](https://github.com/opaopa6969/tinyexpression/pull/252) / [#254](https://github.com/opaopa6969/tinyexpression/pull/254) | This PR adds opt-in javac diagnostics with current retained-source long fences and original UTF-16 positions; the paired editor PR adds EOF completion. No current Rust Tiny LSP consumer or current-long-fence Java/Rust protocol parity is implemented or verified by this profile. | Run `mvn test -Pembedded-languages -Dtest=ProductionEmbeddedDiagnosticsTest,TinyExpressionP4LanguageServerExtTest,TinyExpressionP4DebugAdapterExtTest` here. The integration test runs real javac on production fixture 69 (ten formulas, two Java bodies), checks both error positions, a second-section-only update, Unicode/CRLF, stale updates, save, close and unavailable extraction. Run the 40 existing LSP/DAP tests without the profile as a separate clean build to verify the default dependency path. Use an isolated Maven repository if other checkouts install Tiny artifacts with different generated AST versions. diff --git a/tools/tinyexpression-p4-lsp-vscode/README.md b/tools/tinyexpression-p4-lsp-vscode/README.md index e0a0c669..ff95b458 100644 --- a/tools/tinyexpression-p4-lsp-vscode/README.md +++ b/tools/tinyexpression-p4-lsp-vscode/README.md @@ -19,6 +19,8 @@ for TinyExpression formulas using the P4 grammar (UBNF-generated, type-safe). - `_tinyP4AstNodePath` — breadth-first path through the AST - `parity.*` — 6-backend evaluation comparison (JAVA_CODE / AST_EVALUATOR / DSL_JAVA_CODE / P4_AST / P4_DSL) +Opt-in embedded Java diagnostics in the existing **Java** LSP consumer are described in [EMBEDDED-LANGUAGES.md](EMBEDDED-LANGUAGES.md). That document distinguishes the current Tiny grammar adaptation from the fixed-revision Java/Rust bridge fixtures; it does not claim a Rust Tiny LSP consumer. + ## Requirements - Java 21+