From 79ef1e95fb4ecc3ed34ba6bd8f346643bf06c32b Mon Sep 17 00:00:00 2001 From: David Lurton Date: Fri, 14 May 2021 01:02:05 -0700 Subject: [PATCH] Add import statement (#57) --- README.md | 18 +- pig/src/org/partiql/pig/cmdline/Command.kt | 2 +- .../partiql/pig/cmdline/CommandLineParser.kt | 2 +- .../pig/domain/model/SemanticErrorContext.kt | 6 +- .../pig/domain/parser/ImportSourceOpener.kt | 34 ++ .../partiql/pig/domain/parser/InputSource.kt | 45 ++ .../pig/domain/parser/ParserErrorContext.kt | 11 +- .../pig/domain/parser/SourceLocation.kt | 36 ++ .../pig/domain/parser/TypeDomainParser.kt | 512 ++++++++++-------- pig/src/org/partiql/pig/errors/PigError.kt | 7 +- pig/src/org/partiql/pig/main.kt | 6 +- .../domains/circular_universe_c.ion | 4 + .../domains/circular_universe_d.ion | 4 + pig/test-domains/domains/import_b.ion | 4 + pig/test-domains/domains/universe_a.ion | 3 + pig/test-domains/main.ion | 4 + pig/test-domains/other/universe_b.ion | 3 + .../pig/cmdline/CommandLineParserTests.kt | 12 +- .../partiql/pig/domain/PermuteDomainTests.kt | 5 +- .../pig/domain/TypeDomainParserErrorsTest.kt | 4 +- .../pig/domain/TypeDomainParserImportTests.kt | 40 ++ .../pig/domain/TypeDomainParserTests.kt | 11 +- .../domain/TypeDomainSemanticCheckerTests.kt | 4 +- pig/test/org/partiql/pig/domain/Util.kt | 3 +- pig/test/org/partiql/pig/util/ParseHelpers.kt | 50 ++ 25 files changed, 573 insertions(+), 257 deletions(-) create mode 100644 pig/src/org/partiql/pig/domain/parser/ImportSourceOpener.kt create mode 100644 pig/src/org/partiql/pig/domain/parser/InputSource.kt create mode 100644 pig/src/org/partiql/pig/domain/parser/SourceLocation.kt create mode 100644 pig/test-domains/domains/circular_universe_c.ion create mode 100644 pig/test-domains/domains/circular_universe_d.ion create mode 100644 pig/test-domains/domains/import_b.ion create mode 100644 pig/test-domains/domains/universe_a.ion create mode 100644 pig/test-domains/main.ion create mode 100644 pig/test-domains/other/universe_b.ion create mode 100644 pig/test/org/partiql/pig/domain/TypeDomainParserImportTests.kt create mode 100644 pig/test/org/partiql/pig/util/ParseHelpers.kt diff --git a/README.md b/README.md index 1c017b8..db42771 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,9 @@ assertEquals(onePlusOne, anotherOnePlusOne) // Top level type_universe ::= ... definition ::= '(' 'define' symbol ')' -stmt ::= | +stmt ::= | | + +import ::= `(import | @@ -400,6 +402,20 @@ Unlike record elements, product element defintions must include identifiers. (product int_pair first::int second::int) ``` +#### Imports + +It is possible to split type universe definitions among multiple files: + +``` +// root.ion: +(import "a.ion") +(import "b.ion") +``` + +The resulting type universe will contain all type domains from both `a.ion` and `b.ion`, `root.ion` may also +define additional type domains. The primary purpose of this is to be able to permute domains defined in another +file. + #### Using PIG In Your Project diff --git a/pig/src/org/partiql/pig/cmdline/Command.kt b/pig/src/org/partiql/pig/cmdline/Command.kt index fa5ede4..c565d7e 100644 --- a/pig/src/org/partiql/pig/cmdline/Command.kt +++ b/pig/src/org/partiql/pig/cmdline/Command.kt @@ -20,5 +20,5 @@ import java.io.File sealed class Command { object ShowHelp : Command() data class InvalidCommandLineArguments(val message: String) : Command() - data class Generate(val typeUniverseFile: File, val outputFile: File, val target: TargetLanguage) : Command() + data class Generate(val typeUniverseFile: String, val outputFile: String, val target: TargetLanguage) : Command() } \ No newline at end of file diff --git a/pig/src/org/partiql/pig/cmdline/CommandLineParser.kt b/pig/src/org/partiql/pig/cmdline/CommandLineParser.kt index e8f11c7..232305f 100644 --- a/pig/src/org/partiql/pig/cmdline/CommandLineParser.kt +++ b/pig/src/org/partiql/pig/cmdline/CommandLineParser.kt @@ -145,7 +145,7 @@ class CommandLineParser { LanguageTargetType.CUSTOM -> TargetLanguage.Custom(optSet.valueOf(templateOpt)) } - Command.Generate(typeUniverseFile, outputFile, target) + Command.Generate(typeUniverseFile.toString(), outputFile.toString(), target) } } } catch(ex: OptionException) { diff --git a/pig/src/org/partiql/pig/domain/model/SemanticErrorContext.kt b/pig/src/org/partiql/pig/domain/model/SemanticErrorContext.kt index 5d7b20e..61c9c1e 100644 --- a/pig/src/org/partiql/pig/domain/model/SemanticErrorContext.kt +++ b/pig/src/org/partiql/pig/domain/model/SemanticErrorContext.kt @@ -18,6 +18,8 @@ package org.partiql.pig.domain.model import com.amazon.ionelement.api.IonLocation import com.amazon.ionelement.api.MetaContainer import com.amazon.ionelement.api.location +import org.partiql.pig.domain.parser.SourceLocation +import org.partiql.pig.domain.parser.sourceLocation import org.partiql.pig.errors.PigException import org.partiql.pig.errors.ErrorContext import org.partiql.pig.errors.PigError @@ -109,9 +111,9 @@ sealed class SemanticErrorContext(val msgFormatter: () -> String): ErrorContext * Shortcut for throwing [PigException] with the specified metas and [PigError]. */ fun semanticError(blame: MetaContainer, context: ErrorContext): Nothing = - semanticError(blame.location, context) + semanticError(blame.sourceLocation, context) /** * Shortcut for throwing [PigException] with the specified metas and [PigError]. */ -fun semanticError(blame: IonLocation?, context: ErrorContext): Nothing = +fun semanticError(blame: SourceLocation?, context: ErrorContext): Nothing = throw PigException(PigError(blame, context)) diff --git a/pig/src/org/partiql/pig/domain/parser/ImportSourceOpener.kt b/pig/src/org/partiql/pig/domain/parser/ImportSourceOpener.kt new file mode 100644 index 0000000..6f6e434 --- /dev/null +++ b/pig/src/org/partiql/pig/domain/parser/ImportSourceOpener.kt @@ -0,0 +1,34 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.partiql.pig.domain.parser + +import com.amazon.ion.IonReader +import com.amazon.ion.system.IonReaderBuilder +import java.io.Closeable +import java.io.File +import java.io.FileInputStream +import java.nio.file.Path + +//class ImportSource( +// val fullyQualifiedName: String, +// val reader: IonReader +//) : Closeable { +// override fun close() { +// reader.close() +// } +//} + +// TODO: names diff --git a/pig/src/org/partiql/pig/domain/parser/InputSource.kt b/pig/src/org/partiql/pig/domain/parser/InputSource.kt new file mode 100644 index 0000000..01055fd --- /dev/null +++ b/pig/src/org/partiql/pig/domain/parser/InputSource.kt @@ -0,0 +1,45 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.partiql.pig.domain.parser + +import java.io.File +import java.io.FileInputStream +import java.io.InputStream + +/** + * Provides an abstraction for file-system related functions used when importing files. + */ +internal interface InputSource { + /** + * Opens an input stream for the given source name. + * + * The [sourceName] is implementation-defined. In the case of a file system implementaiton it is the path to a + * file, either relative to the working directory or absolute. + */ + fun openStream(sourceName: String): InputStream + + /** + * Returns the "canonical name" of the given source. In the case of a file system, this converts the relative + * path to an absolute path. + */ + fun getCanonicalName(sourceName: String): String +} + +internal val FILE_SYSTEM_SOURCE = object : InputSource { + override fun openStream(qualifiedSource: String) = FileInputStream(qualifiedSource) + + override fun getCanonicalName(sourceName: String): String = File(sourceName).canonicalFile.toString() +} \ No newline at end of file diff --git a/pig/src/org/partiql/pig/domain/parser/ParserErrorContext.kt b/pig/src/org/partiql/pig/domain/parser/ParserErrorContext.kt index 7c4ddba..495c736 100644 --- a/pig/src/org/partiql/pig/domain/parser/ParserErrorContext.kt +++ b/pig/src/org/partiql/pig/domain/parser/ParserErrorContext.kt @@ -42,6 +42,9 @@ sealed class ParserErrorContext(val msgFormatter: () -> String): ErrorContext { override fun hashCode(): Int = 0 } + data class CouldNotFindImportedTypeUniverse(val tag: String) + : ParserErrorContext({ "Could not find imported type universe: $tag" }) + data class UnknownConstructor(val tag: String) : ParserErrorContext({ "Unknown constructor: '$tag' (expected constructors are 'domain' or 'permute_domain')" }) @@ -79,8 +82,7 @@ sealed class ParserErrorContext(val msgFormatter: () -> String): ErrorContext { : ParserErrorContext({ "Element has multiple name annotations"}) } - -fun parseError(blame: IonLocation?, context: ErrorContext): Nothing = +fun parseError(blame: SourceLocation?, context: ErrorContext): Nothing = PigError(blame, context).let { throw when (context) { is ParserErrorContext.IonElementError -> { @@ -91,8 +93,3 @@ fun parseError(blame: IonLocation?, context: ErrorContext): Nothing = } } -fun parseError(blame: IonElement, context: ErrorContext): Nothing { - val loc = blame.metas.location - parseError(loc, context) -} - diff --git a/pig/src/org/partiql/pig/domain/parser/SourceLocation.kt b/pig/src/org/partiql/pig/domain/parser/SourceLocation.kt new file mode 100644 index 0000000..fdb307c --- /dev/null +++ b/pig/src/org/partiql/pig/domain/parser/SourceLocation.kt @@ -0,0 +1,36 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.partiql.pig.domain.parser + +import com.amazon.ionelement.api.IonLocation +import com.amazon.ionelement.api.MetaContainer + +/** + * Includes path to a source file and a position within it. + * + * Used to construct helpful error messages for the end-user, who will be able to know the file, line & column of + * a given error. + */ +data class SourceLocation(val path: String, val location: IonLocation) { + override fun toString(): String { + return "$path:$location" + } +} + +internal const val SOURCE_LOCATION_META_TAG = "\$pig_source_location" + +internal val MetaContainer.sourceLocation + get() = this[SOURCE_LOCATION_META_TAG] as? SourceLocation diff --git a/pig/src/org/partiql/pig/domain/parser/TypeDomainParser.kt b/pig/src/org/partiql/pig/domain/parser/TypeDomainParser.kt index fb23328..b4c78da 100644 --- a/pig/src/org/partiql/pig/domain/parser/TypeDomainParser.kt +++ b/pig/src/org/partiql/pig/domain/parser/TypeDomainParser.kt @@ -15,275 +15,351 @@ package org.partiql.pig.domain.parser -import com.amazon.ionelement.api.* import com.amazon.ion.IonReader import com.amazon.ion.system.IonReaderBuilder -import org.partiql.pig.domain.model.* +import com.amazon.ionelement.api.AnyElement +import com.amazon.ionelement.api.IonElement +import com.amazon.ionelement.api.IonElementException +import com.amazon.ionelement.api.IonElementLoaderOptions +import com.amazon.ionelement.api.IonLocation +import com.amazon.ionelement.api.MetaContainer +import com.amazon.ionelement.api.SexpElement +import com.amazon.ionelement.api.SymbolElement +import com.amazon.ionelement.api.createIonElementLoader +import com.amazon.ionelement.api.emptyMetaContainer +import com.amazon.ionelement.api.head +import com.amazon.ionelement.api.location +import com.amazon.ionelement.api.metaContainerOf +import com.amazon.ionelement.api.tag +import com.amazon.ionelement.api.tail +import org.partiql.pig.domain.model.Arity +import org.partiql.pig.domain.model.DataType +import org.partiql.pig.domain.model.NamedElement +import org.partiql.pig.domain.model.PermutedDomain +import org.partiql.pig.domain.model.PermutedSum +import org.partiql.pig.domain.model.Statement +import org.partiql.pig.domain.model.Transform +import org.partiql.pig.domain.model.TupleType +import org.partiql.pig.domain.model.TypeDomain +import org.partiql.pig.domain.model.TypeRef +import org.partiql.pig.domain.model.TypeUniverse +import org.partiql.pig.errors.ErrorContext +import java.io.File +import java.io.FileNotFoundException +import java.io.InputStream +import java.util.Stack + + +fun parseTypeUniverseFile(path: String): TypeUniverse { + val parser = Parser(FILE_SYSTEM_SOURCE) + return parser.parseTypeUniverse(path) +} + +private class Parser( + val inputSource: InputSource +) { + /** This contains every file the parser has "seen" and is used detect and prevent import cycles. */ + private val parseHistory = HashSet() + private val qualifedSourceStack = Stack().apply { push(".") } + + /** Parses a type universe in the specified [IonReader]. */ + fun parseTypeUniverse(source: String): TypeUniverse { + val elementLoader = createIonElementLoader(IonElementLoaderOptions(includeLocationMeta = true)) + val qualifiedSource = inputSource.getCanonicalName(source) + return inputSource.openStream(qualifiedSource).use { inputStream: InputStream -> + IonReaderBuilder.standard().build(inputStream).use { reader: IonReader -> + this.parseHistory.add(qualifiedSource) + this.qualifedSourceStack.push(qualifiedSource) + val domains = try { + val topLevelElements = elementLoader.loadAllElements(reader) + topLevelElements.flatMap { topLevelValue -> + val topLevelSexp = topLevelValue.asSexp() + when (topLevelSexp.tag) { + "define" -> listOf(parseDefine(topLevelSexp)) + "transform" -> listOf(parseTransform(topLevelSexp)) + "import" -> parseImport(topLevelSexp) + else -> parseError( + topLevelSexp.head, + ParserErrorContext.InvalidTopLevelTag(topLevelSexp.tag)) + } + } + } catch (iee: IonElementException) { + parseError(iee.location?.toSourceLocation(), ParserErrorContext.IonElementError(iee)) + } + this.qualifedSourceStack.pop() + TypeUniverse(domains) + } + } + + } -/** Parses a type universe contained in [universeText]. */ -fun parseTypeUniverse(universeText: String) = - IonReaderBuilder.standard().build(universeText).use { - parseTypeUniverse(it) + fun parseError(blame: IonElement, context: ErrorContext): Nothing { + val loc = blame.metas.location?.toSourceLocation() + parseError(loc, context) } -/** Parses a type universe in the specified [IonReader]. */ -fun parseTypeUniverse(reader: IonReader): TypeUniverse { - val elementLoader = createIonElementLoader(IonElementLoaderOptions(includeLocationMeta = true)) - - val domains = try { - val topLevelElements = elementLoader.loadAllElements(reader) - topLevelElements.map { topLevelValue -> - val topLevelSexp = topLevelValue.asSexp() - when (topLevelSexp.tag) { - "define" -> parseDefine(topLevelSexp) - "transform" -> parseTransform(topLevelSexp) - else -> parseError( - topLevelSexp.head, - ParserErrorContext.InvalidTopLevelTag(topLevelSexp.tag)) + private fun IonLocation.toSourceLocation() = SourceLocation(qualifedSourceStack.peek(), this) + + private fun MetaContainer.toSourceLocationMetas(): MetaContainer = this.location?.let { + metaContainerOf(SOURCE_LOCATION_META_TAG to it.toSourceLocation()) + } ?: emptyMetaContainer() + + private fun parseImport(sexp: SexpElement): List { + requireArityForTag(sexp, 1) + val relativePath = sexp.tail.single().asString().textValue + // TODO: need to report proper error to user if file does not exist. + + val workingDirectory = File(this.qualifedSourceStack.peek()).parentFile + val qualifiedSourcePath = File(workingDirectory, relativePath).canonicalPath + return if(!parseHistory.contains(qualifiedSourcePath)) { + try { + parseTypeUniverse(qualifiedSourcePath).statements + } catch (e: FileNotFoundException) { + parseError( + sexp.metas.location?.toSourceLocation(), + ParserErrorContext.CouldNotFindImportedTypeUniverse(qualifiedSourcePath)) } + } else { + listOf() } } - catch(iee: IonElementException) { - parseError(iee.location, ParserErrorContext.IonElementError(iee)) + private fun parseDefine(sexp: SexpElement): Statement { + requireArityForTag(sexp, 2) + val args = sexp.tail // Skip tag + val name = args.head.symbolValue + val valueSexp = args.tail.head.asSexp() + + return when (valueSexp.tag) { + "domain" -> parseTypeDomain(name, valueSexp) + "permute_domain" -> parsePermuteDomain(name, valueSexp) + else -> parseError( + valueSexp.head, + ParserErrorContext.UnknownConstructor(valueSexp.tag)) + } } - return TypeUniverse(domains) -} - -private fun parseDefine(sexp: SexpElement): Statement { - requireArityForTag(sexp, 2) - val args = sexp.tail // Skip tag - val name = args.head.symbolValue - val valueSexp = args.tail.head.asSexp() - - return when (valueSexp.tag) { - "domain" -> parseTypeDomain(name, valueSexp) - "permute_domain" -> parsePermuteDomain(name, valueSexp) - else -> parseError( - valueSexp.head, - ParserErrorContext.UnknownConstructor(valueSexp.tag)) + fun parseTransform(sexp: SexpElement): Statement { + requireArityForTag(sexp, 2) + return Transform( + sourceDomainTag = sexp.values[1].symbolValue, + destinationDomainTag = sexp.values[2].symbolValue, + metas = sexp.metas.toSourceLocationMetas() + ) } -} -fun parseTransform(sexp: SexpElement): Statement { - requireArityForTag(sexp, 2) - return Transform( - sourceDomainTag = sexp.values[1].symbolValue, - destinationDomainTag = sexp.values[2].symbolValue, - metas = sexp.metas - ) -} + private fun parseTypeDomain(domainName: String, sexp: SexpElement): TypeDomain { + val args = sexp.tail // Skip tag + //val typesSexps = args.tail -private fun parseTypeDomain(domainName: String, sexp: SexpElement): TypeDomain { - val args = sexp.tail // Skip tag - //val typesSexps = args.tail + val userTypes = args.map { tlv -> + val tlvs = tlv.asSexp() + parseDomainLevelStatement(tlvs) + }.toList() - val userTypes = args.map { tlv -> - val tlvs = tlv.asSexp() - parseDomainLevelStatement(tlvs) - }.toList() - - return TypeDomain( - tag = domainName, - userTypes = userTypes, - metas = sexp.metas) -} + return TypeDomain( + tag = domainName, + userTypes = userTypes, + metas = sexp.metas.toSourceLocationMetas()) + } -private fun parseDomainLevelStatement(tlvs: SexpElement): DataType.UserType { - return when (tlvs.tag) { - "product" -> parseProductBody(tlvs.tail, tlvs.metas) - "record" -> parseRecordBody(tlvs.tail, tlvs.metas) - "sum" -> parseSum(tlvs) - else -> parseError(tlvs.head, ParserErrorContext.InvalidDomainLevelTag(tlvs.tag)) + private fun parseDomainLevelStatement(tlvs: SexpElement): DataType.UserType { + return when (tlvs.tag) { + "product" -> parseProductBody(tlvs.tail, tlvs.metas.toSourceLocationMetas()) + "record" -> parseRecordBody(tlvs.tail, tlvs.metas.toSourceLocationMetas()) + "sum" -> parseSum(tlvs) + else -> parseError(tlvs.head, ParserErrorContext.InvalidDomainLevelTag(tlvs.tag)) + } } -} -private fun parseTypeRefs(values: List): List = - values.map { parseSingleTypeRef(it) } - -// Parses a sum-variant product or record (depending on the syntax used) -private fun parseVariant( - bodyArguments: List, - metas: MetaContainer -): DataType.UserType.Tuple { - val elements = bodyArguments.tail - - // If there are no elements, definitely not a record. - val isRecord = if(elements.none()) { - false - } else { - // if the head element is an s-exp that does not start with `?` or `*` then we're parsing a record - when (val headElem = elements.head) { - is SexpElement -> { - when (headElem.values.head.symbolValue) { - "?", "*" -> false - else -> true + private fun parseTypeRefs(values: List): List = + values.map { parseSingleTypeRef(it) } + + // Parses a sum-variant product or record (depending on the syntax used) + private fun parseVariant( + bodyArguments: List, + metas: MetaContainer + ): DataType.UserType.Tuple { + val elements = bodyArguments.tail + + // If there are no elements, definitely not a record. + val isRecord = if(elements.none()) { + false + } else { + // if the head element is an s-exp that does not start with `?` or `*` then we're parsing a record + when (val headElem = elements.head) { + is SexpElement -> { + when (headElem.values.head.symbolValue) { + "?", "*" -> false + else -> true + } } + is SymbolElement -> false + else -> parseError(elements.head, ParserErrorContext.ExpectedSymbolOrSexp(elements.head.type)) } - is SymbolElement -> false - else -> parseError(elements.head, ParserErrorContext.ExpectedSymbolOrSexp(elements.head.type)) } - } - return when { - isRecord -> { - parseRecordBody(bodyArguments, metas) - } else -> { - parseProductBody(bodyArguments, metas) + return when { + isRecord -> { + parseRecordBody(bodyArguments, metas) + } else -> { + parseProductBody(bodyArguments, metas) + } } } -} - -private fun parseProductBody(bodyArguments: List, metas: MetaContainer): DataType.UserType.Tuple { - val typeName = bodyArguments.head.symbolValue - val namedElements = parseProductElements(bodyArguments.tail) + private fun parseProductBody(bodyArguments: List, metas: MetaContainer): DataType.UserType.Tuple { + val typeName = bodyArguments.head.symbolValue - return DataType.UserType.Tuple(typeName, TupleType.PRODUCT, namedElements, metas) -} - -private fun parseProductElements(values: List): List = - values.map { - val identifier = when(it.annotations.size) { - // TODO: add tests for these errrors - 0 -> parseError(it, ParserErrorContext.MissingElementIdentifierAnnotation) - 1 -> it.annotations.single() - else -> parseError(it, ParserErrorContext.MultipleElementIdentifierAnnotations) - } + val namedElements = parseProductElements(bodyArguments.tail) - NamedElement( - tag = "", // NOTE: tag is not used in the s-expression representation of products! - identifier = identifier, - typeReference = parseSingleTypeRef(it), - metas = it.metas) + return DataType.UserType.Tuple(typeName, TupleType.PRODUCT, namedElements, metas) } -private fun parseRecordBody(bodyArguments: List, metas: MetaContainer): DataType.UserType.Tuple { - val typeName = bodyArguments.head.symbolValue - val namedElements = parseRecordElements(bodyArguments.tail) - return DataType.UserType.Tuple(typeName, TupleType.RECORD, namedElements, metas) -} - -fun parseRecordElements(elementSexps: List): List = - elementSexps.asSequence() - .map { it.asSexp() } - .map { elementSexp -> - if(elementSexp.values.size != 2) { - parseError(elementSexp, ParserErrorContext.InvalidArity(2, elementSexp.size)) - } - val tag = elementSexp.values[0].symbolValue - val identifier = when(elementSexp.annotations.size) { - 0 -> tag - 1 -> elementSexp.annotations.single() - else -> parseError(elementSexp, ParserErrorContext.MultipleElementIdentifierAnnotations) + private fun parseProductElements(values: List): List = + values.map { + val identifier = when(it.annotations.size) { + // TODO: add tests for these errrors + 0 -> parseError(it, ParserErrorContext.MissingElementIdentifierAnnotation) + 1 -> it.annotations.single() + else -> parseError(it, ParserErrorContext.MultipleElementIdentifierAnnotations) } - val typeRef = parseSingleTypeRef(elementSexp.values[1]) + NamedElement( + tag = "", // NOTE: tag is not used in the s-expression representation of products! identifier = identifier, - tag = tag, - typeReference = typeRef, - metas = elementSexp.metas) + typeReference = parseSingleTypeRef(it), + metas = it.metas.toSourceLocationMetas()) } - .toList() -private fun parseSum(sexp: SexpElement): DataType.UserType.Sum { - val args = sexp.tail // Skip tag - val typeName = args.head.symbolValue - - val variants = args.tail.map { - parseSumVariant(it.asSexp()) + private fun parseRecordBody(bodyArguments: List, metas: MetaContainer): DataType.UserType.Tuple { + val typeName = bodyArguments.head.symbolValue + val namedElements = parseRecordElements(bodyArguments.tail) + return DataType.UserType.Tuple(typeName, TupleType.RECORD, namedElements, metas) } - return DataType.UserType.Sum(typeName, variants.toList(), sexp.metas) -} - -private fun parseSumVariant(sexp: SexpElement): DataType.UserType.Tuple { - return parseVariant(sexp.values, sexp.metas) -} - -private fun parseSingleTypeRef(typeRefExp: IonElement): TypeRef { - val metas = typeRefExp.metas - return when (typeRefExp) { - is SymbolElement -> TypeRef(typeRefExp.textValue, Arity.Required, metas) - is SexpElement -> { - when (typeRefExp.tag) { - "?" -> { - requireArityForTag(typeRefExp, 1) - val typeName = typeRefExp.tail.head.symbolValue - TypeRef(typeName, Arity.Optional, metas) + fun parseRecordElements(elementSexps: List): List = + elementSexps.asSequence() + .map { it.asSexp() } + .map { elementSexp -> + if(elementSexp.values.size != 2) { + parseError(elementSexp, ParserErrorContext.InvalidArity(2, elementSexp.size)) } - "*" -> { - requireArityForTag(typeRefExp, IntRange(2, 3)) - val typeName = typeRefExp.tail.head.symbolValue - val arityRange = typeRefExp.tail.tail - val minArity = arityRange.head.longValue - TypeRef(typeName, Arity.Variadic(minArity.toInt()), metas) + val tag = elementSexp.values[0].symbolValue + val identifier = when(elementSexp.annotations.size) { + 0 -> tag + 1 -> elementSexp.annotations.single() + else -> parseError(elementSexp, ParserErrorContext.MultipleElementIdentifierAnnotations) } - else -> parseError(typeRefExp.head, ParserErrorContext.ExpectedTypeReferenceArityTag(typeRefExp.tag)) + val typeRef = parseSingleTypeRef(elementSexp.values[1]) + NamedElement( + identifier = identifier, + tag = tag, + typeReference = typeRef, + metas = elementSexp.metas.toSourceLocationMetas()) } + .toList() + + private fun parseSum(sexp: SexpElement): DataType.UserType.Sum { + val args = sexp.tail // Skip tag + val typeName = args.head.symbolValue + + val variants = args.tail.map { + parseSumVariant(it.asSexp()) } - else -> parseError(typeRefExp, ParserErrorContext.ExpectedSymbolOrSexp(typeRefExp.type)) + + return DataType.UserType.Sum(typeName, variants.toList(), sexp.metas.toSourceLocationMetas()) + } + + private fun parseSumVariant(sexp: SexpElement): DataType.UserType.Tuple { + return parseVariant(sexp.values, sexp.metas.toSourceLocationMetas()) } -} -private fun parsePermuteDomain(domainName: String, sexp: SexpElement): PermutedDomain { - val args = sexp.tail // Skip tag - - val permutingDomain = args.head.symbolValue - val removedTypes = mutableListOf() - val newTypes = mutableListOf() - val permutedSums = mutableListOf() - - val alterSexps = args.tail - alterSexps.map { it.asSexp() }.forEach { alterSexp -> - when(alterSexp.head.symbolValue) { - "with" -> permutedSums.add(parseWithSum(alterSexp)) - "exclude" -> alterSexp.tail.mapTo(removedTypes) { it.symbolValue } - "include" -> alterSexp.tail.mapTo(newTypes) { parseDomainLevelStatement(it.asSexp()) } - else -> parseError(alterSexp, ParserErrorContext.InvalidPermutedDomainTag(alterSexp.head.symbolValue)) + private fun parseSingleTypeRef(typeRefExp: IonElement): TypeRef { + val metas = typeRefExp.metas.toSourceLocationMetas() + return when (typeRefExp) { + is SymbolElement -> TypeRef(typeRefExp.textValue, Arity.Required, metas) + is SexpElement -> { + when (typeRefExp.tag) { + "?" -> { + requireArityForTag(typeRefExp, 1) + val typeName = typeRefExp.tail.head.symbolValue + TypeRef(typeName, Arity.Optional, metas) + } + "*" -> { + requireArityForTag(typeRefExp, IntRange(2, 3)) + val typeName = typeRefExp.tail.head.symbolValue + val arityRange = typeRefExp.tail.tail + val minArity = arityRange.head.longValue + TypeRef(typeName, Arity.Variadic(minArity.toInt()), metas) + } + else -> parseError(typeRefExp.head, ParserErrorContext.ExpectedTypeReferenceArityTag(typeRefExp.tag)) + } + } + else -> parseError(typeRefExp, ParserErrorContext.ExpectedSymbolOrSexp(typeRefExp.type)) } } - return PermutedDomain( - tag = domainName, - permutesDomain = permutingDomain, - excludedTypes = removedTypes, - includedTypes = newTypes, - permutedSums = permutedSums, - metas = sexp.metas) -} + private fun parsePermuteDomain(domainName: String, sexp: SexpElement): PermutedDomain { + val args = sexp.tail // Skip tag + + val permutingDomain = args.head.symbolValue + val removedTypes = mutableListOf() + val newTypes = mutableListOf() + val permutedSums = mutableListOf() + + val alterSexps = args.tail + alterSexps.map { it.asSexp() }.forEach { alterSexp -> + when(alterSexp.head.symbolValue) { + "with" -> permutedSums.add(parseWithSum(alterSexp)) + "exclude" -> alterSexp.tail.mapTo(removedTypes) { it.symbolValue } + "include" -> alterSexp.tail.mapTo(newTypes) { parseDomainLevelStatement(it.asSexp()) } + else -> parseError(alterSexp, ParserErrorContext.InvalidPermutedDomainTag(alterSexp.head.symbolValue)) + } + } -private fun parseWithSum(sexp: SexpElement): PermutedSum { - val args = sexp.tail // Skip tag + return PermutedDomain( + tag = domainName, + permutesDomain = permutingDomain, + excludedTypes = removedTypes, + includedTypes = newTypes, + permutedSums = permutedSums, + metas = sexp.metas.toSourceLocationMetas()) + } - val nameOfAlteredSum = args.head.symbolValue - val removedVariants = mutableListOf() - val addedVariants = mutableListOf() + private fun parseWithSum(sexp: SexpElement): PermutedSum { + val args = sexp.tail // Skip tag - args.tail.forEach { alterationValue -> - val alterationSexp = alterationValue.asSexp() - when (val alterationTag = alterationSexp.tag) { - "exclude" -> alterationSexp.tail.mapTo(removedVariants) { it.symbolValue } - "include" -> alterationSexp.tail.mapTo(addedVariants) { parseSumVariant(it.asSexp()) } - else -> parseError(alterationSexp, ParserErrorContext.InvalidWithSumTag(alterationTag)) + val nameOfAlteredSum = args.head.symbolValue + val removedVariants = mutableListOf() + val addedVariants = mutableListOf() + + args.tail.forEach { alterationValue -> + val alterationSexp = alterationValue.asSexp() + when (val alterationTag = alterationSexp.tag) { + "exclude" -> alterationSexp.tail.mapTo(removedVariants) { it.symbolValue } + "include" -> alterationSexp.tail.mapTo(addedVariants) { parseSumVariant(it.asSexp()) } + else -> parseError(alterationSexp, ParserErrorContext.InvalidWithSumTag(alterationTag)) + } } - } - return PermutedSum(nameOfAlteredSum, removedVariants, addedVariants, sexp.metas) -} + return PermutedSum(nameOfAlteredSum, removedVariants, addedVariants, sexp.metas.toSourceLocationMetas()) + } -private fun requireArityForTag(sexp: SexpElement, arity: Int) { - // Note: arity does not include the tag! - val argCount = sexp.values.size - 1 - if(argCount != arity) { - parseError(sexp, ParserErrorContext.InvalidArityForTag(IntRange(arity, arity), sexp.head.symbolValue, argCount)) + private fun requireArityForTag(sexp: SexpElement, arity: Int) { + // Note: arity does not include the tag! + val argCount = sexp.values.size - 1 + if(argCount != arity) { + parseError(sexp, ParserErrorContext.InvalidArityForTag(IntRange(arity, arity), sexp.head.symbolValue, argCount)) + } } -} -private fun requireArityForTag(sexp: SexpElement, arityRange: IntRange) { - // Note: arity does not include the tag! - val argCount = sexp.values.size - 1 - if(argCount !in arityRange) { - parseError(sexp, ParserErrorContext.InvalidArityForTag(arityRange, sexp.head.symbolValue, argCount)) + private fun requireArityForTag(sexp: SexpElement, arityRange: IntRange) { + // Note: arity does not include the tag! + val argCount = sexp.values.size - 1 + if(argCount !in arityRange) { + parseError(sexp, ParserErrorContext.InvalidArityForTag(arityRange, sexp.head.symbolValue, argCount)) + } } + } + diff --git a/pig/src/org/partiql/pig/errors/PigError.kt b/pig/src/org/partiql/pig/errors/PigError.kt index 8a96c1d..fdc775c 100644 --- a/pig/src/org/partiql/pig/errors/PigError.kt +++ b/pig/src/org/partiql/pig/errors/PigError.kt @@ -15,8 +15,7 @@ package org.partiql.pig.errors -import com.amazon.ionelement.api.IonLocation -import com.amazon.ionelement.api.locationToString +import org.partiql.pig.domain.parser.SourceLocation /** * [ErrorContext] instances provide information about an error message which can later be used @@ -29,7 +28,7 @@ interface ErrorContext { val message: String } -data class PigError(val location: IonLocation?, val context: ErrorContext) { - override fun toString(): String = "${locationToString(location)}: ${context.message}" +data class PigError(val location: SourceLocation?, val context: ErrorContext) { + override fun toString(): String = "${location?.toString() ?: ""}: ${context.message}" } diff --git a/pig/src/org/partiql/pig/main.kt b/pig/src/org/partiql/pig/main.kt index b0f6c9a..0a68584 100644 --- a/pig/src/org/partiql/pig/main.kt +++ b/pig/src/org/partiql/pig/main.kt @@ -21,7 +21,7 @@ import org.partiql.pig.cmdline.CommandLineParser import org.partiql.pig.cmdline.TargetLanguage import org.partiql.pig.errors.PigException import org.partiql.pig.domain.model.TypeUniverse -import org.partiql.pig.domain.parser.parseTypeUniverse +import org.partiql.pig.domain.parser.parseTypeUniverseFile import org.partiql.pig.generator.custom.applyCustomTemplate import org.partiql.pig.generator.html.applyHtmlTemplate import org.partiql.pig.generator.kotlin.applyKotlinTemplate @@ -65,9 +65,7 @@ fun generateCode(command: Command.Generate) { progress("output file : ${command.outputFile}") progress("parsing the universe...") - val typeUniverse: TypeUniverse = FileInputStream(command.typeUniverseFile).use { inputStream -> - IonReaderBuilder.standard().build(inputStream).use { ionReader -> parseTypeUniverse(ionReader) } - } + val typeUniverse: TypeUniverse = parseTypeUniverseFile(command.typeUniverseFile) progress("permuting domains...") diff --git a/pig/test-domains/domains/circular_universe_c.ion b/pig/test-domains/domains/circular_universe_c.ion new file mode 100644 index 0000000..3a3ec73 --- /dev/null +++ b/pig/test-domains/domains/circular_universe_c.ion @@ -0,0 +1,4 @@ + +(define domain_c (domain (product foo))) +// make a circular reference +(import "circular_universe_d.ion") diff --git a/pig/test-domains/domains/circular_universe_d.ion b/pig/test-domains/domains/circular_universe_d.ion new file mode 100644 index 0000000..298d7c0 --- /dev/null +++ b/pig/test-domains/domains/circular_universe_d.ion @@ -0,0 +1,4 @@ + +(define domain_d (domain (product foo))) +// make a circular reference +(import "circular_universe_c.ion") diff --git a/pig/test-domains/domains/import_b.ion b/pig/test-domains/domains/import_b.ion new file mode 100644 index 0000000..84d5782 --- /dev/null +++ b/pig/test-domains/domains/import_b.ion @@ -0,0 +1,4 @@ + +// import file with a relative path +(import "../other/universe_b.ion") + diff --git a/pig/test-domains/domains/universe_a.ion b/pig/test-domains/domains/universe_a.ion new file mode 100644 index 0000000..847e08b --- /dev/null +++ b/pig/test-domains/domains/universe_a.ion @@ -0,0 +1,3 @@ + +(define domain_a (domain (product foo))) +(import "import_b.ion") diff --git a/pig/test-domains/main.ion b/pig/test-domains/main.ion new file mode 100644 index 0000000..e1fea21 --- /dev/null +++ b/pig/test-domains/main.ion @@ -0,0 +1,4 @@ + +(import "domains/universe_a.ion") +(import "domains/circular_universe_c.ion") + diff --git a/pig/test-domains/other/universe_b.ion b/pig/test-domains/other/universe_b.ion new file mode 100644 index 0000000..a8487bd --- /dev/null +++ b/pig/test-domains/other/universe_b.ion @@ -0,0 +1,3 @@ + +(define domain_b (domain (product foo))) + diff --git a/pig/test/org/partiql/pig/cmdline/CommandLineParserTests.kt b/pig/test/org/partiql/pig/cmdline/CommandLineParserTests.kt index 1bb6362..142f032 100644 --- a/pig/test/org/partiql/pig/cmdline/CommandLineParserTests.kt +++ b/pig/test/org/partiql/pig/cmdline/CommandLineParserTests.kt @@ -67,12 +67,12 @@ class CommandLineParserTests { //////////////////////////////////////////////////////// // long parameter names TestCase( - Command.Generate(File("input.ion"), File("output.kt"), TargetLanguage.Kotlin("some.package")), + Command.Generate("input.ion", "output.kt", TargetLanguage.Kotlin("some.package")), "--universe=input.ion", "--target=kotlin", "--output=output.kt", "--namespace=some.package"), // short parameter names TestCase( - Command.Generate(File("input.ion"), File("output.kt"), TargetLanguage.Kotlin("some.package")), + Command.Generate("input.ion", "output.kt", TargetLanguage.Kotlin("some.package")), "-u=input.ion", "-t=kotlin", "-o=output.kt", "-n=some.package"), // missing the --namespace argument @@ -85,12 +85,12 @@ class CommandLineParserTests { //////////////////////////////////////////////////////// // long parameter names TestCase( - Command.Generate(File("input.ion"), File("output.html"), TargetLanguage.Html), + Command.Generate("input.ion", "output.html", TargetLanguage.Html), "--universe=input.ion", "--target=html", "--output=output.html"), // short parameter names TestCase( - Command.Generate(File("input.ion"), File("output.html"), TargetLanguage.Html), + Command.Generate("input.ion", "output.html", TargetLanguage.Html), "-u=input.ion", "-target=html", "--output=output.html"), //////////////////////////////////////////////////////// @@ -98,12 +98,12 @@ class CommandLineParserTests { //////////////////////////////////////////////////////// // long parameter names TestCase( - Command.Generate(File("input.ion"), File("output.txt"), TargetLanguage.Custom(File("template.ftl"))), + Command.Generate("input.ion", "output.txt", TargetLanguage.Custom(File("template.ftl"))), "--universe=input.ion", "--target=custom", "--output=output.txt", "--template=template.ftl"), // short parameter names TestCase( - Command.Generate(File("input.ion"), File("output.txt"), TargetLanguage.Custom(File("template.ftl"))), + Command.Generate("input.ion", "output.txt", TargetLanguage.Custom(File("template.ftl"))), "-u=input.ion", "-t=custom", "-o=output.txt", "-e=template.ftl"), // missing the --template argument diff --git a/pig/test/org/partiql/pig/domain/PermuteDomainTests.kt b/pig/test/org/partiql/pig/domain/PermuteDomainTests.kt index e4c34ac..8c46729 100644 --- a/pig/test/org/partiql/pig/domain/PermuteDomainTests.kt +++ b/pig/test/org/partiql/pig/domain/PermuteDomainTests.kt @@ -15,14 +15,13 @@ package org.partiql.pig.domain -import com.amazon.ion.system.IonReaderBuilder import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.partiql.pig.domain.model.Arity import org.partiql.pig.domain.model.DataType import org.partiql.pig.domain.model.TypeUniverse -import org.partiql.pig.domain.parser.parseTypeUniverse +import org.partiql.pig.util.parseTypeUniverseInString class PermuteDomainTests { /** @@ -59,7 +58,7 @@ class PermuteDomainTests { (e b::symbol))))) """ - val td: TypeUniverse = IonReaderBuilder.standard().build(typeUniverseWithExtensions).use { parseTypeUniverse(it) } + val td: TypeUniverse = parseTypeUniverseInString(typeUniverseWithExtensions) val concretes = td.computeTypeDomains() diff --git a/pig/test/org/partiql/pig/domain/TypeDomainParserErrorsTest.kt b/pig/test/org/partiql/pig/domain/TypeDomainParserErrorsTest.kt index 0bb3135..9bcc5c2 100644 --- a/pig/test/org/partiql/pig/domain/TypeDomainParserErrorsTest.kt +++ b/pig/test/org/partiql/pig/domain/TypeDomainParserErrorsTest.kt @@ -23,9 +23,9 @@ import org.junit.jupiter.api.assertThrows import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource import org.partiql.pig.domain.parser.ParserErrorContext -import org.partiql.pig.domain.parser.parseTypeUniverse import org.partiql.pig.errors.PigError import org.partiql.pig.errors.PigException +import org.partiql.pig.util.parseTypeUniverseInString class TypeDomainParserErrorsTest { @@ -35,7 +35,7 @@ class TypeDomainParserErrorsTest { @MethodSource("parametersForErrorsTest") fun errorsTest(tc: TestCase) { val ex = assertThrows { - val oops = parseTypeUniverse(tc.typeUniverseText) + val oops = parseTypeUniverseInString(tc.typeUniverseText) println("this was erroneously parsed: ${oops.toIonElement()}") } assertEquals(tc.expectedError, ex.error) diff --git a/pig/test/org/partiql/pig/domain/TypeDomainParserImportTests.kt b/pig/test/org/partiql/pig/domain/TypeDomainParserImportTests.kt new file mode 100644 index 0000000..308fb20 --- /dev/null +++ b/pig/test/org/partiql/pig/domain/TypeDomainParserImportTests.kt @@ -0,0 +1,40 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.partiql.pig.domain + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.partiql.pig.domain.model.TypeDomain +import org.partiql.pig.domain.parser.parseTypeUniverseFile +import kotlin.test.assertTrue + +class TypeDomainParserImportTests { + + @Test + fun testImport() { + val universe = parseTypeUniverseFile("test-domains/main.ion") + println(universe.toIonElement()) + + val allDomains = universe.statements.filterIsInstance() + + // If 4 domains are loaded correctly, then we deal with relative paths and circular references correctly. + Assertions.assertEquals(4, allDomains.size) + assertTrue(allDomains.any { it.tag == "domain_a" }) + assertTrue(allDomains.any { it.tag == "domain_b" }) + assertTrue(allDomains.any { it.tag == "domain_c" }) + assertTrue(allDomains.any { it.tag == "domain_d" }) + } +} diff --git a/pig/test/org/partiql/pig/domain/TypeDomainParserTests.kt b/pig/test/org/partiql/pig/domain/TypeDomainParserTests.kt index 3ae4863..1b79e4a 100644 --- a/pig/test/org/partiql/pig/domain/TypeDomainParserTests.kt +++ b/pig/test/org/partiql/pig/domain/TypeDomainParserTests.kt @@ -15,7 +15,6 @@ package org.partiql.pig.domain -import com.amazon.ion.system.IonReaderBuilder import com.amazon.ionelement.api.IonElementLoaderOptions import com.amazon.ionelement.api.createIonElementLoader import com.amazon.ionelement.api.ionSexpOf @@ -23,7 +22,9 @@ import com.amazon.ionelement.api.ionSymbol import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow -import org.partiql.pig.domain.parser.parseTypeUniverse +import org.partiql.pig.domain.model.TypeDomain +import org.partiql.pig.util.parseTypeUniverseInString +import kotlin.test.assertTrue class TypeDomainParserTests { private val loader = createIonElementLoader(IonElementLoaderOptions(includeLocationMeta = true)) @@ -36,6 +37,7 @@ class TypeDomainParserTests { (product foo a::string b::(* int 2)) (product bar a::bat b::(? baz) c::(* blargh 10)))) """) + @Test fun testTransform() = runTestCase("(transform domain_a domain_b)") @@ -88,10 +90,9 @@ class TypeDomainParserTests { val expected = assertDoesNotThrow("loading the expected type universe") { loader.loadSingleElement(tc) } + val parsed = assertDoesNotThrow("parsing type universe") { - IonReaderBuilder.standard().build(tc).use { - parseTypeUniverse(it) - } + parseTypeUniverseInString(tc) } assertEquals( diff --git a/pig/test/org/partiql/pig/domain/TypeDomainSemanticCheckerTests.kt b/pig/test/org/partiql/pig/domain/TypeDomainSemanticCheckerTests.kt index 2aeb03b..99d98ae 100644 --- a/pig/test/org/partiql/pig/domain/TypeDomainSemanticCheckerTests.kt +++ b/pig/test/org/partiql/pig/domain/TypeDomainSemanticCheckerTests.kt @@ -20,9 +20,9 @@ import org.junit.jupiter.api.assertThrows import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource import org.partiql.pig.domain.model.SemanticErrorContext -import org.partiql.pig.domain.parser.parseTypeUniverse import org.partiql.pig.errors.PigError import org.partiql.pig.errors.PigException +import org.partiql.pig.util.parseTypeUniverseInString class TypeDomainSemanticCheckerTests { @@ -43,7 +43,7 @@ class TypeDomainSemanticCheckerTests { fun nameErrorsTest2(tc: TestCase) = runTest(tc) private fun runTest(tc: TestCase) { - val u = parseTypeUniverse(tc.typeUniverseText) + val u = parseTypeUniverseInString(tc.typeUniverseText) val ex = assertThrows { u.computeTypeDomains() } assertEquals(tc.expectedError, ex.error) } diff --git a/pig/test/org/partiql/pig/domain/Util.kt b/pig/test/org/partiql/pig/domain/Util.kt index 6bff621..711d04e 100644 --- a/pig/test/org/partiql/pig/domain/Util.kt +++ b/pig/test/org/partiql/pig/domain/Util.kt @@ -29,11 +29,12 @@ import org.partiql.pig.domain.model.Transform import org.partiql.pig.domain.model.TupleType import org.partiql.pig.domain.model.TypeDomain import org.partiql.pig.domain.model.TypeUniverse +import org.partiql.pig.domain.parser.SourceLocation import org.partiql.pig.errors.ErrorContext import org.partiql.pig.errors.PigError fun makeErr(line: Int, col: Int, errorContext: ErrorContext) = - PigError(IonTextLocation(line.toLong(), col.toLong()), errorContext) + PigError(SourceLocation("root.ion", IonTextLocation(line.toLong(), col.toLong())), errorContext) fun makeErr(errorContext: ErrorContext) = PigError(null, errorContext) diff --git a/pig/test/org/partiql/pig/util/ParseHelpers.kt b/pig/test/org/partiql/pig/util/ParseHelpers.kt new file mode 100644 index 0000000..55a0dbe --- /dev/null +++ b/pig/test/org/partiql/pig/util/ParseHelpers.kt @@ -0,0 +1,50 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.partiql.pig.util + +import org.partiql.pig.domain.model.TypeUniverse +import org.partiql.pig.domain.parser.InputSource +import org.partiql.pig.domain.parser.Parser +import java.io.ByteArrayInputStream +import java.io.FileNotFoundException +import java.io.InputStream + +/** + * For testing purposes, parses the type universe specified in [topUnvierseText]. + * + * [includes] is a map keyed by "fake" filename that will be used instead of a real-file system for looking + * up the content of imported files. [includes] must not contain any filename by the name of "root.ion", which + * is the name given to the type universe specified in [topUnvierseText]. + */ +fun parseTypeUniverseInString(topUnvierseText: String, includes: Map = emptyMap()): TypeUniverse { + assert(!includes.containsKey("root.ion")) + val allIncludes = mapOf("root.ion" to topUnvierseText) + includes + val parser = Parser(StringSource(allIncludes)) + return parser.parseTypeUniverse("root.ion") +} + +class StringSource(val sources: Map) : InputSource { + override fun openStream(sourceName: String): InputStream { + val text: String = sources[sourceName] ?: throw FileNotFoundException("$sourceName does not exist") + + return ByteArrayInputStream(text.toByteArray(Charsets.UTF_8)) + } + + override fun getCanonicalName(sourceName: String): String { + TODO("not implemented") + } +} +