From 6e689c9b46f983b38d5838e0784c8cf6f530ad61 Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 21 Aug 2024 14:06:00 +1200 Subject: [PATCH 1/4] Support for the Yul language --- README.md | 15 +++- src/index.ts | 18 +++++ src/slangYulParser.ts | 40 ++++++++++ .../Yul/__snapshots__/format.test.js.snap | 74 +++++++++++++++++++ tests/format/Yul/example1.yul | 14 ++++ tests/format/Yul/example2.yul | 9 +++ tests/format/Yul/format.test.js | 1 + 7 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 src/slangYulParser.ts create mode 100644 tests/format/Yul/__snapshots__/format.test.js.snap create mode 100644 tests/format/Yul/example1.yul create mode 100644 tests/format/Yul/example2.yul create mode 100644 tests/format/Yul/format.test.js diff --git a/README.md b/README.md index fbcf564b3..2235ad21f 100644 --- a/README.md +++ b/README.md @@ -109,15 +109,26 @@ The following is the default configuration internally used by this plugin. "plugins": ["prettier-plugin-solidity"], "overrides": [ { - "files": "*.sol", + "files": ["*.sol", "*.yul"], "options": { - "parser": "slang", "printWidth": 80, "tabWidth": 4, "useTabs": false, "singleQuote": false, "bracketSpacing": false, } + }, + { + "files": "*.sol", + "options": { + "parser": "slang" + } + }, + { + "files": "*.yul", + "options": { + "parser": "slang-yul" + } } ] } diff --git a/src/index.ts b/src/index.ts index b5efe7d8b..2d8682292 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import options from './options.js'; import antlrParse from './parser.js'; import antlrPrint from './printer.js'; import slangParse from './slangSolidityParser.js'; +import yulParse from './slangYulParser.js'; import slangPrint from './slangPrinter.js'; import { isBlockComment, isComment } from './slang-utils/is-comment.js'; import { locEnd, locStart } from './slang-utils/loc.js'; @@ -19,6 +20,7 @@ import type { import type { AstNode } from './slang-nodes/types.d.ts'; const slangParserId = 'slang'; +const slangYulParserId = 'slang-yul'; const antlrParserId = 'antlr'; const slangAstId = 'slang-ast'; const antlrAstId = 'antlr-ast'; @@ -34,6 +36,15 @@ const languages: SupportLanguage[] = [ extensions: ['.sol'], parsers: [slangParserId, antlrParserId], vscodeLanguageIds: ['solidity'] + }, + { + linguistLanguageId: 237469033, + name: 'Yul', + aceMode: 'text', + tmScope: 'source.yul', + extensions: ['.yul'], + parsers: [slangYulParserId], + vscodeLanguageIds: ['yul'] } ]; @@ -45,9 +56,16 @@ const slangParser: Parser = { locStart, locEnd }; +const yulParser: Parser = { + astFormat: slangAstId, + parse: yulParse, + locStart, + locEnd +}; const parsers = { [slangParserId]: slangParser, + [slangYulParserId]: yulParser, [antlrParserId]: antlrParser }; diff --git a/src/slangYulParser.ts b/src/slangYulParser.ts new file mode 100644 index 000000000..a00f0d7d0 --- /dev/null +++ b/src/slangYulParser.ts @@ -0,0 +1,40 @@ +// https://prettier.io/docs/en/plugins.html#parsers +import { YulBlock as SlangYulBlock } from '@nomicfoundation/slang/ast'; +import { NonterminalKind } from '@nomicfoundation/slang/cst'; +import { Parser } from '@nomicfoundation/slang/parser'; +import { LanguageFacts } from '@nomicfoundation/slang/utils'; +import { coerce } from 'semver'; +import { clearOffsets } from './slang-utils/metadata.js'; +import { YulBlock } from './slang-nodes/YulBlock.js'; + +import type { ParserOptions } from 'prettier'; +import type { AstNode } from './slang-nodes/types.d.ts'; + +export default function parse( + text: string, + options: ParserOptions +): AstNode { + // const [parser, parseOutput] = createParser(text, options); + const compiler = coerce(options.compiler); + const supportedVersions = LanguageFacts.allVersions(); + + const parser = Parser.create( + compiler && supportedVersions.includes(compiler.version) + ? compiler.version + : supportedVersions[supportedVersions.length - 1] + ); + + const parseOutput = parser.parseNonterminal(NonterminalKind.YulBlock, text); + + if (parseOutput.isValid()) { + // We update the compiler version by the inferred one. + options.compiler = parser.languageVersion; + const parsed = new YulBlock( + new SlangYulBlock(parseOutput.tree.asNonterminalNode()), + options + ); + clearOffsets(); + return parsed; + } + throw new Error(parseOutput.errors()[0].message); +} diff --git a/tests/format/Yul/__snapshots__/format.test.js.snap b/tests/format/Yul/__snapshots__/format.test.js.snap new file mode 100644 index 000000000..0bc716c20 --- /dev/null +++ b/tests/format/Yul/__snapshots__/format.test.js.snap @@ -0,0 +1,74 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`example1.yul format 1`] = ` +====================================options===================================== +parsers: ["slang-yul"] +printWidth: 80 + | printWidth +=====================================input====================================== +{ + function power(base, exponent) -> result + { + switch exponent + case 0 { result := 1 } + case 1 { result := base } + default + { + result := power(mul(base, base), div(exponent, 2)) + switch mod(exponent, 2) + case 1 { result := mul(base, result) } + } + } +} +=====================================output===================================== +{ + function power(base, exponent) -> result { + switch exponent + case 0 { + result := 1 + } + case 1 { + result := base + } + default { + result := power(mul(base, base), div(exponent, 2)) + switch mod(exponent, 2) + case 1 { + result := mul(base, result) + } + } + } +} +================================================================================ +`; + +exports[`example2.yul format 1`] = ` +====================================options===================================== +parsers: ["slang-yul"] +printWidth: 80 + | printWidth +=====================================input====================================== +{ + function power(base, exponent) -> result + { + result := 1 for { let i := 0 } lt(i, exponent) { i := add(i, 1) } + { + result := mul(result, base) + } + } +} +=====================================output===================================== +{ + function power(base, exponent) -> result { + result := 1 + for { + let i := 0 + } lt(i, exponent) { + i := add(i, 1) + } { + result := mul(result, base) + } + } +} +================================================================================ +`; diff --git a/tests/format/Yul/example1.yul b/tests/format/Yul/example1.yul new file mode 100644 index 000000000..4491a54b6 --- /dev/null +++ b/tests/format/Yul/example1.yul @@ -0,0 +1,14 @@ +{ + function power(base, exponent) -> result + { + switch exponent + case 0 { result := 1 } + case 1 { result := base } + default + { + result := power(mul(base, base), div(exponent, 2)) + switch mod(exponent, 2) + case 1 { result := mul(base, result) } + } + } +} \ No newline at end of file diff --git a/tests/format/Yul/example2.yul b/tests/format/Yul/example2.yul new file mode 100644 index 000000000..0510ef31c --- /dev/null +++ b/tests/format/Yul/example2.yul @@ -0,0 +1,9 @@ +{ + function power(base, exponent) -> result + { + result := 1 for { let i := 0 } lt(i, exponent) { i := add(i, 1) } + { + result := mul(result, base) + } + } +} \ No newline at end of file diff --git a/tests/format/Yul/format.test.js b/tests/format/Yul/format.test.js new file mode 100644 index 000000000..209d90b96 --- /dev/null +++ b/tests/format/Yul/format.test.js @@ -0,0 +1 @@ +runFormatTest(import.meta, ['slang-yul']); From 25b2fba041e23b83870d456b0c2546a20e5b0f61 Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 18 Jun 2025 10:59:54 +0100 Subject: [PATCH 2/4] using createParser to infer the correct Language Version --- src/constants.ts | 5 +++ src/index.ts | 13 +++---- src/slang-utils/create-parser.ts | 31 +++++++++++----- src/slangYulParser.ts | 36 ++++++------------- .../Yul/__snapshots__/format.test.js.snap | 2 +- tests/unit/slang-utils/create-parser.test.js | 12 +++++-- 6 files changed, 55 insertions(+), 44 deletions(-) create mode 100644 src/constants.ts diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 000000000..56444122d --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,5 @@ +export const slangParserId = 'slang'; +export const slangYulParserId = 'slang-yul'; +export const antlrParserId = 'antlr'; +export const slangAstId = 'slang-ast'; +export const antlrAstId = 'antlr-ast'; diff --git a/src/index.ts b/src/index.ts index 2d8682292..3f989143a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,13 @@ import yulParse from './slangYulParser.js'; import slangPrint from './slangPrinter.js'; import { isBlockComment, isComment } from './slang-utils/is-comment.js'; import { locEnd, locStart } from './slang-utils/loc.js'; +import { + slangParserId, + slangYulParserId, + antlrParserId, + slangAstId, + antlrAstId +} from './constants.js'; import type { Parser, @@ -19,12 +26,6 @@ import type { } from 'prettier'; import type { AstNode } from './slang-nodes/types.d.ts'; -const slangParserId = 'slang'; -const slangYulParserId = 'slang-yul'; -const antlrParserId = 'antlr'; -const slangAstId = 'slang-ast'; -const antlrAstId = 'antlr-ast'; - // https://prettier.io/docs/en/plugins.html#languages // https://github.com/github-linguist/linguist/blob/master/lib/linguist/languages.yml const languages: SupportLanguage[] = [ diff --git a/src/slang-utils/create-parser.ts b/src/slang-utils/create-parser.ts index f5cd9ffc6..87167be02 100644 --- a/src/slang-utils/create-parser.ts +++ b/src/slang-utils/create-parser.ts @@ -2,6 +2,7 @@ import { NonterminalKind } from '@nomicfoundation/slang/cst'; import { Parser } from '@nomicfoundation/slang/parser'; import { LanguageFacts } from '@nomicfoundation/slang/utils'; import { maxSatisfying } from 'semver'; +import { slangParserId, slangYulParserId } from '../constants.js'; import type { ParseOutput } from '@nomicfoundation/slang/parser'; import type { ParserOptions } from 'prettier'; @@ -12,13 +13,25 @@ const supportedLength = supportedVersions.length; function parserAndOutput( text: string, - version: string + version: string, + { parser: optionsParser }: ParserOptions ): { parser: Parser; parseOutput: ParseOutput } { + let rootKind; + switch (optionsParser) { + case slangParserId: + rootKind = NonterminalKind.SourceUnit; + break; + case slangYulParserId: + rootKind = NonterminalKind.YulBlock; + break; + default: + throw new Error( + `Parser '${optionsParser as string}' is not supported for Language Inference.` + ); + } + const parser = Parser.create(version); - return { - parser, - parseOutput: parser.parseNonterminal(NonterminalKind.SourceUnit, text) - }; + return { parser, parseOutput: parser.parseNonterminal(rootKind, text) }; } function createError( @@ -36,7 +49,7 @@ export function createParser( ): { parser: Parser; parseOutput: ParseOutput } { const compiler = maxSatisfying(supportedVersions, options.compiler); if (compiler) { - const result = parserAndOutput(text, compiler); + const result = parserAndOutput(text, compiler, options); if (!result.parseOutput.isValid()) throw createError( @@ -55,7 +68,8 @@ export function createParser( if (inferredLength === 0 || inferredLength === supportedLength) { const result = parserAndOutput( text, - supportedVersions[supportedLength - 1] + supportedVersions[supportedLength - 1], + options ); if (!result.parseOutput.isValid()) @@ -70,7 +84,8 @@ export function createParser( const result = parserAndOutput( text, - inferredRanges[inferredRanges.length - 1] + inferredRanges[inferredLength - 1], + options ); if (!result.parseOutput.isValid()) diff --git a/src/slangYulParser.ts b/src/slangYulParser.ts index a00f0d7d0..e7b7f9cc9 100644 --- a/src/slangYulParser.ts +++ b/src/slangYulParser.ts @@ -1,10 +1,7 @@ // https://prettier.io/docs/en/plugins.html#parsers import { YulBlock as SlangYulBlock } from '@nomicfoundation/slang/ast'; -import { NonterminalKind } from '@nomicfoundation/slang/cst'; -import { Parser } from '@nomicfoundation/slang/parser'; -import { LanguageFacts } from '@nomicfoundation/slang/utils'; -import { coerce } from 'semver'; -import { clearOffsets } from './slang-utils/metadata.js'; +import { clearOffsets } from './slang-nodes/SlangNode.js'; +import { createParser } from './slang-utils/create-parser.js'; import { YulBlock } from './slang-nodes/YulBlock.js'; import type { ParserOptions } from 'prettier'; @@ -14,27 +11,14 @@ export default function parse( text: string, options: ParserOptions ): AstNode { - // const [parser, parseOutput] = createParser(text, options); - const compiler = coerce(options.compiler); - const supportedVersions = LanguageFacts.allVersions(); + const { parser, parseOutput } = createParser(text, options); - const parser = Parser.create( - compiler && supportedVersions.includes(compiler.version) - ? compiler.version - : supportedVersions[supportedVersions.length - 1] + // We update the compiler version by the inferred one. + options.compiler = parser.languageVersion; + const parsed = new YulBlock( + new SlangYulBlock(parseOutput.tree.asNonterminalNode()), + options ); - - const parseOutput = parser.parseNonterminal(NonterminalKind.YulBlock, text); - - if (parseOutput.isValid()) { - // We update the compiler version by the inferred one. - options.compiler = parser.languageVersion; - const parsed = new YulBlock( - new SlangYulBlock(parseOutput.tree.asNonterminalNode()), - options - ); - clearOffsets(); - return parsed; - } - throw new Error(parseOutput.errors()[0].message); + clearOffsets(); + return parsed; } diff --git a/tests/format/Yul/__snapshots__/format.test.js.snap b/tests/format/Yul/__snapshots__/format.test.js.snap index 0bc716c20..4b7434cc2 100644 --- a/tests/format/Yul/__snapshots__/format.test.js.snap +++ b/tests/format/Yul/__snapshots__/format.test.js.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`example1.yul format 1`] = ` ====================================options===================================== diff --git a/tests/unit/slang-utils/create-parser.test.js b/tests/unit/slang-utils/create-parser.test.js index cc28ce06a..78d15d125 100644 --- a/tests/unit/slang-utils/create-parser.test.js +++ b/tests/unit/slang-utils/create-parser.test.js @@ -1,9 +1,10 @@ import { LanguageFacts } from '@nomicfoundation/slang/utils'; import { createParser } from '../../../src/slang-utils/create-parser.js'; +import { slangParserId } from '../../../src/constants.js'; describe('inferLanguage', function () { const latestSupportedVersion = LanguageFacts.latestVersion(); - const options = { filepath: 'test.sol' }; + const options = { parser: slangParserId }; const fixtures = [ { @@ -97,22 +98,27 @@ describe('inferLanguage', function () { test('should use compiler option if given', function () { let { parser } = createParser(`pragma solidity ^0.8.0;`, { + ...options, compiler: '0.8.20' }); expect(parser.languageVersion).toEqual('0.8.20'); ({ parser } = createParser(`pragma solidity ^0.8.0;`, { + ...options, compiler: '0.8.2' })); expect(parser.languageVersion).toEqual('0.8.2'); - ({ parser } = createParser(`pragma solidity ^0.7.0;`, {})); + ({ parser } = createParser(`pragma solidity ^0.7.0;`, options)); expect(parser.languageVersion).toEqual('0.7.6'); }); test('should throw if compiler option does not match the syntax', function () { expect(() => - createParser(`contract Foo {byte bar;}`, { compiler: '0.8.0' }) + createParser(`contract Foo {byte bar;}`, { + ...options, + compiler: '0.8.0' + }) ).toThrow( 'Based on the compiler option provided, we inferred your code to be using Solidity version' ); From 08d0a841fd589c3f0be7a0c0d2e2c4b19e6667db Mon Sep 17 00:00:00 2001 From: Klaus Date: Sat, 15 Nov 2025 22:59:25 +0100 Subject: [PATCH 3/4] small improvement --- src/index.ts | 6 +++--- src/slang-utils/create-parser.ts | 22 ++++++++++------------ 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/index.ts b/src/index.ts index 3f989143a..04f58c9a1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,11 +11,11 @@ import slangPrint from './slangPrinter.js'; import { isBlockComment, isComment } from './slang-utils/is-comment.js'; import { locEnd, locStart } from './slang-utils/loc.js'; import { - slangParserId, - slangYulParserId, + antlrAstId, antlrParserId, slangAstId, - antlrAstId + slangParserId, + slangYulParserId } from './constants.js'; import type { diff --git a/src/slang-utils/create-parser.ts b/src/slang-utils/create-parser.ts index 87167be02..beeb6b5fa 100644 --- a/src/slang-utils/create-parser.ts +++ b/src/slang-utils/create-parser.ts @@ -10,24 +10,22 @@ import type { AstNode } from '../slang-nodes/types.d.ts'; const supportedVersions = LanguageFacts.allVersions(); const supportedLength = supportedVersions.length; +const rootKindMap = new Map['parser'], NonterminalKind>([ + [slangParserId, NonterminalKind.SourceUnit], + [slangYulParserId, NonterminalKind.YulBlock] +]); function parserAndOutput( text: string, version: string, { parser: optionsParser }: ParserOptions ): { parser: Parser; parseOutput: ParseOutput } { - let rootKind; - switch (optionsParser) { - case slangParserId: - rootKind = NonterminalKind.SourceUnit; - break; - case slangYulParserId: - rootKind = NonterminalKind.YulBlock; - break; - default: - throw new Error( - `Parser '${optionsParser as string}' is not supported for Language Inference.` - ); + const rootKind = rootKindMap.get(optionsParser); + + if (rootKind === undefined) { + throw new Error( + `Parser '${optionsParser as string}' is not supported for Language Inference.` + ); } const parser = Parser.create(version); From cbe36adcbe26a9d463c8162cf74693eb98d8bbc9 Mon Sep 17 00:00:00 2001 From: Klaus Date: Fri, 30 Jan 2026 15:56:47 -0300 Subject: [PATCH 4/4] using the new collection of metadata --- src/slangYulParser.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/slangYulParser.ts b/src/slangYulParser.ts index e7b7f9cc9..5020cbe7a 100644 --- a/src/slangYulParser.ts +++ b/src/slangYulParser.ts @@ -1,11 +1,10 @@ // https://prettier.io/docs/en/plugins.html#parsers import { YulBlock as SlangYulBlock } from '@nomicfoundation/slang/ast'; -import { clearOffsets } from './slang-nodes/SlangNode.js'; import { createParser } from './slang-utils/create-parser.js'; import { YulBlock } from './slang-nodes/YulBlock.js'; import type { ParserOptions } from 'prettier'; -import type { AstNode } from './slang-nodes/types.d.ts'; +import type { AstNode, Comment } from './slang-nodes/types.d.ts'; export default function parse( text: string, @@ -15,10 +14,15 @@ export default function parse( // We update the compiler version by the inferred one. options.compiler = parser.languageVersion; + const comments: Comment[] = []; const parsed = new YulBlock( new SlangYulBlock(parseOutput.tree.asNonterminalNode()), + { offsets: new Map(), comments }, options ); - clearOffsets(); + + // Because of comments being extracted like a Russian doll, the order needs + // to be fixed at the end. + parsed.comments = comments.sort((a, b) => a.loc.start - b.loc.start); return parsed; }