From c8ccb9434ce61630a5282e6ad4074ea0ac61dc63 Mon Sep 17 00:00:00 2001 From: opaopa6969 Date: Fri, 9 Oct 2026 13:47:18 +0900 Subject: [PATCH 1/3] 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 bf4f409bc0200bd9fbfc80b5b5ddd3a248179d62 Mon Sep 17 00:00:00 2001 From: opaopa6969 Date: Fri, 9 Oct 2026 14:16:04 +0900 Subject: [PATCH 2/3] 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 3/3] 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+