├── project ├── build.properties └── plugins.sbt ├── tests └── src │ ├── test │ ├── resources │ │ └── binary │ │ │ ├── anon_var.bin │ │ │ ├── builtin.bin │ │ │ ├── pi.bin │ │ │ ├── app.bin │ │ │ ├── int.bin │ │ │ ├── let.bin │ │ │ ├── list.bin │ │ │ ├── nat.bin │ │ │ ├── some.bin │ │ │ ├── true.bin │ │ │ ├── var.bin │ │ │ ├── anon_pi.bin │ │ │ ├── assert.bin │ │ │ ├── false.bin │ │ │ ├── lambda.bin │ │ │ ├── merge.bin │ │ │ ├── to_map.bin │ │ │ ├── union.bin │ │ │ ├── annotation.bin │ │ │ ├── anon_lambda.bin │ │ │ ├── empty_list.bin │ │ │ ├── env_import.bin │ │ │ ├── field_access.bin │ │ │ ├── let_multiple.bin │ │ │ ├── local_import.bin │ │ │ ├── record_type.bin │ │ │ ├── record_literal.bin │ │ │ ├── remote_import.bin │ │ │ ├── local_import_text.bin │ │ │ ├── record_projection.bin │ │ │ ├── local_import_location.bin │ │ │ └── record_type_projection.bin │ └── scala │ │ └── org │ │ └── dhallj │ │ └── tests │ │ ├── ToStringSuite.scala │ │ ├── ImportResolutionSuite.scala │ │ ├── MiscSuite.scala │ │ ├── PreludeSuite.scala │ │ ├── JsonConverterSuite.scala │ │ └── acceptance │ │ └── AcceptanceSuites.scala │ └── main │ └── scala │ └── org │ └── dhallj │ └── tests │ ├── HaskellDhall.scala │ └── acceptance │ ├── AcceptanceFailureSuite.scala │ ├── AcceptanceSuite.scala │ ├── ImportResolutionSuite.scala │ └── AcceptanceSuccessSuite.scala ├── version.sbt ├── modules ├── imports │ ├── src │ │ ├── test │ │ │ ├── resources │ │ │ │ ├── local │ │ │ │ │ └── package.dhall │ │ │ │ ├── alternate │ │ │ │ │ ├── other.dhall │ │ │ │ │ └── package.dhall │ │ │ │ ├── hashed │ │ │ │ │ └── package.dhall │ │ │ │ ├── cache-write │ │ │ │ │ └── package.dhall │ │ │ │ ├── multiple-imports │ │ │ │ │ ├── other.dhall │ │ │ │ │ ├── other2.dhall │ │ │ │ │ └── package.dhall │ │ │ │ ├── text-import │ │ │ │ │ └── package.dhall │ │ │ │ ├── cyclic │ │ │ │ │ ├── package.dhall │ │ │ │ │ └── other.dhall │ │ │ │ ├── local-local-relative │ │ │ │ │ ├── other.dhall │ │ │ │ │ └── package.dhall │ │ │ │ ├── local-local-absolute-2 │ │ │ │ │ └── package.dhall │ │ │ │ ├── cyclic-relative-paths │ │ │ │ │ ├── other.dhall │ │ │ │ │ └── package.dhall │ │ │ │ ├── local-local-absolute │ │ │ │ │ └── package.dhall │ │ │ │ └── local-remote │ │ │ │ │ └── package.dhall │ │ │ └── scala │ │ │ │ └── org │ │ │ │ └── dhallj │ │ │ │ └── imports │ │ │ │ ├── ImportCacheSuite.scala │ │ │ │ ├── ReferentialSanityCheckSuite.scala │ │ │ │ ├── ToHeadersSuite.scala │ │ │ │ └── CorsComplianceCheckSuite.scala │ │ └── main │ │ │ └── scala │ │ │ └── org │ │ │ └── dhallj │ │ │ └── imports │ │ │ ├── syntax │ │ │ └── package.scala │ │ │ ├── ImportContext.scala │ │ │ ├── ReferentialSanityCheck.scala │ │ │ ├── ResolveImports.scala │ │ │ ├── CorsComplianceCheck.scala │ │ │ ├── ToHeaders.scala │ │ │ ├── ImportCache.scala │ │ │ └── Canonicalization.scala │ └── README.md ├── testing │ └── src │ │ └── main │ │ └── scala │ │ └── org │ │ └── dhallj │ │ └── testing │ │ ├── WellTypedExpr.scala │ │ └── package.scala ├── core │ ├── src │ │ ├── main │ │ │ └── java │ │ │ │ └── org │ │ │ │ └── dhallj │ │ │ │ ├── cbor │ │ │ │ ├── CborException.java │ │ │ │ ├── AdditionalInfo.java │ │ │ │ ├── Visitor.java │ │ │ │ ├── MajorType.java │ │ │ │ ├── NullVisitor.java │ │ │ │ └── HalfFloat.java │ │ │ │ └── core │ │ │ │ ├── binary │ │ │ │ ├── DecodingException.java │ │ │ │ ├── Decode.java │ │ │ │ └── Label.java │ │ │ │ ├── typechecking │ │ │ │ ├── NonNegativeIndices.java │ │ │ │ ├── CheckEquivalence.java │ │ │ │ ├── TypeCheckApplication.java │ │ │ │ ├── Universe.java │ │ │ │ ├── Context.java │ │ │ │ └── BuiltInTypes.java │ │ │ │ ├── normalization │ │ │ │ ├── BetaNormalizeIf.java │ │ │ │ ├── NormalizationUtils.java │ │ │ │ ├── Shift.java │ │ │ │ ├── BetaNormalizeToMap.java │ │ │ │ ├── Substitute.java │ │ │ │ ├── BetaNormalizeMerge.java │ │ │ │ ├── BetaNormalizeWith.java │ │ │ │ ├── BetaNormalizeTextLiteral.java │ │ │ │ ├── BetaNormalizeProjection.java │ │ │ │ ├── AlphaNormalize.java │ │ │ │ ├── BetaNormalizeFieldAccess.java │ │ │ │ └── BetaNormalize.java │ │ │ │ ├── IsResolved.java │ │ │ │ ├── ArrayIterable.java │ │ │ │ ├── DhallException.java │ │ │ │ ├── Tags.java │ │ │ │ ├── Source.java │ │ │ │ ├── Operator.java │ │ │ │ └── converters │ │ │ │ ├── JsonHandler.java │ │ │ │ └── JsonConverter.java │ │ └── test │ │ │ └── java │ │ │ └── org │ │ │ └── dhallj │ │ │ └── cbor │ │ │ ├── HalfFloatSuite.scala │ │ │ └── CborSuite.scala │ └── BUILD ├── parser │ ├── src │ │ ├── main │ │ │ └── java │ │ │ │ └── org │ │ │ │ └── dhallj │ │ │ │ └── parser │ │ │ │ ├── support │ │ │ │ ├── package-info.java │ │ │ │ ├── LetBinding.java │ │ │ │ ├── Parser.java │ │ │ │ ├── Comment.java │ │ │ │ └── OperatorPrecedenceTable.java │ │ │ │ └── DhallParser.java │ │ └── test │ │ │ └── scala │ │ │ └── org │ │ │ └── dhallj │ │ │ └── parser │ │ │ └── DhallParserSuite.scala │ └── BUILD ├── javagen │ └── src │ │ └── main │ │ └── java │ │ └── org │ │ └── dhallj │ │ └── javagen │ │ ├── package.scala │ │ └── Code.scala ├── scala-codec │ └── src │ │ └── main │ │ └── scala │ │ └── org │ │ └── dhallj │ │ └── codec │ │ ├── DecodingFailure.scala │ │ ├── syntax │ │ └── package.scala │ │ └── Encoder.scala ├── jawn │ └── src │ │ ├── main │ │ └── scala │ │ │ └── org │ │ │ └── dhallj │ │ │ └── jawn │ │ │ ├── JawnConverter.scala │ │ │ └── FacadeHandler.scala │ │ └── test │ │ └── scala │ │ └── org │ │ └── dhallj │ │ └── jawn │ │ └── JawnConverterSuite.scala ├── cats │ └── src │ │ └── test │ │ └── scala │ │ └── org │ │ └── dhallj │ │ └── cats │ │ └── LiftVisitorSuite.scala ├── circe │ └── src │ │ ├── test │ │ └── scala │ │ │ └── org │ │ │ └── dhallj │ │ │ └── circe │ │ │ ├── JsonCleaner.scala │ │ │ └── CirceConverterSuite.scala │ │ └── main │ │ └── scala │ │ └── org │ │ └── dhallj │ │ └── circe │ │ ├── Converter.scala │ │ └── CirceHandler.scala ├── yaml │ └── src │ │ ├── main │ │ └── java │ │ │ └── org │ │ │ └── dhallj │ │ │ └── yaml │ │ │ ├── YamlConverter.java │ │ │ ├── YamlContext.java │ │ │ └── YamlHandler.java │ │ └── test │ │ └── scala │ │ └── org │ │ └── dhallj │ │ └── yaml │ │ └── YamlConverterSuite.scala ├── scala │ └── src │ │ └── main │ │ └── scala │ │ └── org │ │ └── dhallj │ │ └── syntax │ │ └── package.scala └── imports-mini │ └── src │ └── main │ └── java │ └── org │ └── dhallj │ └── imports │ └── mini │ └── Resolver.java ├── .gitmodules ├── javascript ├── jre │ ├── URISyntaxException.java │ ├── InvalidPathException.java │ ├── BufferedReader.java │ ├── Paths.java │ ├── InputStreamReader.java │ ├── URI.java │ ├── Path.java │ └── BUILD ├── BUILD └── api │ ├── BUILD │ ├── DhallJs.java │ └── dhall.js ├── .gitignore ├── .scalafmt.conf ├── WORKSPACE ├── benchmarks └── src │ └── main │ └── scala │ └── org │ └── dhallj │ └── benchmarks │ ├── ParsingBenchmark.scala │ └── EncodingBenchmark.scala ├── LICENSE ├── cli └── src │ └── main │ └── java │ └── org │ └── dhallj │ └── cli │ └── Dhall.java ├── .github └── workflows │ ├── ci.yml │ └── clean.yml └── scalastyle-config.xml /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.4.7 2 | -------------------------------------------------------------------------------- /tests/src/test/resources/binary/anon_var.bin: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tests/src/test/resources/binary/builtin.bin: -------------------------------------------------------------------------------- 1 | lNatural/even -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | version in ThisBuild := "0.8.0-SNAPSHOT" 2 | -------------------------------------------------------------------------------- /modules/imports/src/test/resources/local/package.dhall: -------------------------------------------------------------------------------- 1 | let x = 1 in x -------------------------------------------------------------------------------- /modules/imports/src/test/resources/alternate/other.dhall: -------------------------------------------------------------------------------- 1 | let x = 2 in x -------------------------------------------------------------------------------- /modules/imports/src/test/resources/alternate/package.dhall: -------------------------------------------------------------------------------- 1 | let x = 1 in x -------------------------------------------------------------------------------- /modules/imports/src/test/resources/hashed/package.dhall: -------------------------------------------------------------------------------- 1 | let x = 1 in x -------------------------------------------------------------------------------- /modules/imports/src/test/resources/cache-write/package.dhall: -------------------------------------------------------------------------------- 1 | let x = 2 in x -------------------------------------------------------------------------------- /modules/imports/src/test/resources/multiple-imports/other.dhall: -------------------------------------------------------------------------------- 1 | let x = 1 in x -------------------------------------------------------------------------------- /modules/imports/src/test/resources/multiple-imports/other2.dhall: -------------------------------------------------------------------------------- 1 | let x = 2 in x -------------------------------------------------------------------------------- /modules/imports/src/test/resources/text-import/package.dhall: -------------------------------------------------------------------------------- 1 | let x = 1 in x -------------------------------------------------------------------------------- /modules/imports/src/test/resources/cyclic/package.dhall: -------------------------------------------------------------------------------- 1 | let x = ./other.dhall in x -------------------------------------------------------------------------------- /modules/imports/src/test/resources/local-local-relative/other.dhall: -------------------------------------------------------------------------------- 1 | let x = 1 in x -------------------------------------------------------------------------------- /modules/imports/src/test/resources/cyclic/other.dhall: -------------------------------------------------------------------------------- 1 | let x = /cyclic/package.dhall in x -------------------------------------------------------------------------------- /modules/imports/src/test/resources/local-local-absolute-2/package.dhall: -------------------------------------------------------------------------------- 1 | let x = 1 in x -------------------------------------------------------------------------------- /modules/imports/src/test/resources/cyclic-relative-paths/other.dhall: -------------------------------------------------------------------------------- 1 | let x = ./package.dhall in x -------------------------------------------------------------------------------- /modules/imports/src/test/resources/cyclic-relative-paths/package.dhall: -------------------------------------------------------------------------------- 1 | let x = ./other.dhall in x -------------------------------------------------------------------------------- /modules/imports/src/test/resources/local-local-relative/package.dhall: -------------------------------------------------------------------------------- 1 | let x = ./other.dhall in x -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "dhall-lang"] 2 | path = dhall-lang 3 | url = https://github.com/dhall-lang/dhall-lang.git 4 | -------------------------------------------------------------------------------- /javascript/jre/URISyntaxException.java: -------------------------------------------------------------------------------- 1 | package java.net; 2 | 3 | public class URISyntaxException extends Throwable {} 4 | -------------------------------------------------------------------------------- /modules/imports/src/test/resources/local-local-absolute/package.dhall: -------------------------------------------------------------------------------- 1 | let x = /local-local-absolute-2/package.dhall in x -------------------------------------------------------------------------------- /tests/src/test/resources/binary/pi.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/pi.bin -------------------------------------------------------------------------------- /tests/src/test/resources/binary/app.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/app.bin -------------------------------------------------------------------------------- /tests/src/test/resources/binary/int.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/int.bin -------------------------------------------------------------------------------- /tests/src/test/resources/binary/let.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/let.bin -------------------------------------------------------------------------------- /tests/src/test/resources/binary/list.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/list.bin -------------------------------------------------------------------------------- /tests/src/test/resources/binary/nat.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/nat.bin -------------------------------------------------------------------------------- /tests/src/test/resources/binary/some.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/some.bin -------------------------------------------------------------------------------- /tests/src/test/resources/binary/true.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/true.bin -------------------------------------------------------------------------------- /tests/src/test/resources/binary/var.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/var.bin -------------------------------------------------------------------------------- /tests/src/test/resources/binary/anon_pi.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/anon_pi.bin -------------------------------------------------------------------------------- /tests/src/test/resources/binary/assert.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/assert.bin -------------------------------------------------------------------------------- /tests/src/test/resources/binary/false.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/false.bin -------------------------------------------------------------------------------- /tests/src/test/resources/binary/lambda.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/lambda.bin -------------------------------------------------------------------------------- /tests/src/test/resources/binary/merge.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/merge.bin -------------------------------------------------------------------------------- /tests/src/test/resources/binary/to_map.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/to_map.bin -------------------------------------------------------------------------------- /tests/src/test/resources/binary/union.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/union.bin -------------------------------------------------------------------------------- /modules/imports/src/test/resources/multiple-imports/package.dhall: -------------------------------------------------------------------------------- 1 | let x = ./other.dhall 2 | 3 | let y = ./other2.dhall 4 | 5 | in [x,y] -------------------------------------------------------------------------------- /tests/src/test/resources/binary/annotation.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/annotation.bin -------------------------------------------------------------------------------- /tests/src/test/resources/binary/anon_lambda.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/anon_lambda.bin -------------------------------------------------------------------------------- /tests/src/test/resources/binary/empty_list.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/empty_list.bin -------------------------------------------------------------------------------- /tests/src/test/resources/binary/env_import.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/env_import.bin -------------------------------------------------------------------------------- /tests/src/test/resources/binary/field_access.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/field_access.bin -------------------------------------------------------------------------------- /tests/src/test/resources/binary/let_multiple.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/let_multiple.bin -------------------------------------------------------------------------------- /tests/src/test/resources/binary/local_import.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/local_import.bin -------------------------------------------------------------------------------- /tests/src/test/resources/binary/record_type.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/record_type.bin -------------------------------------------------------------------------------- /javascript/jre/InvalidPathException.java: -------------------------------------------------------------------------------- 1 | package java.nio.file; 2 | 3 | public class InvalidPathException extends IllegalArgumentException {} 4 | -------------------------------------------------------------------------------- /modules/imports/README.md: -------------------------------------------------------------------------------- 1 | # Imports 2 | 3 | A reference implementation of import resolution using [Cats Effect](https://typelevel.org/cats-effect/). -------------------------------------------------------------------------------- /tests/src/test/resources/binary/record_literal.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/record_literal.bin -------------------------------------------------------------------------------- /tests/src/test/resources/binary/remote_import.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/remote_import.bin -------------------------------------------------------------------------------- /tests/src/test/resources/binary/local_import_text.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/local_import_text.bin -------------------------------------------------------------------------------- /tests/src/test/resources/binary/record_projection.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/record_projection.bin -------------------------------------------------------------------------------- /modules/imports/src/test/resources/local-remote/package.dhall: -------------------------------------------------------------------------------- 1 | let any = https://raw.githubusercontent.com/dhall-lang/dhall-lang/master/Prelude/List/any in any -------------------------------------------------------------------------------- /tests/src/test/resources/binary/local_import_location.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/local_import_location.bin -------------------------------------------------------------------------------- /tests/src/test/resources/binary/record_type_projection.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/note/dhallj/main/tests/src/test/resources/binary/record_type_projection.bin -------------------------------------------------------------------------------- /modules/testing/src/main/scala/org/dhallj/testing/WellTypedExpr.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.testing 2 | 3 | import org.dhallj.core.Expr 4 | 5 | case class WellTypedExpr(value: Expr) 6 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/cbor/CborException.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.cbor; 2 | 3 | public class CborException extends RuntimeException { 4 | CborException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /modules/parser/src/main/java/org/dhallj/parser/support/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Support classes generated by JavaCC. 3 | * 4 | *

Please do not use the contents of this package directly! 5 | */ 6 | package org.dhallj.parser.support; 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | target/ 3 | .idea/ 4 | .idea_modules/ 5 | .DS_STORE 6 | .cache 7 | .settings 8 | .project 9 | .classpath 10 | tmp/ 11 | .bloop/ 12 | .metals/ 13 | .bsp/ 14 | project/metals.sbt 15 | bazel-bin 16 | bazel-dhallj 17 | bazel-out 18 | bazel-testlogs 19 | -------------------------------------------------------------------------------- /modules/testing/src/main/scala/org/dhallj/testing/package.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.testing 2 | 3 | import org.scalacheck.Gen 4 | 5 | package object instances extends ArbitraryInstances { 6 | def genNameString: Gen[String] = Gen.alphaStr 7 | def genTextString: Gen[String] = Gen.alphaStr 8 | } 9 | -------------------------------------------------------------------------------- /javascript/jre/BufferedReader.java: -------------------------------------------------------------------------------- 1 | package java.io; 2 | 3 | public class BufferedReader extends Reader { 4 | public BufferedReader(InputStreamReader stream) {} 5 | 6 | public int read(char[] cbuf, int off, int len) { 7 | return -1; 8 | } 9 | 10 | public void close() {} 11 | } 12 | -------------------------------------------------------------------------------- /modules/imports/src/main/scala/org/dhallj/imports/syntax/package.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.imports 2 | 3 | import org.dhallj.core.Expr 4 | import scala.language.implicitConversions 5 | 6 | package object syntax { 7 | implicit def toResolveImportOps(expr: Expr): Resolver.Ops = new Resolver.Ops(expr) 8 | } 9 | -------------------------------------------------------------------------------- /javascript/jre/Paths.java: -------------------------------------------------------------------------------- 1 | package java.nio.file; 2 | 3 | public class Paths { 4 | /** 5 | * @throws InvalidPathException 6 | * if the path string cannot be converted to a {@code Path} 7 | */ 8 | public static final Path get(String input) { 9 | return new Path(input); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /modules/javagen/src/main/java/org/dhallj/javagen/package.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj 2 | 3 | import org.dhallj.core.Expr 4 | 5 | package object javagen { 6 | def toJavaCode(expr: Expr, packageName: String, className: String): String = 7 | expr.accept(ToCodeVisitor.instance).toClassDef(packageName, className) 8 | } 9 | -------------------------------------------------------------------------------- /javascript/BUILD: -------------------------------------------------------------------------------- 1 | load("@com_google_j2cl//build_defs:rules.bzl", "j2cl_application") 2 | 3 | j2cl_application( 4 | name = "dhall", 5 | closure_defines = {"jre.classMetadata": "'STRIPPED'"}, 6 | entry_points = ["dhall.js"], 7 | jre_checks_check_level = "MINIMAL", 8 | deps = ["//javascript/api:dhall_js"], 9 | ) 10 | -------------------------------------------------------------------------------- /modules/scala-codec/src/main/scala/org/dhallj/codec/DecodingFailure.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.codec 2 | 3 | import org.dhallj.core.{DhallException, Expr} 4 | 5 | class DecodingFailure(val target: String, val value: Expr) extends DhallException(s"Error decoding $target") { 6 | final override def fillInStackTrace(): Throwable = this 7 | } 8 | -------------------------------------------------------------------------------- /javascript/jre/InputStreamReader.java: -------------------------------------------------------------------------------- 1 | package java.io; 2 | 3 | public class InputStreamReader extends Reader { 4 | public InputStreamReader(InputStream in) {} 5 | public InputStreamReader(InputStream in, String charsetName) {} 6 | public int read(char[] cbuf, int off, int len) { 7 | return -1; 8 | } 9 | 10 | public void close() {} 11 | } 12 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version=2.7.5 2 | align.openParenCallSite = true 3 | align.openParenDefnSite = true 4 | maxColumn = 120 5 | continuationIndent.defnSite = 2 6 | assumeStandardLibraryStripMargin = true 7 | danglingParentheses.preset = true 8 | rewrite.rules = [AvoidInfix, SortImports, RedundantParens, SortModifiers] 9 | docstrings = JavaDoc 10 | newlines.alwaysBeforeMultilineDef = false 11 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/core/binary/DecodingException.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.core.binary; 2 | 3 | import org.dhallj.core.DhallException; 4 | 5 | public class DecodingException extends DhallException { 6 | public DecodingException(String message) { 7 | super(message); 8 | } 9 | 10 | public DecodingException(String message, Throwable cause) { 11 | super(message, cause); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/core/typechecking/NonNegativeIndices.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.core.typechecking; 2 | 3 | import org.dhallj.core.Expr; 4 | import org.dhallj.core.Visitor; 5 | 6 | final class NonNegativeIndices extends Visitor.Property { 7 | public static final Visitor instance = new NonNegativeIndices(); 8 | 9 | @Override 10 | public Boolean onIdentifier(Expr self, String name, long index) { 11 | return index >= 0; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /modules/scala-codec/src/main/scala/org/dhallj/codec/syntax/package.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.codec 2 | 3 | import org.dhallj.core.Expr 4 | 5 | package object syntax { 6 | implicit final class DhallCodecAnyOps[A](val value: A) extends AnyVal { 7 | def asExpr(implicit A: Encoder[A]): Expr = A.encode(value) 8 | } 9 | 10 | implicit final class DhallCodecExprOps(val expr: Expr) extends AnyVal { 11 | def as[A: Decoder]: Decoder.Result[A] = Decoder[A].decode(expr) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /modules/jawn/src/main/scala/org/dhallj/jawn/JawnConverter.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.jawn 2 | 3 | import org.dhallj.core.Expr 4 | import org.dhallj.core.converters.JsonConverter 5 | import org.typelevel.jawn.Facade 6 | 7 | class JawnConverter[J](facade: Facade[J]) { 8 | def apply(expr: Expr): Option[J] = { 9 | val handler = new FacadeHandler(facade) 10 | val wasConverted = expr.accept(new JsonConverter(handler)) 11 | 12 | if (wasConverted) Some(handler.result) else None 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /javascript/jre/URI.java: -------------------------------------------------------------------------------- 1 | package java.net; 2 | 3 | public class URI { 4 | private final String input; 5 | 6 | public URI(String input) throws URISyntaxException { 7 | this.input = input; 8 | } 9 | 10 | public final String getScheme() { 11 | return ""; 12 | } 13 | 14 | public final String getAuthority() { 15 | return ""; 16 | } 17 | 18 | public final String getPath() { 19 | return ""; 20 | } 21 | 22 | public final String getQuery() { 23 | return ""; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/core/binary/Decode.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.core.binary; 2 | 3 | import org.dhallj.cbor.Reader; 4 | import org.dhallj.core.Expr; 5 | 6 | public class Decode { 7 | public static final Expr decode(byte[] bytes) { 8 | Reader reader = new Reader.ByteArrayReader(bytes); 9 | // TODO check: if identifier then must be builtin using Expr.Constants.isBuiltInConstant 10 | Expr e = reader.nextSymbol(new CborDecodingVisitor(reader)); 11 | return e; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /WORKSPACE: -------------------------------------------------------------------------------- 1 | workspace(name = "org_dhallj") 2 | 3 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 4 | 5 | # Load j2cl repository 6 | http_archive( 7 | name = "com_google_j2cl", 8 | strip_prefix = "j2cl-master", 9 | url = "https://github.com/google/j2cl/archive/master.zip", 10 | ) 11 | 12 | load("@com_google_j2cl//build_defs:repository.bzl", "load_j2cl_repo_deps") 13 | 14 | load_j2cl_repo_deps() 15 | 16 | load("@com_google_j2cl//build_defs:rules.bzl", "setup_j2cl_workspace") 17 | 18 | setup_j2cl_workspace() 19 | -------------------------------------------------------------------------------- /javascript/api/BUILD: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_library") 2 | load("@com_google_j2cl//build_defs:rules.bzl", "j2cl_library") 3 | 4 | package( 5 | default_visibility = ["//visibility:public"], 6 | ) 7 | 8 | j2cl_library( 9 | name = "dhall_js_java", 10 | srcs = ["DhallJs.java"], 11 | deps = [ 12 | "//modules/core", 13 | "//modules/parser", 14 | ], 15 | ) 16 | 17 | closure_js_library( 18 | name = "dhall_js", 19 | srcs = ["dhall.js"], 20 | deps = [":dhall_js_java"], 21 | ) 22 | -------------------------------------------------------------------------------- /javascript/jre/Path.java: -------------------------------------------------------------------------------- 1 | package java.nio.file; 2 | 3 | import java.util.Iterator; 4 | 5 | public class Path { 6 | private final String input; 7 | 8 | public Path(String input) { 9 | this.input = input; 10 | } 11 | 12 | public final boolean isAbsolute() { 13 | return this.input.charAt(0) == '/'; 14 | } 15 | 16 | public final Iterator iterator() { 17 | return null; 18 | } 19 | 20 | public final int getNameCount() { 21 | return 0; 22 | } 23 | 24 | public Path resolve(String other) { 25 | return this; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /modules/parser/src/main/java/org/dhallj/parser/DhallParser.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.parser; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import org.dhallj.core.Expr; 6 | import org.dhallj.parser.support.Parser; 7 | 8 | /** Parses text input into Dhall expressions. */ 9 | public final class DhallParser { 10 | public static Expr.Parsed parse(String input) { 11 | return Parser.parse(input); 12 | } 13 | 14 | public static Expr.Parsed parse(InputStream input) throws IOException { 15 | return Parser.parse(input); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /modules/cats/src/test/scala/org/dhallj/cats/LiftVisitorSuite.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.cats 2 | 3 | import cats.Applicative 4 | import cats.instances.option._ 5 | import munit.ScalaCheckSuite 6 | import org.dhallj.core.Expr 7 | import org.dhallj.testing.instances._ 8 | import org.scalacheck.Prop 9 | 10 | class ToStringSuite extends ScalaCheckSuite { 11 | property("LiftVisitor with no overrides is pure") { 12 | Prop.forAll { (expr: Expr) => 13 | val lift = new LiftVisitor[Option](Applicative[Option]) 14 | 15 | expr.accept(lift) == Some(expr) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /modules/parser/BUILD: -------------------------------------------------------------------------------- 1 | load("@com_google_j2cl//build_defs:rules.bzl", "j2cl_library") 2 | 3 | package( 4 | default_visibility = ["//visibility:public"], 5 | ) 6 | 7 | j2cl_library( 8 | name = "parser", 9 | srcs = glob([ 10 | "src/main/java/org/dhallj/parser/*.java", 11 | "src/main/java/org/dhallj/parser/support/*.java", 12 | "target/javacc/*.java", 13 | ]), 14 | deps = [ 15 | "//javascript/jre:java_io", 16 | "//javascript/jre:java_net", 17 | "//javascript/jre:java_nio_file", 18 | "//modules/core", 19 | ], 20 | ) 21 | -------------------------------------------------------------------------------- /javascript/api/DhallJs.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.js; 2 | 3 | import org.dhallj.core.Expr; 4 | import org.dhallj.parser.DhallParser; 5 | import jsinterop.annotations.JsType; 6 | 7 | @JsType 8 | public class DhallJs { 9 | public static String parse(String input) { 10 | return DhallParser.parse(input).toString(); 11 | } 12 | 13 | public static String normalize(String input) { 14 | return DhallParser.parse(input).normalize().toString(); 15 | } 16 | 17 | public static String typeCheck(String input) { 18 | return Expr.Util.typeCheck(DhallParser.parse(input)).toString(); 19 | } 20 | } -------------------------------------------------------------------------------- /modules/imports/src/main/scala/org/dhallj/imports/ImportContext.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.imports 2 | 3 | import java.net.URI 4 | import java.nio.file.Path 5 | import org.dhallj.core.Expr 6 | 7 | sealed abstract class ImportContext extends Product with Serializable 8 | 9 | object ImportContext { 10 | case object Missing extends ImportContext 11 | case class Env(value: String) extends ImportContext 12 | case class Local(absolutePath: Path) extends ImportContext 13 | case class Classpath(absolutePath: Path) extends ImportContext 14 | case class Remote(url: URI, using: Expr) extends ImportContext 15 | } 16 | -------------------------------------------------------------------------------- /tests/src/main/scala/org/dhallj/tests/HaskellDhall.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.tests 2 | 3 | import java.io.ByteArrayInputStream 4 | import java.nio.charset.StandardCharsets.UTF_8 5 | import scala.sys.process._ 6 | import scala.util.Try 7 | 8 | class HaskellDhall {} 9 | object HaskellDhall { 10 | def isAvailable(): Boolean = 11 | Try(Process("dhall --version").lineStream.head.split("\\.")).toOption.exists(_.length == 3) 12 | 13 | def hash(input: String): String = { 14 | val stream = new ByteArrayInputStream(input.getBytes(UTF_8)) 15 | Process("dhall hash").#<(stream).lineStream.head.substring(7) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /javascript/jre/BUILD: -------------------------------------------------------------------------------- 1 | load("@com_google_j2cl//build_defs:rules.bzl", "j2cl_library") 2 | 3 | package( 4 | default_visibility = ["//visibility:public"], 5 | ) 6 | 7 | j2cl_library( 8 | name = "java_io", 9 | srcs = [ 10 | "BufferedReader.java", 11 | "InputStreamReader.java", 12 | ], 13 | ) 14 | 15 | j2cl_library( 16 | name = "java_net", 17 | srcs = [ 18 | "URI.java", 19 | "URISyntaxException.java", 20 | ], 21 | ) 22 | 23 | j2cl_library( 24 | name = "java_nio_file", 25 | srcs = [ 26 | "InvalidPathException.java", 27 | "Path.java", 28 | "Paths.java", 29 | ], 30 | ) 31 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/core/typechecking/CheckEquivalence.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.core.typechecking; 2 | 3 | import org.dhallj.core.Expr; 4 | import org.dhallj.core.Operator; 5 | import org.dhallj.core.ExternalVisitor; 6 | 7 | final class CheckEquivalence extends ExternalVisitor.Constant { 8 | public static final ExternalVisitor instance = new CheckEquivalence(); 9 | 10 | CheckEquivalence() { 11 | super(null); 12 | } 13 | 14 | @Override 15 | public Boolean onOperatorApplication(Operator operator, Expr lhs, Expr rhs) { 16 | return operator.equals(Operator.EQUIVALENT) && lhs.equivalent(rhs); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /modules/core/BUILD: -------------------------------------------------------------------------------- 1 | load("@com_google_j2cl//build_defs:rules.bzl", "j2cl_library") 2 | 3 | package( 4 | default_visibility = ["//visibility:public"], 5 | ) 6 | 7 | j2cl_library( 8 | name = "cbor", 9 | srcs = glob([ 10 | "src/main/java/org/dhallj/cbor/*.java", 11 | ]), 12 | ) 13 | 14 | j2cl_library( 15 | name = "core", 16 | srcs = glob([ 17 | "src/main/java/org/dhallj/core/*.java", 18 | "src/main/java/org/dhallj/core/binary/*.java", 19 | "src/main/java/org/dhallj/core/normalization/*.java", 20 | "src/main/java/org/dhallj/core/typechecking/*.java", 21 | ]), 22 | deps = [ 23 | ":cbor", 24 | "//javascript/jre:java_net", 25 | "//javascript/jre:java_nio_file", 26 | ], 27 | ) 28 | -------------------------------------------------------------------------------- /javascript/api/dhall.js: -------------------------------------------------------------------------------- 1 | goog.module('dhall.js'); 2 | 3 | var DhallJs = goog.require('org.dhallj.js.DhallJs'); 4 | 5 | /** 6 | * @param {string} input 7 | * @return {null|string} 8 | */ 9 | function parse(input) { 10 | return DhallJs.parse(input); 11 | } 12 | 13 | /** 14 | * @param {string} input 15 | * @return {null|string} 16 | */ 17 | function typeCheck(input) { 18 | return DhallJs.typeCheck(input); 19 | } 20 | 21 | /** 22 | * @param {string} input 23 | * @return {null|string} 24 | */ 25 | function normalize(input) { 26 | return DhallJs.normalize(input); 27 | } 28 | 29 | // Otherwise we seem to lose stuff with some configurations? 30 | parse("1"); 31 | typeCheck("1"); 32 | normalize("1"); 33 | 34 | goog.exportSymbol("parse", parse); 35 | goog.exportSymbol("typeCheck", typeCheck); 36 | goog.exportSymbol("normalize", normalize); -------------------------------------------------------------------------------- /benchmarks/src/main/scala/org/dhallj/benchmarks/ParsingBenchmark.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.benchmarks 2 | 3 | import java.util.concurrent.TimeUnit 4 | import org.openjdk.jmh.annotations._ 5 | import org.dhallj.core.Expr 6 | import org.dhallj.parser.DhallParser 7 | import org.dhallj.prelude.Prelude 8 | 9 | /** 10 | * Compare the performance of various ways of folding JSON values. 11 | * 12 | * The following command will run the benchmarks with reasonable settings: 13 | * 14 | * > sbt "benchmarks/jmh:run -i 10 -wi 10 -f 2 -t 1 org.dhallj.benchmarks.ParsingBenchmark" 15 | */ 16 | @State(Scope.Thread) 17 | @BenchmarkMode(Array(Mode.Throughput)) 18 | @OutputTimeUnit(TimeUnit.SECONDS) 19 | class ParsingBenchmark { 20 | val preludeAsString = Prelude.instance.toString 21 | 22 | @Benchmark 23 | def parsePrelude: Expr = DhallParser.parse(preludeAsString) 24 | } 25 | -------------------------------------------------------------------------------- /modules/parser/src/main/java/org/dhallj/parser/support/LetBinding.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.parser.support; 2 | 3 | import org.dhallj.core.Expr; 4 | 5 | final class LetBinding { 6 | final String name; 7 | final Expr.Parsed type; 8 | final Expr.Parsed value; 9 | final String text0; 10 | final String text1; 11 | final String text2; 12 | final int beginLine; 13 | final int beginColumn; 14 | 15 | LetBinding( 16 | String name, 17 | Expr.Parsed type, 18 | Expr.Parsed value, 19 | String text0, 20 | String text1, 21 | String text2, 22 | int beginLine, 23 | int beginColumn) { 24 | this.name = name; 25 | this.type = type; 26 | this.value = value; 27 | this.text0 = text0; 28 | this.text1 = text1; 29 | this.text2 = text2; 30 | this.beginLine = beginLine; 31 | this.beginColumn = beginColumn; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/core/normalization/BetaNormalizeIf.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.core.normalization; 2 | 3 | import org.dhallj.core.Expr; 4 | 5 | final class BetaNormalizeIf { 6 | static final Expr apply(Expr predicate, Expr thenValue, Expr elseValue) { 7 | Boolean predicateAsBool = Expr.Util.asBoolLiteral(predicate); 8 | 9 | if (predicateAsBool != null) { 10 | return (predicateAsBool) ? thenValue : elseValue; 11 | } else { 12 | Boolean thenAsBool = Expr.Util.asBoolLiteral(thenValue); 13 | Boolean elseAsBool = Expr.Util.asBoolLiteral(elseValue); 14 | 15 | if (thenAsBool != null && elseAsBool != null && thenAsBool && !elseAsBool) { 16 | return predicate; 17 | } else if (thenValue.equivalent(elseValue)) { 18 | return thenValue; 19 | } else { 20 | return Expr.makeIf(predicate, thenValue, elseValue); 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /modules/circe/src/test/scala/org/dhallj/circe/JsonCleaner.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.circe 2 | 3 | import io.circe.{Json, JsonNumber, JsonObject} 4 | 5 | object JsonCleaner extends Json.Folder[Json] { 6 | def onBoolean(value: Boolean): Json = Json.fromBoolean(value) 7 | def onNull: Json = Json.Null 8 | def onNumber(value: JsonNumber): Json = { 9 | val asDouble = value.toDouble 10 | val asJson = Json.fromDoubleOrNull(asDouble) 11 | if (asJson.asNumber == Some(value)) { 12 | asJson 13 | } else { 14 | Json.fromLong(0) 15 | } 16 | } 17 | def onString(value: String): Json = Json.fromString(value) 18 | def onObject(value: JsonObject): Json = Json.fromFields( 19 | value.toIterable.flatMap { 20 | case ("", v) => None 21 | case (k, v) => Some((k, v.foldWith(this))) 22 | } 23 | ) 24 | def onArray(values: Vector[Json]): Json = Json.fromValues(values.map(_.foldWith(this))) 25 | } 26 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/cbor/AdditionalInfo.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.cbor; 2 | 3 | public enum AdditionalInfo { 4 | DIRECT(0), // 0-23 5 | ONE_BYTE(24), // 24 6 | TWO_BYTES(25), // 25 7 | FOUR_BYTES(26), // 26 8 | EIGHT_BYTES(27), // 27 9 | RESERVED(28), // 28-30 10 | INDEFINITE(31); // 31 11 | 12 | final int value; 13 | 14 | private AdditionalInfo(int value) { 15 | this.value = value; 16 | } 17 | 18 | public static AdditionalInfo fromByte(byte b) { 19 | switch (b & 31) { 20 | case 24: 21 | return ONE_BYTE; 22 | case 25: 23 | return TWO_BYTES; 24 | case 26: 25 | return FOUR_BYTES; 26 | case 27: 27 | return EIGHT_BYTES; 28 | case 28: 29 | case 29: 30 | case 30: 31 | return RESERVED; 32 | case 31: 33 | return INDEFINITE; 34 | default: 35 | return DIRECT; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /modules/parser/src/main/java/org/dhallj/parser/support/Parser.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.parser.support; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import org.dhallj.core.DhallException.ParsingFailure; 6 | import org.dhallj.core.Expr; 7 | 8 | /** Wrapper for the JavaCC-generated parser. */ 9 | public final class Parser { 10 | public static Expr.Parsed parse(String input) { 11 | try { 12 | return new JavaCCParser(new StringProvider(input)).TOP_LEVEL(); 13 | } catch (ParseException underlying) { 14 | throw new ParsingFailure(underlying.getMessage(), underlying); 15 | } 16 | } 17 | 18 | public static Expr.Parsed parse(InputStream input) throws IOException { 19 | try { 20 | return new JavaCCParser(new StreamProvider(input)).TOP_LEVEL(); 21 | } catch (ParseException underlying) { 22 | throw new ParsingFailure(underlying.getMessage(), underlying); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/core/IsResolved.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.core; 2 | 3 | import java.net.URI; 4 | import java.nio.file.Path; 5 | 6 | final class IsResolved extends Visitor.Property { 7 | public static final Visitor instance = new IsResolved(); 8 | 9 | @Override 10 | public Boolean onLocalImport(Path path, Expr.ImportMode mode, byte[] hash) { 11 | return false; 12 | } 13 | 14 | @Override 15 | public Boolean onClasspathImport(Path path, Expr.ImportMode mode, byte[] hash) { 16 | return false; 17 | } 18 | 19 | @Override 20 | public Boolean onRemoteImport(URI url, Boolean using, Expr.ImportMode mode, byte[] hash) { 21 | return false; 22 | } 23 | 24 | @Override 25 | public Boolean onEnvImport(String value, Expr.ImportMode mode, byte[] hash) { 26 | return false; 27 | } 28 | 29 | @Override 30 | public Boolean onMissingImport(Expr.ImportMode mode, byte[] hash) { 31 | return false; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/cbor/Visitor.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.cbor; 2 | 3 | import java.math.BigInteger; 4 | import java.util.List; 5 | import java.util.Map; 6 | 7 | /** 8 | * Represents a function from a CBOR expression to a value. 9 | * 10 | * @param R The result type 11 | */ 12 | public interface Visitor { 13 | 14 | public R onUnsignedInteger(BigInteger value); 15 | 16 | public R onNegativeInteger(BigInteger value); 17 | 18 | public R onByteString(byte[] value); 19 | 20 | public R onTextString(String value); 21 | 22 | public R onVariableArray(BigInteger length, String name); 23 | 24 | public R onArray(BigInteger length, BigInteger tagI); 25 | 26 | public R onMap(BigInteger size); 27 | 28 | public R onFalse(); 29 | 30 | public R onTrue(); 31 | 32 | public R onNull(); 33 | 34 | public R onHalfFloat(float value); 35 | 36 | public R onSingleFloat(float value); 37 | 38 | public R onDoubleFloat(double value); 39 | 40 | public R onTag(); 41 | } 42 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.codecommit" % "sbt-github-actions" % "0.10.1") 2 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.15.0") 3 | addSbtPlugin("com.eed3si9n" % "sbt-unidoc" % "0.4.3") 4 | addSbtPlugin("com.github.sbt" % "sbt-release" % "1.0.15") 5 | addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2") 6 | addSbtPlugin("com.lightbend.sbt" % "sbt-java-formatter" % "0.6.0") 7 | addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "0.8.1") 8 | addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.6.3") 9 | addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "1.0.0") 10 | addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.8.0") 11 | addSbtPlugin("com.typesafe.sbt" % "sbt-site" % "1.4.1") 12 | addSbtPlugin("dev.travisbrown" % "sbt-javacc" % "0.1.0") 13 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.2") 14 | addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "1.0.0") 15 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.1") 16 | addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.0") 17 | -------------------------------------------------------------------------------- /modules/parser/src/main/java/org/dhallj/parser/support/Comment.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.parser.support; 2 | 3 | final class Comment { 4 | private final String content; 5 | private final int beginLine; 6 | private final int beginColumn; 7 | private final int endLine; 8 | private final int endColumn; 9 | 10 | Comment(String content, int beginLine, int beginColumn, int endLine, int endColumn) { 11 | this.content = content; 12 | this.beginLine = beginLine; 13 | this.beginColumn = beginColumn; 14 | this.endLine = endLine; 15 | this.endColumn = endColumn; 16 | } 17 | 18 | public String getContent() { 19 | return this.content; 20 | } 21 | 22 | public final int getBeginLine() { 23 | return this.beginLine; 24 | } 25 | 26 | public final int getBeginColumn() { 27 | return this.beginColumn; 28 | } 29 | 30 | public final int getEndLine() { 31 | return this.endLine; 32 | } 33 | 34 | public final int getEndColumn() { 35 | return this.endColumn; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/cbor/MajorType.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.cbor; 2 | 3 | public enum MajorType { 4 | UNSIGNED_INTEGER(0), 5 | NEGATIVE_INTEGER(1), 6 | BYTE_STRING(2), 7 | TEXT_STRING(3), 8 | ARRAY(4), 9 | MAP(5), 10 | SEMANTIC_TAG(6), 11 | PRIMITIVE(7); 12 | 13 | final int value; 14 | 15 | private MajorType(int value) { 16 | this.value = value; 17 | } 18 | 19 | public static MajorType fromByte(byte b) { 20 | switch ((b & 0xff) >> 5) { 21 | case 0: 22 | return UNSIGNED_INTEGER; 23 | case 1: 24 | return NEGATIVE_INTEGER; 25 | case 2: 26 | return BYTE_STRING; 27 | case 3: 28 | return TEXT_STRING; 29 | case 4: 30 | return ARRAY; 31 | case 5: 32 | return MAP; 33 | case 6: 34 | return SEMANTIC_TAG; 35 | case 7: 36 | return PRIMITIVE; 37 | default: 38 | throw new IllegalArgumentException("Invalid CBOR major type " + Byte.toString(b)); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /benchmarks/src/main/scala/org/dhallj/benchmarks/EncodingBenchmark.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.benchmarks 2 | 3 | import java.util.concurrent.TimeUnit 4 | import org.openjdk.jmh.annotations._ 5 | import org.dhallj.core.Expr 6 | import org.dhallj.prelude.Prelude 7 | 8 | /** 9 | * Compare the performance of various ways of folding JSON values. 10 | * 11 | * The following command will run the benchmarks with reasonable settings: 12 | * 13 | * > sbt "benchmarks/jmh:run -i 10 -wi 10 -f 2 -t 1 org.dhallj.benchmarks.EncodingBenchmark" 14 | */ 15 | @State(Scope.Thread) 16 | @BenchmarkMode(Array(Mode.Throughput)) 17 | @OutputTimeUnit(TimeUnit.SECONDS) 18 | class EncodingBenchmark { 19 | val prelude: Expr = Prelude.instance 20 | val deep: Expr = (0 to 100000).foldLeft(Expr.makeDoubleLiteral(0)) { case (acc, i) => 21 | Expr.makeRecordLiteral(s"a$i", acc) 22 | } 23 | 24 | @Benchmark 25 | def encodePreludeToBytes: Array[Byte] = prelude.getEncodedBytes 26 | 27 | @Benchmark 28 | def encodeDeepToBytes: Array[Byte] = deep.getEncodedBytes 29 | } 30 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/core/ArrayIterable.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.core; 2 | 3 | import java.util.Iterator; 4 | import java.util.NoSuchElementException; 5 | 6 | final class ArrayIterable implements Iterable { 7 | private final A[] values; 8 | 9 | public ArrayIterable(A[] values) { 10 | this.values = values; 11 | } 12 | 13 | public final Iterator iterator() { 14 | return new ArrayIterator(this.values); 15 | } 16 | 17 | private static final class ArrayIterator implements Iterator { 18 | private final A[] values; 19 | private int i = 0; 20 | 21 | ArrayIterator(A[] values) { 22 | this.values = values; 23 | } 24 | 25 | public final boolean hasNext() { 26 | return this.i < this.values.length; 27 | } 28 | 29 | public final A next() { 30 | try { 31 | return this.values[this.i++]; 32 | } catch (ArrayIndexOutOfBoundsException e) { 33 | throw new NoSuchElementException(); 34 | } 35 | } 36 | 37 | public final void remove() { 38 | throw new UnsupportedOperationException("remove"); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/src/test/scala/org/dhallj/tests/ToStringSuite.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.tests 2 | 3 | import munit.{Ignore, ScalaCheckSuite, Slow} 4 | import org.dhallj.core.Expr 5 | import org.dhallj.parser.DhallParser 6 | import org.dhallj.testing.WellTypedExpr 7 | import org.dhallj.testing.instances._ 8 | import org.scalacheck.Prop 9 | 10 | class ToStringSuite extends ScalaCheckSuite() { 11 | property("toString produces parseable code given well-typed values") { 12 | Prop.forAll((expr: WellTypedExpr) => DhallParser.parse(clue(expr.value.toString)) == expr.value) 13 | } 14 | 15 | property("toString produces parseable code".tag(Ignore)) { 16 | Prop.forAll((expr: Expr) => DhallParser.parse(clue(expr.toString)) == expr) 17 | } 18 | 19 | test("Unnormalized Prelude should round-trip through toString".tag(Slow)) { 20 | import org.dhallj.syntax._ 21 | 22 | val Right(prelude) = "./dhall-lang/Prelude/package.dhall".parseExpr.flatMap(_.resolve) 23 | val Right(fromToString) = prelude.toString.parseExpr 24 | 25 | assert(prelude.hash.sameElements(fromToString.hash)) 26 | assert(prelude.diff(fromToString).isEmpty) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /modules/imports/src/test/scala/org/dhallj/imports/ImportCacheSuite.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.imports 2 | 3 | import java.nio.file.{Files, Path} 4 | 5 | import cats.effect.IO 6 | import cats.implicits._ 7 | import munit.FunSuite 8 | import scala.reflect.io.Directory 9 | 10 | class ImportCacheSuite extends FunSuite { 11 | 12 | val rootDir = FunFixture[(ImportCache[IO], Path)]( 13 | setup = { test => 14 | val rootDir = Files.createTempDirectory(test.name).resolve("dhall") 15 | ImportCache[IO](rootDir).unsafeRunSync().get -> rootDir 16 | }, 17 | teardown = { case (_, rootDir) => 18 | new Directory(rootDir.toFile).deleteRecursively() 19 | } 20 | ) 21 | 22 | val key = "0f86d".getBytes 23 | 24 | val bytes: Array[Byte] = "test".getBytes 25 | 26 | rootDir.test("Get-if-absent") { case (cache, _) => 27 | val prog = cache.get(key) 28 | 29 | assertEquals(prog.unsafeRunSync(), None) 30 | } 31 | 32 | rootDir.test("Get-if-present") { case (cache, _) => 33 | val prog = cache.put(key, bytes) >> cache.get(key) 34 | 35 | assert(prog.unsafeRunSync().exists(_.sameElements(bytes))) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/core/normalization/NormalizationUtils.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.core.normalization; 2 | 3 | import java.util.Comparator; 4 | import java.util.Map.Entry; 5 | import org.dhallj.core.Expr; 6 | 7 | /** Static utility classes and methods for internal use. */ 8 | final class NormalizationUtilities { 9 | static final A lookup(Iterable> entries, String key) { 10 | for (Entry entry : entries) { 11 | if (entry.getKey().equals(key)) { 12 | return entry.getValue(); 13 | } 14 | } 15 | return null; 16 | } 17 | 18 | static final Entry lookupEntry(Iterable> entries, String key) { 19 | for (Entry entry : entries) { 20 | if (entry.getKey().equals(key)) { 21 | return entry; 22 | } 23 | } 24 | return null; 25 | } 26 | 27 | static final Comparator> entryComparator = 28 | new Comparator>() { 29 | public int compare(Entry a, Entry b) { 30 | return a.getKey().compareTo(b.getKey()); 31 | } 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /modules/core/src/test/java/org/dhallj/cbor/HalfFloatSuite.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.cbor 2 | 3 | import munit.FunSuite 4 | import org.scalacheck.Prop 5 | 6 | class HalfFloatSuite extends FunSuite { 7 | def roundTripInt(x: Int): Int = 8 | HalfFloat.toFloat(HalfFloat.fromFloat(x.toFloat)).toInt 9 | 10 | test("HalfFloat conversions round-trip integers with abs <= 2048") { 11 | 0.to(2048).foreach { x => 12 | assertEquals(roundTripInt(x), x) 13 | assertEquals(roundTripInt(-x), -x) 14 | } 15 | } 16 | 17 | test("HalfFloat conversions round-trip even integers with abs <= 4096") { 18 | 1.to(1024).foreach { x => 19 | assertEquals(roundTripInt((x * 2) + 2048), (x * 2) + 2048) 20 | assertEquals(roundTripInt(-((x * 2) + 2048)), -((x * 2) + 2048)) 21 | assertEquals(roundTripInt((x * 2) + 2048 - 1), (x * 2) + 2048) 22 | assertEquals(roundTripInt(-((x * 2) + 2048 - 1)), -((x * 2) + 2048)) 23 | } 24 | } 25 | 26 | test("HalfFloat conversions round-trip through float for all values") { 27 | 0.until(1 << 16).foreach { x => 28 | val asFloat = HalfFloat.toFloat(x) 29 | 30 | if (!asFloat.isNaN) { 31 | assertEquals(HalfFloat.fromFloat(asFloat), x) 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/src/test/scala/org/dhallj/tests/ImportResolutionSuite.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.tests 2 | 3 | import java.nio.file.{Path, Paths} 4 | 5 | import cats.effect.{ContextShift, IO, Resource} 6 | import munit.FunSuite 7 | import org.dhallj.core.Expr 8 | import org.dhallj.imports.syntax._ 9 | import org.dhallj.parser.DhallParser.parse 10 | import org.http4s.client._ 11 | import org.http4s.client.blaze._ 12 | 13 | import scala.concurrent.ExecutionContext.global 14 | 15 | class ImportResolutionSuite extends FunSuite { 16 | 17 | implicit val cs: ContextShift[IO] = IO.contextShift(global) 18 | 19 | implicit val client: Resource[IO, Client[IO]] = BlazeClientBuilder[IO](global).resource 20 | 21 | test("Resolve with different base directory") { 22 | //Path inside dhall-lang submodule 23 | val expr = parse("let x = ./success/prelude/Bool/and/0B.dhall in x") 24 | val expected = parse("Bool").normalize 25 | 26 | assert(resolveRelativeTo(Paths.get("./dhall-lang/tests/type-inference"))(expr) == expected) 27 | } 28 | 29 | private def resolveRelativeTo(relativeTo: Path)(e: Expr): Expr = 30 | client 31 | .use { c => 32 | implicit val http: Client[IO] = c 33 | 34 | e.resolveImportsRelativeTo[IO](relativeTo).map(_.normalize) 35 | } 36 | .unsafeRunSync() 37 | 38 | } 39 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/core/DhallException.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.core; 2 | 3 | /** Base class of exceptions that may be thrown or returned by DhallJ. */ 4 | public class DhallException extends RuntimeException { 5 | public DhallException(String message) { 6 | super(message); 7 | } 8 | 9 | public DhallException(String message, Throwable cause) { 10 | super(message, cause); 11 | } 12 | 13 | /** Represents a parsing failure, generally wrapping an underlying exception. */ 14 | public static final class ParsingFailure extends DhallException { 15 | @Override 16 | public Throwable fillInStackTrace() { 17 | // This is a failure type; stack traces aren't useful. 18 | return this; 19 | } 20 | 21 | public ParsingFailure(String message, Throwable cause) { 22 | super(message, cause); 23 | } 24 | } 25 | 26 | public static final class ResolutionFailure extends DhallException { 27 | @Override 28 | public Throwable fillInStackTrace() { 29 | // This is a failure type; stack traces aren't useful. 30 | return this; 31 | } 32 | 33 | public ResolutionFailure(String message, Throwable cause) { 34 | super(message, cause); 35 | } 36 | 37 | public ResolutionFailure(String message) { 38 | super(message); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /modules/yaml/src/main/java/org/dhallj/yaml/YamlConverter.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.yaml; 2 | 3 | import org.dhallj.core.Expr; 4 | import org.dhallj.core.converters.JsonConverter; 5 | import org.yaml.snakeyaml.Yaml; 6 | import org.yaml.snakeyaml.DumperOptions; 7 | 8 | public class YamlConverter { 9 | private static final DumperOptions defaultOptions = new DumperOptions(); 10 | 11 | static { 12 | defaultOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); 13 | } 14 | 15 | public static final String toYamlString(Expr expr) { 16 | return toYamlString(expr, defaultOptions, true); 17 | } 18 | 19 | public static final String toYamlString(Expr expr, DumperOptions options) { 20 | return toYamlString(expr, options, true); 21 | } 22 | 23 | public static final String toYamlString(Expr expr, boolean skipNulls) { 24 | return toYamlString(expr, defaultOptions, skipNulls); 25 | } 26 | 27 | public static final String toYamlString(Expr expr, DumperOptions options, boolean skipNulls) { 28 | YamlHandler handler = new YamlHandler(skipNulls); 29 | boolean wasConverted = expr.accept(new JsonConverter(handler)); 30 | 31 | if (wasConverted) { 32 | Yaml yaml = new Yaml(options); 33 | 34 | return yaml.dump(handler.getResult()); 35 | } else { 36 | return null; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/src/main/scala/org/dhallj/tests/acceptance/AcceptanceFailureSuite.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.tests.acceptance 2 | 3 | import org.dhallj.core.Expr 4 | import org.dhallj.core.binary.Decode.decode 5 | import org.dhallj.parser.DhallParser 6 | import scala.reflect.ClassTag 7 | 8 | abstract class AcceptanceFailureSuite[A, E <: Throwable: ClassTag] extends AcceptanceSuite { 9 | def loadInput(input: Array[Byte]): A 10 | 11 | testInputs 12 | .map { case (name, path) => 13 | (name, readBytes(path)) 14 | } 15 | .foreach { case (name, input) => 16 | test(name) { 17 | intercept[E](loadInput(input)) 18 | } 19 | } 20 | } 21 | 22 | class ParsingFailureSuite(val base: String) extends AcceptanceFailureSuite[Expr, Exception] { 23 | def loadInput(input: Array[Byte]): Expr = DhallParser.parse(new String(input)) 24 | } 25 | 26 | class TypeCheckingFailureSuite(val base: String) extends AcceptanceFailureSuite[Expr, RuntimeException] { 27 | def loadInput(input: Array[Byte]): Expr = Expr.Util.typeCheck(DhallParser.parse(new String(input))) 28 | } 29 | 30 | class BinaryDecodingFailureSuite(val base: String) extends AcceptanceFailureSuite[Expr, RuntimeException] { 31 | override def isInputFileName(fileName: String): Boolean = fileName.endsWith(".dhallb") 32 | 33 | def loadInput(input: Array[Byte]): Expr = decode(input) 34 | } 35 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/core/typechecking/TypeCheckApplication.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.core.typechecking; 2 | 3 | import org.dhallj.core.Expr; 4 | import org.dhallj.core.ExternalVisitor; 5 | 6 | final class TypeCheckApplication extends ExternalVisitor.Constant { 7 | private final Expr arg; 8 | private final Expr argType; 9 | private final TypeCheck typeCheck; 10 | 11 | TypeCheckApplication(Expr arg, Expr argType, TypeCheck typeCheck) { 12 | super(null); 13 | this.arg = arg; 14 | this.argType = argType; 15 | this.typeCheck = typeCheck; 16 | } 17 | 18 | @Override 19 | public Expr onBuiltIn(String name) { 20 | if (name.equals("Some")) { 21 | if (TypeCheck.isType(this.argType.accept(this.typeCheck))) { 22 | return Expr.makeApplication(Expr.Constants.OPTIONAL, this.argType); 23 | } else { 24 | throw TypeCheckFailure.makeSomeApplicationError(this.arg, this.argType); 25 | } 26 | } 27 | throw TypeCheckFailure.makeBuiltInApplicationError(name, this.arg, this.argType); 28 | } 29 | 30 | @Override 31 | public Expr onPi(String name, Expr input, Expr result) { 32 | if (input.equivalent(this.argType)) { 33 | return result.substitute(name, arg).normalize(); 34 | } else { 35 | throw TypeCheckFailure.makeApplicationTypeError(input, this.argType); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /modules/scala/src/main/scala/org/dhallj/syntax/package.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj 2 | 3 | import org.dhallj.core.DhallException.{ParsingFailure, ResolutionFailure} 4 | import org.dhallj.core.Expr 5 | import org.dhallj.core.typechecking.TypeCheckFailure 6 | import org.dhallj.imports.mini.Resolver 7 | import org.dhallj.parser.DhallParser 8 | 9 | package object syntax { 10 | implicit final class DhallStringOps(val value: String) extends AnyVal { 11 | def parseExpr: Either[ParsingFailure, Expr] = 12 | try { 13 | Right(DhallParser.parse(value)) 14 | } catch { 15 | case e: ParsingFailure => Left(e) 16 | } 17 | } 18 | 19 | implicit final class DhallExprOps(val expr: Expr) extends AnyVal { 20 | def apply(args: Expr*): Expr = Expr.makeApplication(expr, args.toArray) 21 | 22 | def typeCheck: Either[TypeCheckFailure, Expr] = 23 | try { 24 | Right(Expr.Util.typeCheck(expr)) 25 | } catch { 26 | case e: TypeCheckFailure => Left(e) 27 | } 28 | 29 | def diff(other: Expr): Option[(Option[Expr], Option[Expr])] = 30 | Option(Expr.Util.getFirstDiff(expr, other)).map(entry => (Option(entry.getKey), Option(entry.getValue))) 31 | 32 | def resolve: Either[ResolutionFailure, Expr] = 33 | try { 34 | Right(Resolver.resolve(expr)) 35 | } catch { 36 | case e: ResolutionFailure => Left(e) 37 | } 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /modules/imports/src/main/scala/org/dhallj/imports/ReferentialSanityCheck.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.imports 2 | 3 | import cats.effect.Sync 4 | import org.dhallj.core.DhallException.ResolutionFailure 5 | 6 | object ReferentialSanityCheck { 7 | 8 | def apply[F[_]](parent: ImportContext, child: ImportContext)(implicit F: Sync[F]): F[Unit] = parent match { 9 | case ImportContext.Remote(uri, _) => 10 | child match { 11 | case ImportContext.Remote(_, _) => F.unit 12 | case ImportContext.Missing => F.unit 13 | case ImportContext.Local(path) => 14 | F.raiseError( 15 | new ResolutionFailure( 16 | s"Referential sanity violation - remote import $uri cannot reference local import $path" 17 | ) 18 | ) 19 | case ImportContext.Classpath(path) => 20 | F.raiseError( 21 | new ResolutionFailure( 22 | s"Referential sanity violation - remote import $uri cannot reference classpath import $path" 23 | ) 24 | ) 25 | case ImportContext.Env(v) => 26 | F.raiseError( 27 | new ResolutionFailure(s"Referential sanity violation - remote import $uri cannot reference env import $v") 28 | ) 29 | } 30 | case ImportContext.Missing => F.raiseError(new ResolutionFailure(s"Missing import cannot reference import $child")) 31 | case _ => F.unit 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/core/typechecking/Universe.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.core.typechecking; 2 | 3 | import org.dhallj.core.Expr; 4 | 5 | public enum Universe { 6 | TYPE, 7 | KIND, 8 | SORT; 9 | 10 | public final Universe max(Universe other) { 11 | if (this == SORT || other == SORT) { 12 | return SORT; 13 | } else if (this == KIND || other == KIND) { 14 | return KIND; 15 | } else { 16 | return TYPE; 17 | } 18 | } 19 | 20 | public final Expr toExpr() { 21 | if (this == TYPE) { 22 | return Expr.Constants.TYPE; 23 | } else if (this == KIND) { 24 | return Expr.Constants.KIND; 25 | } else { 26 | return Expr.Constants.SORT; 27 | } 28 | } 29 | 30 | public static final boolean isUniverse(Expr expr) { 31 | return fromExpr(expr) != null; 32 | } 33 | 34 | public static final Universe fromExpr(Expr expr) { 35 | String name = Expr.Util.asBuiltIn(expr); 36 | 37 | if (name != null) { 38 | if (name.equals("Type")) { 39 | return TYPE; 40 | } else if (name.equals("Kind")) { 41 | return KIND; 42 | } else if (name.equals("Sort")) { 43 | return SORT; 44 | } 45 | } 46 | return null; 47 | } 48 | 49 | public static Universe functionCheck(Universe input, Universe output) { 50 | if (output == Universe.TYPE) { 51 | return Universe.TYPE; 52 | } else { 53 | return input.max(output); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /modules/parser/src/main/java/org/dhallj/parser/support/OperatorPrecedenceTable.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.parser.support; 2 | 3 | import org.dhallj.core.Operator; 4 | 5 | final class OperatorPrecedenceTable implements JavaCCParserConstants { 6 | private static final int MAX_OPERATOR_TOKEN_KIND = 256; 7 | private static final int[] table = new int[MAX_OPERATOR_TOKEN_KIND]; 8 | 9 | static { 10 | for (int i = 0; i < MAX_OPERATOR_TOKEN_KIND; i++) { 11 | table[i] = -1; 12 | } 13 | 14 | table[OR] = Operator.OR.getPrecedence(); 15 | table[AND] = Operator.AND.getPrecedence(); 16 | table[EQUALS] = Operator.EQUALS.getPrecedence(); 17 | table[NOT_EQUALS] = Operator.NOT_EQUALS.getPrecedence(); 18 | table[PLUS] = Operator.PLUS.getPrecedence(); 19 | table[TIMES] = Operator.TIMES.getPrecedence(); 20 | table[TEXT_APPEND] = Operator.TEXT_APPEND.getPrecedence(); 21 | table[LIST_APPEND] = Operator.LIST_APPEND.getPrecedence(); 22 | table[COMBINE] = Operator.COMBINE.getPrecedence(); 23 | table[PREFER] = Operator.PREFER.getPrecedence(); 24 | table[COMBINE_TYPES] = Operator.COMBINE_TYPES.getPrecedence(); 25 | table[IMPORT_ALT] = Operator.IMPORT_ALT.getPrecedence(); 26 | table[EQUIVALENT] = Operator.EQUIVALENT.getPrecedence(); 27 | } 28 | 29 | static final int get(int tokenKind) { 30 | if (tokenKind >= MAX_OPERATOR_TOKEN_KIND) { 31 | return -1; 32 | } else { 33 | return table[tokenKind]; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/core/Tags.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.core; 2 | 3 | final class Tags { 4 | static final int NOTE = 0; 5 | 6 | // Non-recursive constructors. 7 | static final int NATURAL = 1; 8 | static final int INTEGER = 2; 9 | static final int DOUBLE = 3; 10 | static final int BUILT_IN = 4; 11 | static final int IDENTIFIER = 5; 12 | 13 | // Binding constructors. 14 | static final int LAMBDA = 6; 15 | static final int PI = 7; 16 | static final int LET = 8; 17 | 18 | // Other. 19 | static final int TEXT = 9; 20 | static final int NON_EMPTY_LIST = 10; 21 | static final int EMPTY_LIST = 11; 22 | 23 | static final int RECORD = 12; 24 | static final int RECORD_TYPE = 13; 25 | static final int UNION_TYPE = 14; 26 | 27 | static final int FIELD_ACCESS = 15; 28 | static final int PROJECTION = 16; 29 | static final int PROJECTION_BY_TYPE = 17; 30 | 31 | static final int APPLICATION = 18; 32 | static final int OPERATOR_APPLICATION = 19; 33 | static final int IF = 20; 34 | static final int ANNOTATED = 21; 35 | static final int ASSERT = 22; 36 | 37 | // Syntactic sugar. 38 | static final int MERGE = 23; 39 | static final int TO_MAP = 24; 40 | 41 | // Imports. 42 | static final int MISSING_IMPORT = 25; 43 | static final int ENV_IMPORT = 26; 44 | static final int LOCAL_IMPORT = 27; 45 | static final int REMOTE_IMPORT = 28; 46 | static final int CLASSPATH_IMPORT = 29; 47 | 48 | static final int WITH = 30; 49 | } 50 | -------------------------------------------------------------------------------- /modules/imports/src/main/scala/org/dhallj/imports/ResolveImports.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.imports 2 | 3 | import java.nio.file.{Path, Paths} 4 | 5 | import cats.effect.Sync 6 | import org.dhallj.core.Expr 7 | import org.http4s.client.Client 8 | 9 | object Resolver { 10 | def resolve[F[_] <: AnyRef](expr: Expr)(implicit Client: Client[F], F: Sync[F]): F[Expr] = 11 | F.flatMap(ResolveImportsVisitor[F](cwd))(expr.accept(_)) 12 | 13 | def resolveRelativeTo[F[_] <: AnyRef](relativeTo: Path)(expr: Expr)(implicit Client: Client[F], F: Sync[F]): F[Expr] = 14 | F.flatMap(ResolveImportsVisitor[F](relativeTo))(expr.accept(_)) 15 | 16 | def resolve[F[_] <: AnyRef]( 17 | semanticCache: ImportCache[F], 18 | semiSemanticCache: ImportCache[F] 19 | )(expr: Expr)(implicit Client: Client[F], F: Sync[F]): F[Expr] = 20 | expr.accept(ResolveImportsVisitor[F](semanticCache, semiSemanticCache, cwd)) 21 | 22 | def resolve[F[_] <: AnyRef]( 23 | semanticCache: ImportCache[F] 24 | )(expr: Expr)(implicit Client: Client[F], F: Sync[F]): F[Expr] = 25 | expr.accept(ResolveImportsVisitor[F](semanticCache, new ImportCache.NoopImportCache[F], cwd)) 26 | 27 | private def cwd: Path = Paths.get(".") 28 | 29 | final class Ops(val expr: Expr) extends AnyVal { 30 | def resolveImports[F[_] <: AnyRef](implicit Client: Client[F], F: Sync[F]): F[Expr] = 31 | resolve[F](expr) 32 | 33 | def resolveImportsRelativeTo[F[_] <: AnyRef](relativeTo: Path)(implicit Client: Client[F], F: Sync[F]): F[Expr] = 34 | resolveRelativeTo[F](relativeTo)(expr) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Travis Brown 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /tests/src/test/scala/org/dhallj/tests/MiscSuite.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.tests 2 | 3 | import munit.ScalaCheckSuite 4 | import org.dhallj.ast._ 5 | import org.dhallj.core.Expr 6 | import org.dhallj.parser.DhallParser 7 | import org.scalacheck.{Arbitrary, Gen, Prop} 8 | 9 | class MiscSuite extends ScalaCheckSuite { 10 | case class AsciiPrintableString(value: String) 11 | 12 | implicit val arbitraryAsciiPrintableString: Arbitrary[AsciiPrintableString] = 13 | Arbitrary(Gen.alphaNumStr.map(AsciiPrintableString(_))) 14 | 15 | def checkParse(name: String, input: String, expected: Expr)(implicit loc: munit.Location): Unit = 16 | test(name)(assert(clue(DhallParser.parse(input)).equivalent(clue(expected)))) 17 | 18 | def checkBetaNormalization(name: String, input: String, expected: Expr)(implicit loc: munit.Location): Unit = 19 | test(name)(assert(clue(DhallParser.parse(input).normalize).equivalent(clue(expected)))) 20 | 21 | def parsesTo(input: String, expected: Expr): Boolean = DhallParser.parse(input).equivalent(expected) 22 | 23 | test("getFirstDiff should work for classpath imports") { 24 | assert( 25 | Option(Expr.Util.getFirstDiff(DhallParser.parse("classpath:/foo"), DhallParser.parse("classpath:/bar"))).nonEmpty 26 | ) 27 | } 28 | 29 | property("doubles")(Prop.forAll((value: Double) => parsesTo(value.toString, DoubleLiteral(value)))) 30 | property("naturals")(Prop.forAll((value: BigInt) => parsesTo(value.abs.toString, NaturalLiteral(value.abs).get))) 31 | property("strings")( 32 | Prop.forAll((value: AsciiPrintableString) => parsesTo(s""""${value.value}"""", TextLiteral(value.value))) 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/core/typechecking/Context.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.core.typechecking; 2 | 3 | import org.dhallj.core.Expr; 4 | 5 | public final class Context { 6 | private final String key; 7 | private final Expr value; 8 | private final Context tail; 9 | 10 | Context(String key, Expr value, Context tail) { 11 | this.key = key; 12 | this.value = value; 13 | this.tail = tail; 14 | } 15 | 16 | public Expr lookup(String targetKey, long index) { 17 | if (this.key != null && this.key.equals(targetKey)) { 18 | if (index == 0) { 19 | return this.value; 20 | } else { 21 | if (this.tail == null) { 22 | return null; 23 | } else { 24 | return this.tail.lookup(targetKey, index - 1); 25 | } 26 | } 27 | } else { 28 | if (this.tail == null) { 29 | return null; 30 | } else { 31 | return this.tail.lookup(targetKey, index); 32 | } 33 | } 34 | } 35 | 36 | public Context insert(String key, Expr value) { 37 | return new Context(key, value, this); 38 | } 39 | 40 | public Context increment(String name) { 41 | if (this.key == null) { 42 | return this; 43 | } else { 44 | return new Context(this.key, this.value.increment(name), this.tail.increment(name)); 45 | } 46 | } 47 | 48 | public Context decrement(String name) { 49 | if (this.key == null) { 50 | return this; 51 | } else { 52 | return new Context(this.key, this.value.decrement(name), this.tail.decrement(name)); 53 | } 54 | } 55 | 56 | public static final Context EMPTY = new Context(null, null, null); 57 | } 58 | -------------------------------------------------------------------------------- /modules/yaml/src/main/java/org/dhallj/yaml/YamlContext.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.yaml; 2 | 3 | import java.util.ArrayList; 4 | import java.util.LinkedHashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | interface YamlContext { 9 | void add(String key); 10 | 11 | void add(Object value); 12 | 13 | Object getResult(); 14 | } 15 | 16 | final class RootContext implements YamlContext { 17 | private final Object result; 18 | 19 | RootContext(Object result) { 20 | this.result = result; 21 | } 22 | 23 | public void add(String key) {} 24 | 25 | public void add(Object value) {} 26 | 27 | public Object getResult() { 28 | return this.result; 29 | } 30 | } 31 | 32 | final class ObjectContext implements YamlContext { 33 | private String key = null; 34 | private final Map fields = new LinkedHashMap<>(); 35 | private final boolean skipNulls; 36 | 37 | public ObjectContext(boolean skipNulls) { 38 | this.skipNulls = skipNulls; 39 | } 40 | 41 | public void add(String key) { 42 | this.key = key; 43 | } 44 | 45 | public void add(Object value) { 46 | if (!skipNulls || value != null) { 47 | this.fields.put(this.key, value); 48 | } else { 49 | this.key = null; 50 | } 51 | } 52 | 53 | public Object getResult() { 54 | return this.fields; 55 | } 56 | } 57 | 58 | final class ArrayContext implements YamlContext { 59 | private final List values = new ArrayList<>(); 60 | 61 | public void add(String key) {} 62 | 63 | public void add(Object value) { 64 | this.values.add(value); 65 | } 66 | 67 | public Object getResult() { 68 | return this.values; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/src/main/scala/org/dhallj/tests/acceptance/AcceptanceSuite.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.tests.acceptance 2 | 3 | import java.nio.file.{Files, Path, Paths} 4 | 5 | import munit.{FunSuite, Ignore, Slow} 6 | 7 | import scala.collection.JavaConverters._ 8 | 9 | trait AcceptanceSuite extends FunSuite { 10 | def prefix: String = "./dhall-lang/tests" 11 | 12 | def base: String 13 | def recurse: Boolean = false 14 | 15 | def isInputFileName(fileName: String): Boolean = fileName.endsWith("A.dhall") 16 | def makeName(inputFileName: String): String = inputFileName.dropRight(7) 17 | 18 | /** 19 | * Test names to ignore. 20 | */ 21 | def ignored: Set[String] = Set.empty 22 | 23 | /** 24 | * Names of tests that are slow. 25 | */ 26 | def slow: Set[String] = Set.empty 27 | 28 | private def basePath: Path = Paths.get(s"$prefix/$base") 29 | 30 | /** 31 | * Returns a list of name-path pairs. 32 | */ 33 | def testInputs: List[(String, Path)] = ( 34 | if (this.recurse) Files.walk(basePath) else Files.list(basePath) 35 | ).iterator.asScala 36 | .map(path => (basePath.relativize(path).toString, path)) 37 | .flatMap { case (name, path) => 38 | if (isInputFileName(name)) { 39 | Some((makeName(name), path)) 40 | } else None 41 | } 42 | .toList 43 | .sortBy(_._2) 44 | 45 | final override def munitTests(): Seq[Test] = 46 | super 47 | .munitTests() 48 | .map(test => if (ignored(test.name)) test.tag(Ignore) else if (slow(test.name)) test.tag(Slow) else test) 49 | 50 | final protected def readString(path: Path): String = 51 | new String(readBytes(path)) 52 | 53 | final protected def readBytes(path: Path): Array[Byte] = 54 | Files.readAllBytes(path) 55 | } 56 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/core/binary/Label.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.core.binary; 2 | 3 | final class Label { 4 | public static final int APPLICATION = 0; 5 | public static final int LAMBDA = 1; 6 | public static final int PI = 2; 7 | public static final int OPERATOR_APPLICATION = 3; 8 | public static final int LIST = 4; 9 | public static final int SOME = 5; 10 | public static final int MERGE = 6; 11 | public static final int RECORD_TYPE = 7; 12 | public static final int RECORD_LITERAL = 8; 13 | public static final int FIELD_ACCESS = 9; 14 | public static final int PROJECTION = 10; 15 | public static final int UNION_TYPE = 11; 16 | public static final int IF = 14; 17 | public static final int NATURAL = 15; 18 | public static final int INTEGER = 16; 19 | public static final int TEXT = 18; 20 | public static final int ASSERT = 19; 21 | public static final int IMPORT = 24; 22 | public static final int LET = 25; 23 | public static final int ANNOTATED = 26; 24 | public static final int TO_MAP = 27; 25 | public static final int EMPTY_LIST_WITH_ABSTRACT_TYPE = 28; 26 | public static final int WITH = 29; 27 | 28 | public static final int IMPORT_TYPE_REMOTE_HTTP = 0; 29 | public static final int IMPORT_TYPE_REMOTE_HTTPS = 1; 30 | public static final int IMPORT_TYPE_LOCAL_ABSOLUTE = 2; 31 | public static final int IMPORT_TYPE_LOCAL_HERE = 3; 32 | public static final int IMPORT_TYPE_LOCAL_PARENT = 4; 33 | public static final int IMPORT_TYPE_LOCAL_HOME = 5; 34 | public static final int IMPORT_TYPE_ENV = 6; 35 | public static final int IMPORT_TYPE_MISSING = 7; 36 | // Allows CBOR tiny field encoding but leaves room for Dhall spec to expand import types 37 | public static final int IMPORT_TYPE_CLASSPATH = 23; 38 | } 39 | -------------------------------------------------------------------------------- /modules/imports/src/main/scala/org/dhallj/imports/CorsComplianceCheck.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.imports 2 | 3 | import java.net.URI 4 | 5 | import cats.effect.Sync 6 | import org.dhallj.core.DhallException.ResolutionFailure 7 | import org.http4s.Headers 8 | import org.http4s.headers.`Access-Control-Allow-Origin` 9 | 10 | object CorsComplianceCheck { 11 | 12 | def apply[F[_]](parent: ImportContext, child: ImportContext, headers: Headers)(implicit F: Sync[F]): F[Unit] = 13 | parent match { 14 | case ImportContext.Remote(uri, _) => 15 | child match { 16 | case ImportContext.Remote(uri2, _) => 17 | if (sameOrigin(uri, uri2)) 18 | F.unit 19 | else 20 | headers 21 | .get(`Access-Control-Allow-Origin`) 22 | .fold( 23 | F.raiseError[Unit]( 24 | new ResolutionFailure( 25 | s"CORS compliance failure - No Access-Control-Allow-Origin header for import $uri2 from $uri" 26 | ) 27 | ) 28 | ) { h => 29 | if (h.value.trim == "*" || sameOrigin(new URI(h.value), uri)) 30 | F.unit 31 | else 32 | F.raiseError( 33 | new ResolutionFailure( 34 | s"CORS compliance failure - ${h.value.trim} is invalid for import $uri2 from $uri" 35 | ) 36 | ) 37 | } 38 | case _ => F.unit 39 | } 40 | case _ => F.unit 41 | } 42 | 43 | private def sameOrigin(uri: URI, uri2: URI): Boolean = 44 | uri.getScheme == uri2.getScheme && uri.getAuthority == uri2.getAuthority && uri.getPort == uri2.getPort 45 | 46 | } 47 | -------------------------------------------------------------------------------- /tests/src/test/scala/org/dhallj/tests/PreludeSuite.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.tests 2 | 3 | import java.nio.file.Paths 4 | import munit.{FunSuite, Ignore, Slow, TestOptions} 5 | import org.dhallj.imports.mini.Resolver 6 | import org.dhallj.parser.DhallParser 7 | import scala.io.Source 8 | 9 | class PreludeSuite extends FunSuite() { 10 | val haskellDhallIsAvailable = HaskellDhall.isAvailable() 11 | 12 | val preludeFiles = Source.fromResource(s"Prelude").getLines().toList.sorted.flatMap { 13 | case "Monoid" => Nil 14 | case "Monoid.dhall" => List("Prelude/Monoid.dhall") 15 | case "README.md" => Nil 16 | case "package.dhall" => List("Prelude/package.dhall") 17 | case other => 18 | Source.fromResource(s"Prelude/$other").getLines().toList.sorted.map { case file => 19 | s"Prelude/$other/$file" 20 | } 21 | } 22 | 23 | def slow: Set[String] = Set( 24 | "Prelude/JSON/render", 25 | "Prelude/JSON/renderAs", 26 | "Prelude/JSON/renderYAML", 27 | "Prelude/JSON/package.dhall", 28 | "Prelude/package.dhall" 29 | ) 30 | 31 | def checkHash(path: String)(implicit loc: munit.Location): Unit = { 32 | val content = Source.fromResource(path).getLines().mkString("\n") 33 | val parsed = DhallParser.parse(content) 34 | 35 | val resolved = Resolver.resolveFromResources(parsed, false, Paths.get(path), this.getClass.getClassLoader) 36 | 37 | val name = s"$path hash matches dhall hash" 38 | 39 | val testOptions: TestOptions = if (haskellDhallIsAvailable) { 40 | if (slow(path)) name.tag(Slow) else name 41 | } else name.tag(Ignore) 42 | 43 | test(testOptions)(assert(resolved.normalize.alphaNormalize.hash == clue(HaskellDhall.hash(content)))) 44 | } 45 | 46 | preludeFiles.filterNot(_.endsWith("dhall")).foreach(checkHash) 47 | } 48 | -------------------------------------------------------------------------------- /cli/src/main/java/org/dhallj/cli/Dhall.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.cli; 2 | 3 | import java.io.IOException; 4 | import org.dhallj.core.Expr; 5 | import org.dhallj.core.converters.JsonConverter; 6 | import org.dhallj.imports.mini.Resolver; 7 | import org.dhallj.parser.DhallParser; 8 | 9 | public class Dhall { 10 | public static void main(String[] args) throws IOException { 11 | boolean resolveImports = false; 12 | boolean typeCheck = false; 13 | boolean normalize = false; 14 | boolean alphaNormalize = false; 15 | 16 | for (int i = 1; i < args.length; i++) { 17 | if (args[i].equals("--resolve")) { 18 | resolveImports = true; 19 | } else if (args[i].equals("--type-check")) { 20 | typeCheck = true; 21 | } else if (args[i].equals("--normalize")) { 22 | normalize = true; 23 | } else if (args[i].equals("--alpha")) { 24 | alphaNormalize = true; 25 | } 26 | } 27 | 28 | Expr expr = DhallParser.parse(System.in); 29 | Expr type = null; 30 | 31 | if (resolveImports) { 32 | expr = Resolver.resolve(expr, false); 33 | } 34 | 35 | if (normalize) { 36 | expr = expr.normalize(); 37 | } 38 | 39 | if (alphaNormalize) { 40 | expr = expr.alphaNormalize(); 41 | } 42 | 43 | if (typeCheck) { 44 | type = Expr.Util.typeCheck(expr); 45 | } 46 | 47 | if (args.length == 0 || args[0].startsWith("--")) { 48 | System.out.println(expr); 49 | } else if (args[0].equals("hash")) { 50 | System.out.printf("sha256:%s\n", expr.hash()); 51 | } else if (args[0].equals("type")) { 52 | if (!typeCheck) { 53 | type = Expr.Util.typeCheck(expr); 54 | } 55 | System.out.println(type); 56 | } else if (args[0].equals("json")) { 57 | System.out.println(JsonConverter.toCompactString(expr)); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Continuous Integration 9 | 10 | on: 11 | pull_request: 12 | branches: ['*'] 13 | push: 14 | branches: ['*'] 15 | 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | 19 | jobs: 20 | build: 21 | name: Build and Test 22 | strategy: 23 | matrix: 24 | os: [ubuntu-latest] 25 | scala: [2.12.12, 2.13.5] 26 | java: [adopt@1.8] 27 | runs-on: ${{ matrix.os }} 28 | steps: 29 | - name: Checkout current branch (full) 30 | uses: actions/checkout@v2 31 | with: 32 | fetch-depth: 0 33 | submodules: recursive 34 | 35 | - name: Setup Java and Scala 36 | uses: olafurpg/setup-scala@v10 37 | with: 38 | java-version: ${{ matrix.java }} 39 | 40 | - name: Cache sbt 41 | uses: actions/cache@v2 42 | with: 43 | path: | 44 | ~/.sbt 45 | ~/.ivy2/cache 46 | ~/.coursier/cache/v1 47 | ~/.cache/coursier/v1 48 | ~/AppData/Local/Coursier/Cache/v1 49 | ~/Library/Caches/Coursier/v1 50 | key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} 51 | 52 | - name: Check that workflows are up to date 53 | run: sbt ++${{ matrix.scala }} githubWorkflowCheck 54 | 55 | - name: Test 56 | run: 'sbt ++${{ matrix.scala }} clean javacc coverage scalastyle scalafmtCheckAll scalafmtSbtCheck test slow:test coverageReport' 57 | 58 | - uses: codecov/codecov-action@v1 -------------------------------------------------------------------------------- /modules/imports/src/main/scala/org/dhallj/imports/ToHeaders.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.imports 2 | 3 | import org.dhallj.core.Expr 4 | import org.dhallj.core.Expr.Util.{asListLiteral, asRecordLiteral, asSimpleTextLiteral} 5 | import org.http4s.{Header, Headers} 6 | 7 | import scala.collection.JavaConverters._ 8 | 9 | object ToHeaders { 10 | 11 | // See https://discourse.dhall-lang.org/t/valid-expressions-for-using-headers/205 12 | // For the moment, this is consistent with the Haskell implementation 13 | def apply(expr: Expr): Headers = 14 | if (expr eq null) Headers.empty 15 | else { 16 | //TODO do we need to .accept(this) on expr? 17 | val e = expr.normalize 18 | val l = asListLiteral(e) 19 | if (l eq null) { 20 | Headers.empty 21 | } else { 22 | val hs: List[Header] = l.asScala.toList.flatMap { e => 23 | // e should have type `List { header : Text, value Text }` 24 | // or `List { mapKey : Text, mapValue Text }` 25 | val r = asRecordLiteral(e) 26 | if (r eq null) { 27 | None 28 | } else { 29 | if (r.size == 2) { 30 | val map = r.asScala.map(e => e.getKey -> e.getValue).toMap 31 | if (map.contains("header") && map.contains("value")) { 32 | val key = asSimpleTextLiteral(map("header")) 33 | val value = asSimpleTextLiteral(map("value")) 34 | 35 | if ((key ne null) && (value ne null)) { 36 | Some(Header(key, value)) 37 | } else None 38 | } else if (map.contains("mapKey") && map.contains("mapValue")) { 39 | val key = asSimpleTextLiteral(map("mapKey")) 40 | val value = asSimpleTextLiteral(map("mapValue")) 41 | 42 | if ((key ne null) && (value ne null)) { 43 | Some(Header(key, value)) 44 | } else None 45 | } else None 46 | } else None 47 | } 48 | } 49 | Headers(hs) 50 | } 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /tests/src/main/scala/org/dhallj/tests/acceptance/ImportResolutionSuite.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.tests.acceptance 2 | 3 | import java.nio.file.{Files, Paths} 4 | 5 | import cats.effect.{ContextShift, IO} 6 | 7 | import org.dhallj.core.Expr 8 | import org.dhallj.imports.{ImportCache, Resolver} 9 | import org.dhallj.parser.DhallParser 10 | 11 | import org.http4s.client._ 12 | import org.http4s.client.blaze._ 13 | 14 | import scala.concurrent.ExecutionContext.global 15 | import scala.io.Source 16 | 17 | class ImportResolutionSuite(val base: String) 18 | extends ExprOperationAcceptanceSuite(_.normalize) 19 | with CachedResolvingInput { 20 | 21 | setEnv("DHALL_TEST_VAR", "6 * 7") //Yes, this is SUPER hacky but the JVM doesn't really support setting env vars 22 | 23 | override def parseInput(path: String, input: String): Expr = { 24 | val parsed = DhallParser.parse(s"./$path") 25 | 26 | if (parsed.isResolved) parsed 27 | else { 28 | implicit val cs: ContextShift[IO] = IO.contextShift(global) 29 | val cache = initializeCache 30 | BlazeClientBuilder[IO](global).resource 31 | .use { client => 32 | implicit val c: Client[IO] = client 33 | Resolver.resolve[IO](cache, new ImportCache.NoopImportCache[IO])(parsed) 34 | } 35 | .unsafeRunSync() 36 | } 37 | } 38 | 39 | private def initializeCache: ImportCache[IO] = { 40 | val dir = Files.createTempDirectory("dhallj") 41 | val cache = ImportCache[IO](dir).unsafeRunSync().get 42 | Source 43 | .fromResource("tests/import/cache/dhall") 44 | .getLines() 45 | .foreach { p => 46 | val content = readBytes(Paths.get(s"dhall-lang/tests/import/cache/dhall/$p")) 47 | Files.write(dir.resolve(p), content) 48 | } 49 | cache 50 | } 51 | 52 | def setEnv(key: String, value: String): Unit = { 53 | val field = System.getenv().getClass.getDeclaredField("m") 54 | field.setAccessible(true) 55 | val map = field.get(System.getenv()).asInstanceOf[java.util.Map[java.lang.String, java.lang.String]] 56 | map.put(key, value) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/cbor/NullVisitor.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.cbor; 2 | 3 | import java.math.BigInteger; 4 | 5 | /** To read a CBOR primitive and ensure it is null. */ 6 | final class NullVisitor implements Visitor { 7 | static final Visitor instanceForString = new NullVisitor(); 8 | static final Visitor instanceForByteArray = new NullVisitor(); 9 | 10 | @Override 11 | public R onUnsignedInteger(BigInteger value) { 12 | return notExpected("Unsigned integer"); 13 | } 14 | 15 | @Override 16 | public R onNegativeInteger(BigInteger value) { 17 | return notExpected("Negative integer"); 18 | } 19 | 20 | @Override 21 | public R onByteString(byte[] value) { 22 | return notExpected("Byte string"); 23 | } 24 | 25 | @Override 26 | public R onTextString(String value) { 27 | return notExpected("Text string"); 28 | } 29 | 30 | @Override 31 | public R onVariableArray(BigInteger length, String name) { 32 | return notExpected("Variable array"); 33 | } 34 | 35 | @Override 36 | public R onArray(BigInteger length, BigInteger tagI) { 37 | return notExpected("Array"); 38 | } 39 | 40 | @Override 41 | public R onMap(BigInteger size) { 42 | return notExpected("Map"); 43 | } 44 | 45 | @Override 46 | public R onFalse() { 47 | return notExpected("False"); 48 | } 49 | 50 | @Override 51 | public R onTrue() { 52 | return notExpected("True"); 53 | } 54 | 55 | @Override 56 | public R onNull() { 57 | return null; 58 | } 59 | 60 | @Override 61 | public R onHalfFloat(float value) { 62 | return notExpected("Half float"); 63 | } 64 | 65 | @Override 66 | public R onSingleFloat(float value) { 67 | return notExpected("Single float"); 68 | } 69 | 70 | @Override 71 | public R onDoubleFloat(double value) { 72 | return notExpected("Double float"); 73 | } 74 | 75 | @Override 76 | public R onTag() { 77 | return null; 78 | } 79 | 80 | private R notExpected(String msg) { 81 | throw new CborException(msg + " not expected - expected null"); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/core/normalization/Shift.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.core.normalization; 2 | 3 | import java.math.BigInteger; 4 | import java.net.URI; 5 | import java.nio.file.Path; 6 | import java.util.ArrayList; 7 | import java.util.Arrays; 8 | import java.util.Iterator; 9 | import java.util.List; 10 | import java.util.Map.Entry; 11 | import org.dhallj.core.Expr; 12 | import org.dhallj.core.Operator; 13 | import org.dhallj.core.Source; 14 | import org.dhallj.core.Visitor; 15 | 16 | /** 17 | * Shifts all instances of a variable. 18 | * 19 | *

Note that this visitor maintains internal state and instances should not be reused. 20 | */ 21 | public final class Shift extends Visitor.Identity { 22 | private final int change; 23 | private final String name; 24 | private int cutoff = 0; 25 | 26 | public Shift(boolean isIncrement, String name) { 27 | this.change = isIncrement ? 1 : -1; 28 | this.name = name; 29 | } 30 | 31 | @Override 32 | public Expr onIdentifier(Expr self, String name, long index) { 33 | if (name.equals(this.name) && index >= this.cutoff) { 34 | return Expr.makeIdentifier(name, index + this.change); 35 | } else { 36 | return self; 37 | } 38 | } 39 | 40 | @Override 41 | public void bind(String name, Expr type) { 42 | if (name.equals(this.name)) { 43 | this.cutoff += 1; 44 | } 45 | } 46 | 47 | @Override 48 | public Expr onLambda(String name, Expr type, Expr result) { 49 | if (name.equals(this.name)) { 50 | this.cutoff -= 1; 51 | } 52 | 53 | return Expr.makeLambda(name, type, result); 54 | } 55 | 56 | @Override 57 | public Expr onPi(String name, Expr type, Expr result) { 58 | if (name.equals(this.name)) { 59 | this.cutoff -= 1; 60 | } 61 | 62 | return Expr.makePi(name, type, result); 63 | } 64 | 65 | @Override 66 | public Expr onLet(List> bindings, Expr body) { 67 | for (Expr.LetBinding binding : bindings) { 68 | if (binding.getName().equals(this.name)) { 69 | this.cutoff -= 1; 70 | } 71 | } 72 | return Expr.makeLet(bindings, body); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /modules/circe/src/main/scala/org/dhallj/circe/Converter.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.circe 2 | 3 | import io.circe.{Json, JsonNumber, JsonObject} 4 | import java.math.BigInteger 5 | import java.util.AbstractMap.SimpleImmutableEntry 6 | import java.util.Map.Entry 7 | import org.dhallj.core.Expr 8 | import org.dhallj.core.converters.JsonConverter 9 | 10 | object Converter { 11 | def apply(expr: Expr): Option[Json] = { 12 | val handler = new CirceHandler() 13 | val wasConverted = expr.accept(new JsonConverter(handler, false)) 14 | 15 | if (wasConverted) Some(handler.result) else None 16 | } 17 | 18 | private[this] val folder: Json.Folder[Expr] = new Json.Folder[Expr] { 19 | def onBoolean(value: Boolean): Expr = if (value) Expr.Constants.TRUE else Expr.Constants.FALSE 20 | def onNull: Expr = Expr.makeApplication(Expr.Constants.NONE, Expr.Constants.EMPTY_RECORD_TYPE) 21 | def onNumber(value: JsonNumber): Expr = { 22 | val asDouble = value.toDouble 23 | 24 | if (java.lang.Double.compare(asDouble, -0.0) == 0) { 25 | Expr.makeDoubleLiteral(asDouble) 26 | } else 27 | value.toBigInt match { 28 | case Some(integer) => 29 | if (integer.underlying.compareTo(BigInteger.ZERO) > 0) { 30 | Expr.makeNaturalLiteral(integer.underlying) 31 | } else { 32 | Expr.makeIntegerLiteral(integer.underlying) 33 | } 34 | case None => Expr.makeDoubleLiteral(asDouble) 35 | } 36 | } 37 | def onString(value: String): Expr = Expr.makeTextLiteral(value) 38 | def onObject(value: JsonObject): Expr = Expr.makeRecordLiteral( 39 | value.toMap.map { case (k, v) => 40 | new SimpleImmutableEntry(k, v.foldWith(this)): Entry[String, Expr] 41 | }.toArray 42 | ) 43 | def onArray(value: Vector[Json]): Expr = 44 | if (value.isEmpty) { 45 | Expr.makeEmptyListLiteral(Expr.makeApplication(Expr.Constants.LIST, Expr.Constants.EMPTY_RECORD_TYPE)) 46 | } else { 47 | Expr.makeNonEmptyListLiteral(value.map(_.foldWith(this)).toArray) 48 | } 49 | } 50 | 51 | def apply(json: Json): Expr = json.foldWith(folder) 52 | } 53 | -------------------------------------------------------------------------------- /modules/imports/src/test/scala/org/dhallj/imports/ReferentialSanityCheckSuite.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.imports 2 | 3 | import java.net.URI 4 | import java.nio.file.Paths 5 | 6 | import cats.effect.IO 7 | import munit.FunSuite 8 | import org.dhallj.imports.ImportContext._ 9 | 10 | class ReferentialSanityCheckSuite extends FunSuite { 11 | 12 | private val someUri = new URI("http://example.com/foo.dhall") 13 | private val somePath = Paths.get("/foo.dhall") 14 | 15 | test("Remote imports local".fail) { 16 | ReferentialSanityCheck[IO](Remote(someUri, null), Local(somePath)).unsafeRunSync() 17 | } 18 | 19 | test("Remote imports env".fail) { 20 | ReferentialSanityCheck[IO](Remote(someUri, null), Env("foo")).unsafeRunSync() 21 | } 22 | 23 | test("Remote imports remote") { 24 | ReferentialSanityCheck[IO](Remote(someUri, null), Remote(someUri, null)).unsafeRunSync() 25 | } 26 | 27 | test("Remote imports missing") { 28 | ReferentialSanityCheck[IO](Remote(someUri, null), Missing).unsafeRunSync() 29 | } 30 | 31 | test("Remote imports classpath".fail) { 32 | ReferentialSanityCheck[IO](Remote(someUri, null), Classpath(somePath)).unsafeRunSync() 33 | } 34 | 35 | test("Local imports local") { 36 | ReferentialSanityCheck[IO](Local(somePath), Local(somePath)).unsafeRunSync() 37 | } 38 | 39 | test("Local imports env") { 40 | ReferentialSanityCheck[IO](Local(somePath), Env("foo")).unsafeRunSync() 41 | } 42 | 43 | test("Local imports remote") { 44 | ReferentialSanityCheck[IO](Local(somePath), Remote(someUri, null)).unsafeRunSync() 45 | } 46 | 47 | test("Local imports missing") { 48 | ReferentialSanityCheck[IO](Local(somePath), Missing).unsafeRunSync() 49 | } 50 | 51 | test("Env imports local") { 52 | ReferentialSanityCheck[IO](Env("foo"), Local(somePath)).unsafeRunSync() 53 | } 54 | 55 | test("Env imports env") { 56 | ReferentialSanityCheck[IO](Env("foo"), Env("foo")).unsafeRunSync() 57 | } 58 | 59 | test("Env imports remote") { 60 | ReferentialSanityCheck[IO](Env("foo"), Remote(someUri, null)).unsafeRunSync() 61 | } 62 | 63 | test("Env imports missing") { 64 | ReferentialSanityCheck[IO](Env("foo"), Missing).unsafeRunSync() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/core/normalization/BetaNormalizeToMap.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.core.normalization; 2 | 3 | import java.util.AbstractMap.SimpleImmutableEntry; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import java.util.Map.Entry; 7 | import org.dhallj.core.Expr; 8 | 9 | final class BetaNormalizeToMap { 10 | static final Expr apply(Expr base, Expr type) { 11 | List> baseAsRecordLiteral = Expr.Util.asRecordLiteral(base); 12 | 13 | if (baseAsRecordLiteral != null) { 14 | if (baseAsRecordLiteral.size() == 0) { 15 | return Expr.makeEmptyListLiteral(type); 16 | } else { 17 | Expr[] result = new Expr[baseAsRecordLiteral.size()]; 18 | 19 | for (int i = 0; i < baseAsRecordLiteral.size(); i++) { 20 | Entry entry = baseAsRecordLiteral.get(i); 21 | 22 | result[i] = makeRecord(entry.getKey(), entry.getValue()); 23 | } 24 | 25 | return Expr.makeNonEmptyListLiteral(result); 26 | } 27 | } else { 28 | return Expr.makeToMap(base, type); 29 | } 30 | } 31 | 32 | private static final String escape(String input) { 33 | StringBuilder builder = new StringBuilder(); 34 | 35 | for (int i = 0; i < input.length(); i++) { 36 | char c = input.charAt(i); 37 | 38 | if (c == '\\') { 39 | char next = input.charAt(++i); 40 | 41 | if (next == '\\') { 42 | builder.append("\\\\"); 43 | } else if (next == 'n') { 44 | builder.append("\\\\n"); 45 | } else if (next == '$') { 46 | builder.append("\\\\\\$"); 47 | } else { 48 | builder.append("\\\\"); 49 | builder.append(next); 50 | } 51 | } else { 52 | builder.append(c); 53 | } 54 | } 55 | 56 | return builder.toString(); 57 | } 58 | 59 | private static final Expr makeRecord(String key, Expr value) { 60 | Entry[] fields = { 61 | new SimpleImmutableEntry<>( 62 | Expr.Constants.MAP_KEY_FIELD_NAME, Expr.makeTextLiteral(escape(key))), 63 | new SimpleImmutableEntry<>(Expr.Constants.MAP_VALUE_FIELD_NAME, value) 64 | }; 65 | 66 | return Expr.makeRecordLiteral(fields); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/core/Source.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.core; 2 | 3 | /** Represents a section of a source document corresponding to a parsed expression. */ 4 | public abstract class Source { 5 | final int beginLine; 6 | final int beginColumn; 7 | final int endLine; 8 | final int endColumn; 9 | 10 | public Source(int beginLine, int beginColumn, int endLine, int endColumn) { 11 | this.beginLine = beginLine; 12 | this.beginColumn = beginColumn; 13 | this.endLine = endLine; 14 | this.endColumn = endColumn; 15 | } 16 | 17 | public abstract void printText(StringBuilder builder); 18 | 19 | public final String getText() { 20 | StringBuilder builder = new StringBuilder(); 21 | this.printText(builder); 22 | return builder.toString(); 23 | } 24 | 25 | public final int getBeginLine() { 26 | return this.beginLine; 27 | } 28 | 29 | public final int getBeginColumn() { 30 | return this.beginColumn; 31 | } 32 | 33 | public final int getEndLine() { 34 | return this.endLine; 35 | } 36 | 37 | public final int getEndColumn() { 38 | return this.endColumn; 39 | } 40 | 41 | public final String toString() { 42 | StringBuilder builder = new StringBuilder("[("); 43 | builder.append(this.beginLine); 44 | builder.append(", "); 45 | builder.append(this.beginColumn); 46 | builder.append(") ("); 47 | builder.append(this.endLine); 48 | builder.append(", "); 49 | builder.append(this.endColumn); 50 | builder.append(")]\n"); 51 | this.printText(builder); 52 | return builder.toString(); 53 | } 54 | 55 | private static final class FromString extends Source { 56 | private final String text; 57 | 58 | FromString(String text, int beginLine, int beginColumn, int endLine, int endColumn) { 59 | super(beginLine, beginColumn, endLine, endColumn); 60 | this.text = text; 61 | } 62 | 63 | public final void printText(StringBuilder builder) { 64 | builder.append(this.text); 65 | } 66 | } 67 | 68 | public static final Source fromString( 69 | String text, int beginLine, int beginColumn, int endLine, int endColumn) { 70 | return new FromString(text, beginLine, beginColumn, endLine, endColumn); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /modules/yaml/src/main/java/org/dhallj/yaml/YamlHandler.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.yaml; 2 | 3 | import java.math.BigInteger; 4 | import java.util.ArrayDeque; 5 | import java.util.Deque; 6 | import org.dhallj.core.converters.JsonHandler; 7 | 8 | public class YamlHandler implements JsonHandler { 9 | private final Deque stack = new ArrayDeque<>(); 10 | private final boolean skipNulls; 11 | 12 | public YamlHandler(boolean skipNulls) { 13 | this.skipNulls = skipNulls; 14 | } 15 | 16 | public YamlHandler() { 17 | this(true); 18 | } 19 | 20 | private final void addValue(Object value) { 21 | if (stack.isEmpty()) { 22 | stack.push(new RootContext(value)); 23 | } else { 24 | stack.peek().add(value); 25 | } 26 | } 27 | 28 | public Object getResult() { 29 | return this.stack.pop().getResult(); 30 | } 31 | 32 | public void onNull() { 33 | this.addValue(null); 34 | } 35 | 36 | public void onBoolean(boolean value) { 37 | this.addValue(value); 38 | } 39 | 40 | public void onNumber(BigInteger value) { 41 | this.addValue(value); 42 | } 43 | 44 | public void onDouble(double value) { 45 | this.addValue(value); 46 | } 47 | 48 | public void onString(String value) { 49 | this.addValue(value.replaceAll("\\\\n", "\n").replaceAll("\\\\\"", "\"")); 50 | } 51 | 52 | public void onArrayStart() { 53 | this.stack.push(new ArrayContext()); 54 | } 55 | 56 | public void onArrayEnd() { 57 | YamlContext current = this.stack.pop(); 58 | 59 | if (this.stack.isEmpty()) { 60 | this.stack.push(current); 61 | } else { 62 | this.stack.peek().add(current.getResult()); 63 | } 64 | } 65 | 66 | public void onArrayElementGap() {} 67 | 68 | public void onObjectStart() { 69 | this.stack.push(new ObjectContext(this.skipNulls)); 70 | } 71 | 72 | public void onObjectEnd() { 73 | YamlContext current = this.stack.pop(); 74 | 75 | if (this.stack.isEmpty()) { 76 | this.stack.push(current); 77 | } else { 78 | this.stack.peek().add(current.getResult()); 79 | } 80 | } 81 | 82 | public void onObjectField(String name) { 83 | this.stack.peek().add(name); 84 | } 85 | 86 | public void onObjectFieldGap() {} 87 | } 88 | -------------------------------------------------------------------------------- /modules/jawn/src/main/scala/org/dhallj/jawn/FacadeHandler.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.jawn 2 | 3 | import java.math.BigInteger 4 | import java.util.{ArrayDeque, Deque} 5 | import org.dhallj.core.converters.JsonHandler 6 | import org.typelevel.jawn.{FContext, Facade} 7 | 8 | class FacadeHandler[J](facade: Facade[J]) extends JsonHandler { 9 | final protected[this] val stack: Deque[FContext[J]] = new ArrayDeque() 10 | final protected[this] val position: Int = 0 11 | 12 | protected[this] def addValue(value: J): Unit = { 13 | if (stack.isEmpty) { 14 | stack.push(facade.singleContext(position)) 15 | } 16 | stack.peek.add(value, position) 17 | } 18 | 19 | final def result: J = stack.pop().finish(position) 20 | 21 | final def onNull(): Unit = addValue(facade.jnull(position)) 22 | final def onBoolean(value: Boolean): Unit = addValue(if (value) facade.jtrue(position) else facade.jfalse(position)) 23 | def onNumber(value: BigInteger): Unit = addValue(facade.jnum(value.toString(), -1, -1, position)) 24 | def onDouble(value: Double): Unit = 25 | if (java.lang.Double.isFinite(value)) { 26 | val asString = java.lang.Double.toString(value) 27 | 28 | addValue(facade.jnum(asString, asString.indexOf('.'), asString.indexOf('E'), position)) 29 | } else { 30 | addValue(facade.jnull(position)) 31 | } 32 | 33 | def onString(value: String): Unit = addValue(facade.jstring(value, position)) 34 | 35 | final def onArrayStart(): Unit = stack.push(facade.arrayContext(position)) 36 | 37 | final def onArrayEnd(): Unit = { 38 | val current = stack.pop() 39 | 40 | if (stack.isEmpty) { 41 | stack.push(current) 42 | } else { 43 | stack.peek.add(current.finish(position), position) 44 | } 45 | } 46 | 47 | final def onArrayElementGap(): Unit = () 48 | 49 | final def onObjectStart(): Unit = stack.push(facade.objectContext(position)) 50 | 51 | final def onObjectEnd(): Unit = { 52 | val current = stack.pop() 53 | 54 | if (stack.isEmpty) { 55 | stack.push(current) 56 | } else { 57 | stack.peek.add(current.finish(position), position) 58 | } 59 | } 60 | 61 | final def onObjectField(name: String): Unit = stack.peek.add(name, position) 62 | 63 | final def onObjectFieldGap(): Unit = () 64 | } 65 | -------------------------------------------------------------------------------- /tests/src/test/scala/org/dhallj/tests/JsonConverterSuite.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.tests 2 | 3 | import munit.FunSuite 4 | import org.dhallj.core.converters.JsonConverter 5 | import org.dhallj.parser.DhallParser 6 | 7 | class JsonConverterSuite extends FunSuite() { 8 | test("toCompactString correctly escapes text") { 9 | val expr = DhallParser.parse("""[{mapKey = " \n \$ \" ", mapValue = " \n \$ \" "}]""") 10 | 11 | assert(clue(JsonConverter.toCompactString(expr)) == clue("""{" \n $ \" ":" \n $ \" "}""")) 12 | } 13 | 14 | test("toCompactString correctly escapes text from Text/show") { 15 | val expr = DhallParser.parse("""Text/show "$100 #"""").normalize 16 | 17 | assert(clue(JsonConverter.toCompactString(expr)) == clue(""""\"\\u0024100 #\""""")) 18 | } 19 | 20 | test("toCompactString correctly escapes text from toMap") { 21 | val expr = DhallParser.parse("""toMap {` \n \$ \" ` = " \n \$ \" "}""").normalize 22 | 23 | assert(clue(JsonConverter.toCompactString(expr)) == clue("""{" \\n \\$ \\\" ":" \n $ \" "}""")) 24 | } 25 | 26 | test("toCompactString flattens toMap-formatted lists") { 27 | val expr = DhallParser.parse( 28 | """[{ mapKey = "foo", mapValue = 1}, {mapKey = "bar", mapValue = 2}]""" 29 | ) 30 | 31 | assert(clue(JsonConverter.toCompactString(expr)) == """{"foo":1,"bar":2}""") 32 | } 33 | 34 | test("toCompactString flattens toMap-typed empty lists") { 35 | val expr = DhallParser.parse( 36 | """[]: List { mapKey: Text, mapValue: Integer }""" 37 | ) 38 | 39 | assert(clue(JsonConverter.toCompactString(expr)) == """{}""") 40 | } 41 | 42 | test("toCompactString keeps last value in case of toMap-format duplicates") { 43 | val expr = DhallParser.parse( 44 | """[{ mapKey = "foo", mapValue = 100}, {mapKey = "foo", mapValue = 2 }]""" 45 | ) 46 | 47 | assert(clue(JsonConverter.toCompactString(expr)) == """{"foo":2}""") 48 | } 49 | 50 | test("toCompactString doesn't flatten near-toMap-format lists") { 51 | val expr = DhallParser.parse( 52 | """[{ mapKey = "foo", mapValue = 1}, {mapKey = "bar", other = 1, mapValue = 2}]""" 53 | ) 54 | 55 | val expected = """[{"mapKey":"foo","mapValue":1},{"mapKey":"bar","other":1,"mapValue":2}]""" 56 | 57 | assert(clue(JsonConverter.toCompactString(expr)) == clue(expected)) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/cbor/HalfFloat.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.cbor; 2 | 3 | /** 4 | * Conversions between 16 and 32-bit floating point numbers. 5 | * 6 | * @see this Stack Overflow answer 7 | */ 8 | public final class HalfFloat { 9 | public static final int fromFloat(float asFloat) { 10 | int bits = Float.floatToIntBits(asFloat); 11 | int sign = bits >>> 16 & 0x8000; // sign only 12 | int value = (bits & 0x7fffffff) + 0x1000; // rounded value 13 | 14 | if (value >= 0x47800000) // might be or become NaN/Inf 15 | { // avoid Inf due to rounding 16 | 17 | if (value < 0x7f800000) // was value but too large 18 | return sign | 0x7c00; // make it +/-Inf 19 | return sign 20 | | 0x7c00 21 | | // remains +/-Inf or NaN 22 | (bits & 0x007fffff) >>> 13; // keep NaN (and Inf) bits 23 | } 24 | if (value >= 0x38800000) { // remains normalized value 25 | return sign | value - 0x38000000 >>> 13; // exp - 127 + 15 26 | } 27 | if (value < 0x33000000) { // too small for subnormal 28 | return sign; // becomes +/-0 29 | } 30 | value = (bits & 0x7fffffff) >>> 23; // tmp exp for subnormal calc 31 | 32 | return sign 33 | | ((bits & 0x7fffff | 0x800000) // add subnormal bit 34 | + (0x800000 >>> value - 102) // round depending on cut off 35 | >>> 126 - value); // div by 2^(1-(exp-127+15)) and >> 13 | exp=0 36 | } 37 | 38 | public static final float toFloat(int bits) { 39 | int mant = bits & 0x03ff; // 10 bits mantissa 40 | int exp = bits & 0x7c00; // 5 bits exponent 41 | if (exp == 0x7c00) { // NaN/Inf 42 | exp = 0x3fc00; // -> NaN/Inf 43 | } else if (exp != 0) { // normalized value 44 | exp += 0x1c000; // exp - 15 + 127 45 | } else if (mant != 0) { // && exp==0 -> subnormal 46 | exp = 0x1c400; // make it normal 47 | do { 48 | mant <<= 1; // mantissa * 2 49 | exp -= 0x400; // decrease exp by 1 50 | } while ((mant & 0x400) == 0); // while not normal 51 | mant &= 0x3ff; // discard subnormal bit 52 | } // else +/-0 -> +/-0 53 | return Float.intBitsToFloat( // combine all parts 54 | (bits & 0x8000) << 16 // sign << ( 31 - 15 ) 55 | | (exp | mant) << 13); // value << ( 23 - 10 ) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /modules/imports/src/test/scala/org/dhallj/imports/ToHeadersSuite.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.imports 2 | 3 | import munit.FunSuite 4 | import org.dhallj.core.Expr 5 | import org.http4s.{Header, Headers} 6 | 7 | import scala.collection.JavaConverters._ 8 | 9 | class ToHeadersSuite extends FunSuite { 10 | 11 | test("Success case") { 12 | val expr = Expr.makeNonEmptyListLiteral( 13 | Array( 14 | Expr.makeRecordLiteral( 15 | Map("header" -> Expr.makeTextLiteral("foo"), "value" -> Expr.makeTextLiteral("bar")).asJava.entrySet() 16 | ), 17 | Expr.makeRecordLiteral( 18 | Map("header" -> Expr.makeTextLiteral("baz"), "value" -> Expr.makeTextLiteral("x")).asJava.entrySet() 19 | ) 20 | ) 21 | ) 22 | 23 | val expected = Headers( 24 | List( 25 | Header("foo", "bar"), 26 | Header("baz", "x") 27 | ) 28 | ) 29 | 30 | assertEquals(ToHeaders(expr), expected) 31 | } 32 | 33 | test("Success case 2") { 34 | val expr = Expr.makeNonEmptyListLiteral( 35 | Array( 36 | Expr.makeRecordLiteral( 37 | Map("mapKey" -> Expr.makeTextLiteral("foo"), "mapValue" -> Expr.makeTextLiteral("bar")).asJava.entrySet() 38 | ), 39 | Expr.makeRecordLiteral( 40 | Map("mapKey" -> Expr.makeTextLiteral("baz"), "mapValue" -> Expr.makeTextLiteral("x")).asJava.entrySet() 41 | ) 42 | ) 43 | ) 44 | 45 | val expected = Headers( 46 | List( 47 | Header("foo", "bar"), 48 | Header("baz", "x") 49 | ) 50 | ) 51 | 52 | assertEquals(ToHeaders(expr), expected) 53 | } 54 | 55 | test("Failure case 1") { 56 | val expr = Expr.makeNonEmptyListLiteral( 57 | Array( 58 | Expr.makeRecordLiteral( 59 | Map("header" -> Expr.makeTextLiteral("foo"), "mapValue" -> Expr.makeTextLiteral("bar")).asJava.entrySet() 60 | ), 61 | Expr.makeRecordLiteral( 62 | Map("mapKey" -> Expr.makeTextLiteral("baz"), "value" -> Expr.makeTextLiteral("x")).asJava.entrySet() 63 | ) 64 | ) 65 | ) 66 | 67 | val expected = Headers(Nil) 68 | 69 | assertEquals(ToHeaders(expr), expected) 70 | } 71 | 72 | test("Failure case 2") { 73 | val expr = Expr.makeTextLiteral("foo") 74 | 75 | val expected = Headers(Nil) 76 | 77 | assertEquals(ToHeaders(expr), expected) 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/core/normalization/Substitute.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.core.normalization; 2 | 3 | import java.util.ArrayDeque; 4 | import java.util.Deque; 5 | import java.util.List; 6 | import org.dhallj.core.Expr; 7 | import org.dhallj.core.Visitor; 8 | 9 | /** 10 | * Substitutes an expression for all instances of a variable in another expression. 11 | * 12 | *

Note that this visitor maintains internal state and instances should not be reused. 13 | */ 14 | public final class Substitute extends Visitor.Identity { 15 | private final String name; 16 | private int index = 0; 17 | private final Deque replacementStack = new ArrayDeque<>(); 18 | 19 | public Substitute(String name, Expr replacement) { 20 | this.name = name; 21 | this.replacementStack.push(replacement); 22 | } 23 | 24 | @Override 25 | public void bind(String name, Expr type) { 26 | this.replacementStack.push(this.replacementStack.peekFirst().increment(name)); 27 | 28 | if (name.equals(this.name)) { 29 | this.index += 1; 30 | } 31 | } 32 | 33 | @Override 34 | public Expr onIdentifier(Expr self, String name, long index) { 35 | if (name.equals(this.name)) { 36 | if (index == this.index) { 37 | return this.replacementStack.peekFirst(); 38 | } else if (index > this.index) { 39 | return Expr.makeIdentifier(name, index - 1); 40 | } else { 41 | return self; 42 | } 43 | } else { 44 | return self; 45 | } 46 | } 47 | 48 | @Override 49 | public Expr onLambda(String name, Expr type, Expr result) { 50 | this.replacementStack.pop(); 51 | 52 | if (name.equals(this.name)) { 53 | this.index -= 1; 54 | } 55 | 56 | return Expr.makeLambda(name, type, result); 57 | } 58 | 59 | @Override 60 | public Expr onPi(String name, Expr type, Expr result) { 61 | this.replacementStack.pop(); 62 | 63 | if (name.equals(this.name)) { 64 | this.index -= 1; 65 | } 66 | 67 | return Expr.makePi(name, type, result); 68 | } 69 | 70 | @Override 71 | public Expr onLet(List> bindings, Expr body) { 72 | for (Expr.LetBinding binding : bindings) { 73 | String name = binding.getName(); 74 | 75 | this.replacementStack.pop(); 76 | 77 | if (name.equals(this.name)) { 78 | this.index -= 1; 79 | } 80 | } 81 | return Expr.makeLet(bindings, body); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /.github/workflows/clean.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Clean 9 | 10 | on: push 11 | 12 | jobs: 13 | delete-artifacts: 14 | name: Delete Artifacts 15 | runs-on: ubuntu-latest 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | steps: 19 | - name: Delete artifacts 20 | run: | 21 | # Customize those three lines with your repository and credentials: 22 | REPO=${GITHUB_API_URL}/repos/${{ github.repository }} 23 | 24 | # A shortcut to call GitHub API. 25 | ghapi() { curl --silent --location --user _:$GITHUB_TOKEN "$@"; } 26 | 27 | # A temporary file which receives HTTP response headers. 28 | TMPFILE=/tmp/tmp.$$ 29 | 30 | # An associative array, key: artifact name, value: number of artifacts of that name. 31 | declare -A ARTCOUNT 32 | 33 | # Process all artifacts on this repository, loop on returned "pages". 34 | URL=$REPO/actions/artifacts 35 | while [[ -n "$URL" ]]; do 36 | 37 | # Get current page, get response headers in a temporary file. 38 | JSON=$(ghapi --dump-header $TMPFILE "$URL") 39 | 40 | # Get URL of next page. Will be empty if we are at the last page. 41 | URL=$(grep '^Link:' "$TMPFILE" | tr ',' '\n' | grep 'rel="next"' | head -1 | sed -e 's/.*.*//') 42 | rm -f $TMPFILE 43 | 44 | # Number of artifacts on this page: 45 | COUNT=$(( $(jq <<<$JSON -r '.artifacts | length') )) 46 | 47 | # Loop on all artifacts on this page. 48 | for ((i=0; $i < $COUNT; i++)); do 49 | 50 | # Get name of artifact and count instances of this name. 51 | name=$(jq <<<$JSON -r ".artifacts[$i].name?") 52 | ARTCOUNT[$name]=$(( $(( ${ARTCOUNT[$name]} )) + 1)) 53 | 54 | id=$(jq <<<$JSON -r ".artifacts[$i].id?") 55 | size=$(( $(jq <<<$JSON -r ".artifacts[$i].size_in_bytes?") )) 56 | printf "Deleting '%s' #%d, %'d bytes\n" $name ${ARTCOUNT[$name]} $size 57 | ghapi -X DELETE $REPO/actions/artifacts/$id 58 | done 59 | done -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/core/normalization/BetaNormalizeMerge.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.core.normalization; 2 | 3 | import java.util.List; 4 | import java.util.Map.Entry; 5 | import org.dhallj.core.Expr; 6 | import org.dhallj.core.ExternalVisitor; 7 | import org.dhallj.core.Operator; 8 | 9 | final class BetaNormalizeMerge extends ExternalVisitor.Constant { 10 | private final List> handlers; 11 | 12 | private BetaNormalizeMerge(List> handlers) { 13 | super(null); 14 | this.handlers = handlers; 15 | } 16 | 17 | static final Expr apply(Expr handlers, Expr union, Expr type) { 18 | List> fields = Expr.Util.asRecordLiteral(handlers); 19 | 20 | Expr result = union.accept(new BetaNormalizeMerge(fields)); 21 | 22 | if (result != null) { 23 | return result.accept(BetaNormalize.instance); 24 | } else { 25 | 26 | return Expr.makeMerge(handlers, union, type); 27 | } 28 | } 29 | 30 | @Override 31 | public Expr onFieldAccess(Expr base, String fieldName) { 32 | List> baseAsUnion = Expr.Util.asUnionType(base); 33 | 34 | if (baseAsUnion != null) { 35 | return merge(this.handlers, fieldName); 36 | } else { 37 | return null; 38 | } 39 | } 40 | 41 | @Override 42 | public Expr onApplication(Expr base, Expr arg) { 43 | Entry baseAsFieldAccess = Expr.Util.asFieldAccess(base); 44 | if (baseAsFieldAccess != null) { 45 | List> accessedAsUnion = Expr.Util.asUnionType(baseAsFieldAccess.getKey()); 46 | 47 | if (accessedAsUnion != null) { 48 | return merge(this.handlers, baseAsFieldAccess.getValue(), arg); 49 | } else { 50 | return null; 51 | } 52 | 53 | } else { 54 | String baseAsBuiltIn = Expr.Util.asBuiltIn(base); 55 | 56 | if (baseAsBuiltIn != null) { 57 | if (baseAsBuiltIn.equals("Some")) { 58 | return merge(this.handlers, baseAsBuiltIn, arg); 59 | } else if (baseAsBuiltIn.equals("None")) { 60 | return merge(this.handlers, baseAsBuiltIn); 61 | } 62 | } 63 | return null; 64 | } 65 | } 66 | 67 | private static Expr merge(List> handlers, String fieldName) { 68 | return NormalizationUtilities.lookup(handlers, fieldName); 69 | } 70 | 71 | private static Expr merge(List> handlers, String fieldName, Expr arg) { 72 | Expr handler = NormalizationUtilities.lookup(handlers, fieldName); 73 | if (handler != null) { 74 | return Expr.makeApplication(handler, arg); 75 | } else { 76 | return null; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /modules/imports-mini/src/main/java/org/dhallj/imports/mini/Resolver.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.imports.mini; 2 | 3 | import org.dhallj.core.DhallException.ResolutionFailure; 4 | import org.dhallj.core.Expr; 5 | import java.nio.file.Path; 6 | 7 | public final class Resolver { 8 | public static final Expr resolve(Expr expr, boolean integrityChecks, Path currentPath) 9 | throws ResolutionFailure { 10 | return resolveWithVisitor(expr, new ResolutionVisitor.Filesystem(currentPath, integrityChecks)); 11 | } 12 | 13 | public static final Expr resolve(Expr expr, boolean integrityChecks) throws ResolutionFailure { 14 | return resolve(expr, integrityChecks, null); 15 | } 16 | 17 | public static final Expr resolve(Expr expr) throws ResolutionFailure { 18 | return resolve(expr, true); 19 | } 20 | 21 | public static final Expr resolveFromResources( 22 | Expr expr, boolean integrityChecks, Path currentPath, ClassLoader classLoader) 23 | throws ResolutionFailure { 24 | return resolveWithVisitor( 25 | expr, new ResolutionVisitor.Resources(currentPath, integrityChecks, classLoader)); 26 | } 27 | 28 | public static final Expr resolveFromResources( 29 | Expr expr, boolean integrityChecks, Path currentPath) throws ResolutionFailure { 30 | return resolveFromResources( 31 | expr, integrityChecks, currentPath, Resolver.class.getClassLoader()); 32 | } 33 | 34 | public static final Expr resolveFromResources(Expr expr, boolean integrityChecks) 35 | throws ResolutionFailure { 36 | return resolveFromResources(expr, integrityChecks, null); 37 | } 38 | 39 | public static final Expr resolveFromResources(Expr expr) throws ResolutionFailure { 40 | return resolveFromResources(expr, true); 41 | } 42 | 43 | private static final Expr resolveWithVisitor(Expr expr, ResolutionVisitor visitor) 44 | throws ResolutionFailure { 45 | Expr result; 46 | try { 47 | result = expr.accept(visitor); 48 | } catch (ResolutionVisitor.WrappedParsingFailure e) { 49 | throw new ResolutionFailure(e.getMessage(), e.underlying); 50 | } catch (ResolutionVisitor.WrappedIOException e) { 51 | throw new ResolutionFailure(e.getMessage(), e.underlying); 52 | } catch (ResolutionVisitor.Missing e) { 53 | throw new ResolutionFailure(e.getMessage()); 54 | } catch (ResolutionVisitor.MissingEnv e) { 55 | throw new ResolutionFailure(e.getMessage()); 56 | } catch (ResolutionVisitor.IntegrityCheckException e) { 57 | throw new ResolutionFailure(e.getMessage()); 58 | } catch (UnsupportedOperationException e) { 59 | throw new ResolutionFailure(e.getMessage()); 60 | } 61 | return result; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /modules/core/src/test/java/org/dhallj/cbor/CborSuite.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.cbor 2 | 3 | import co.nstant.in.cbor.{CborBuilder, CborEncoder} 4 | import java.io.ByteArrayOutputStream 5 | import java.math.BigInteger 6 | import munit.ScalaCheckSuite 7 | import org.scalacheck.Prop 8 | 9 | class CborSuite extends ScalaCheckSuite { 10 | def roundTripDouble(value: Double): Option[Double] = { 11 | val writer = new Writer.ByteArrayWriter() 12 | writer.writeDouble(value) 13 | 14 | val bytes = writer.getBytes 15 | val reader = new Reader.ByteArrayReader(bytes) 16 | reader.nextSymbol(DoubleValueVisitor) 17 | } 18 | 19 | def encodeDoubleWithCborJava(value: Double): Array[Byte] = { 20 | val stream = new ByteArrayOutputStream() 21 | new CborEncoder(stream).encode(new CborBuilder().add(value).build()) 22 | 23 | return stream.toByteArray 24 | } 25 | 26 | property("Writer and Reader should round-trip doubles") { 27 | Prop.forAll((value: Double) => roundTripDouble(value) == Some(value)) 28 | } 29 | 30 | property("Writer should agree with cbor-java") { 31 | Prop.forAll { (value: Double) => 32 | val writer = new Writer.ByteArrayWriter() 33 | writer.writeDouble(value) 34 | 35 | writer.getBytes.sameElements(encodeDoubleWithCborJava(value)) 36 | } 37 | } 38 | 39 | test("Writer and Reader should round-trip special-case doubles") { 40 | assertEquals(roundTripDouble(0.0), Some(0.0)) 41 | assertEquals(roundTripDouble(-0.0), Some(-0.0)) 42 | assertEquals(roundTripDouble(Double.PositiveInfinity), Some(Double.PositiveInfinity)) 43 | assertEquals(roundTripDouble(Double.NegativeInfinity), Some(Double.NegativeInfinity)) 44 | assert(roundTripDouble(Double.NaN).exists(_.isNaN)) 45 | } 46 | 47 | object DoubleValueVisitor extends Visitor[Option[Double]] { 48 | def onUnsignedInteger(value: BigInteger): Option[Double] = None 49 | def onNegativeInteger(value: BigInteger): Option[Double] = None 50 | def onByteString(value: Array[Byte]): Option[Double] = None 51 | def onTextString(value: String): Option[Double] = None 52 | def onVariableArray(length: BigInteger, name: String): Option[Double] = None 53 | def onArray(length: BigInteger, tagI: BigInteger): Option[Double] = None 54 | def onMap(size: BigInteger): Option[Double] = None 55 | def onFalse: Option[Double] = None 56 | def onTrue: Option[Double] = None 57 | def onNull: Option[Double] = None 58 | def onHalfFloat(value: Float): Option[Double] = Some(value.toDouble) 59 | def onSingleFloat(value: Float): Option[Double] = Some(value.toDouble) 60 | def onDoubleFloat(value: Double): Option[Double] = Some(value) 61 | def onTag: Option[Double] = None 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/core/normalization/BetaNormalizeWith.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.core.normalization; 2 | 3 | import java.util.AbstractMap.SimpleImmutableEntry; 4 | import java.util.ArrayList; 5 | import java.util.Arrays; 6 | import java.util.Comparator; 7 | import java.util.List; 8 | import java.util.Map.Entry; 9 | import org.dhallj.core.Expr; 10 | 11 | final class BetaNormalizeWith { 12 | static final Expr apply(Expr base, String[] path, Expr value) { 13 | List> baseAsRecordLiteral = Expr.Util.asRecordLiteral(base); 14 | 15 | if (baseAsRecordLiteral != null) { 16 | List> result = new ArrayList<>(); 17 | boolean found = false; 18 | 19 | for (Entry field : baseAsRecordLiteral) { 20 | String key = field.getKey(); 21 | 22 | if (key.equals(path[0])) { 23 | found = true; 24 | 25 | Expr newValue; 26 | 27 | if (path.length == 1) { 28 | newValue = value; 29 | } else { 30 | String[] remainingPath = new String[path.length - 1]; 31 | System.arraycopy(path, 1, remainingPath, 0, path.length - 1); 32 | 33 | newValue = 34 | Expr.makeWith(field.getValue(), remainingPath, value) 35 | .accept(BetaNormalize.instance); 36 | } 37 | 38 | result.add(new SimpleImmutableEntry<>(key, newValue)); 39 | } else { 40 | result.add(field); 41 | } 42 | } 43 | 44 | if (!found) { 45 | Expr newValue; 46 | 47 | if (path.length == 1) { 48 | newValue = value; 49 | } else { 50 | String[] remainingPath = new String[path.length - 1]; 51 | System.arraycopy(path, 1, remainingPath, 0, path.length - 1); 52 | 53 | newValue = 54 | Expr.makeWith(Expr.Constants.EMPTY_RECORD_LITERAL, remainingPath, value) 55 | .accept(BetaNormalize.instance); 56 | } 57 | 58 | result.add(new SimpleImmutableEntry<>(path[0], newValue)); 59 | } 60 | 61 | Entry[] resultArray = result.toArray(new Entry[result.size()]); 62 | 63 | Arrays.sort(resultArray, entryComparator); 64 | 65 | return Expr.makeRecordLiteral(resultArray); 66 | } else { 67 | return Expr.makeWith(base, path, value); 68 | } 69 | } 70 | 71 | /** Java 8 introduce {@code comparingByKey}, but we can roll our own pretty easily. */ 72 | private static final Comparator> entryComparator = 73 | new Comparator>() { 74 | public int compare(Entry a, Entry b) { 75 | return a.getKey().compareTo(b.getKey()); 76 | } 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/core/Operator.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.core; 2 | 3 | /** Represents a Dhall operator. */ 4 | public enum Operator { 5 | OR("||", 3, true), 6 | AND("&&", 7, true), 7 | EQUALS("==", 12, true), 8 | NOT_EQUALS("!=", 13, true), 9 | PLUS("+", 4, false), 10 | TIMES("*", 11, false), 11 | TEXT_APPEND("++", 5, false), 12 | LIST_APPEND("#", 6, false), 13 | COMBINE("\u2227", 8, false), 14 | PREFER("\u2afd", 9, false), 15 | COMBINE_TYPES("\u2a53", 10, false), 16 | IMPORT_ALT("?", 2, false), 17 | EQUIVALENT("\u2261", 1, false), 18 | COMPLETE("::", 0, false); 19 | 20 | private static final Operator[] values = values(); 21 | 22 | private final String value; 23 | private final int precedence; 24 | private final boolean isBoolOperator; 25 | 26 | Operator(String value, int precedence, boolean isBoolOperator) { 27 | this.value = value; 28 | this.precedence = precedence; 29 | this.isBoolOperator = isBoolOperator; 30 | } 31 | 32 | public final boolean isBoolOperator() { 33 | return this.isBoolOperator; 34 | } 35 | 36 | public final int getLabel() { 37 | return this.ordinal(); 38 | } 39 | 40 | public final int getPrecedence() { 41 | return this.precedence; 42 | } 43 | 44 | public final String toString() { 45 | return this.value; 46 | } 47 | 48 | public static final Operator fromLabel(int ordinal) { 49 | if (ordinal >= 0 && ordinal < values.length) { 50 | return values[ordinal]; 51 | } else { 52 | return null; 53 | } 54 | } 55 | 56 | public static final Operator parse(String input) { 57 | if (input.equals(OR.value)) { 58 | return OR; 59 | } else if (input.equals(AND.value)) { 60 | return AND; 61 | } else if (input.equals(EQUALS.value)) { 62 | return EQUALS; 63 | } else if (input.equals(NOT_EQUALS.value)) { 64 | return NOT_EQUALS; 65 | } else if (input.equals(PLUS.value)) { 66 | return PLUS; 67 | } else if (input.equals(TIMES.value)) { 68 | return TIMES; 69 | } else if (input.equals(TEXT_APPEND.value)) { 70 | return TEXT_APPEND; 71 | } else if (input.equals(LIST_APPEND.value)) { 72 | return LIST_APPEND; 73 | } else if (input.equals(COMBINE.value) || input.equals("/\\")) { 74 | return COMBINE; 75 | } else if (input.equals(PREFER.value) || input.equals("//")) { 76 | return PREFER; 77 | } else if (input.equals(COMBINE_TYPES.value) || input.equals("//\\\\")) { 78 | return COMBINE_TYPES; 79 | } else if (input.equals(IMPORT_ALT.value)) { 80 | return IMPORT_ALT; 81 | } else if (input.equals(EQUIVALENT.value) || input.equals("===")) { 82 | return EQUIVALENT; 83 | } else if (input.equals(COMPLETE.value)) { 84 | return COMPLETE; 85 | } else { 86 | throw new IllegalArgumentException("No org.dhallj.core.Operator represented by " + input); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /modules/circe/src/main/scala/org/dhallj/circe/CirceHandler.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.circe 2 | 3 | import io.circe.Json 4 | import java.math.BigInteger 5 | import java.util.{ArrayDeque, ArrayList, Deque, LinkedHashMap, List} 6 | import org.dhallj.core.converters.JsonHandler 7 | import scala.collection.JavaConverters._ 8 | 9 | sealed private trait Context { 10 | def add(key: String): Unit 11 | def add(value: Json): Unit 12 | def result: Json 13 | } 14 | 15 | final private class RootContext(value: Json) extends Context { 16 | def add(key: String): Unit = () 17 | def add(value: Json): Unit = () 18 | def result: Json = value 19 | } 20 | 21 | final private class ObjectContext() extends Context { 22 | private[this] var key: String = null 23 | private[this] val fields: LinkedHashMap[String, Json] = new LinkedHashMap() 24 | 25 | def add(key: String): Unit = this.key = key 26 | def add(value: Json): Unit = this.fields.put(this.key, value) 27 | def result: Json = Json.fromFields(this.fields.entrySet.asScala.map(entry => entry.getKey -> entry.getValue)) 28 | } 29 | 30 | final private class ArrayContext() extends Context { 31 | private[this] val values: List[Json] = new ArrayList() 32 | 33 | def add(key: String): Unit = () 34 | def add(value: Json): Unit = this.values.add(value) 35 | def result: Json = Json.fromValues(this.values.asScala) 36 | } 37 | 38 | class CirceHandler extends JsonHandler { 39 | private[this] val stack: Deque[Context] = new ArrayDeque() 40 | 41 | protected[this] def addValue(value: Json): Unit = 42 | if (stack.isEmpty) { 43 | stack.push(new RootContext(value)) 44 | } else { 45 | stack.peek.add(value) 46 | } 47 | 48 | final def result: Json = stack.pop().result 49 | 50 | final def onNull(): Unit = addValue(Json.Null) 51 | final def onBoolean(value: Boolean): Unit = addValue(if (value) Json.True else Json.False) 52 | final def onNumber(value: BigInteger): Unit = addValue(Json.fromBigInt(new BigInt(value))) 53 | def onDouble(value: Double): Unit = addValue(Json.fromDoubleOrNull(value)) 54 | final def onString(value: String): Unit = addValue(Json.fromString(value)) 55 | 56 | final def onArrayStart(): Unit = stack.push(new ArrayContext()) 57 | 58 | final def onArrayEnd(): Unit = { 59 | val current = stack.pop() 60 | 61 | if (stack.isEmpty) { 62 | stack.push(current) 63 | } else { 64 | stack.peek.add(current.result) 65 | } 66 | } 67 | 68 | final def onArrayElementGap(): Unit = () 69 | 70 | final def onObjectStart(): Unit = stack.push(new ObjectContext()) 71 | 72 | final def onObjectEnd(): Unit = { 73 | val current = stack.pop() 74 | 75 | if (stack.isEmpty) { 76 | stack.push(current) 77 | } else { 78 | stack.peek.add(current.result) 79 | } 80 | } 81 | 82 | final def onObjectField(name: String): Unit = stack.peek.add(name) 83 | 84 | final def onObjectFieldGap(): Unit = () 85 | } 86 | -------------------------------------------------------------------------------- /modules/imports/src/test/scala/org/dhallj/imports/CorsComplianceCheckSuite.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.imports 2 | 3 | import java.net.URI 4 | import java.nio.file.Paths 5 | 6 | import cats.effect.IO 7 | import munit.FunSuite 8 | import org.dhallj.imports.ImportContext._ 9 | import org.http4s.{Header, Headers} 10 | 11 | class CorsComplianceCheckSuite extends FunSuite { 12 | 13 | val fooOrigin = new URI("http://foo.org/foo.dhall") 14 | val fooOrigin8080 = new URI("http://foo.org:8080/foo.dhall") 15 | val fooOrigin2 = new URI("http://foo.org/baz.dhall") 16 | val barOrigin = new URI("http://bar.org/bar.dhall") 17 | 18 | val localPath = Paths.get("/foo/bar.dhall") 19 | 20 | test("Remote - same origin") { 21 | CorsComplianceCheck[IO](Remote(fooOrigin, null), Remote(fooOrigin2, null), Headers.empty).unsafeRunSync() 22 | } 23 | 24 | test("Remote - different origin, allow *") { 25 | CorsComplianceCheck[IO](Remote(fooOrigin, null), 26 | Remote(barOrigin, null), 27 | Headers.of(Header("Access-Control-Allow-Origin", "*")) 28 | ).unsafeRunSync() 29 | } 30 | 31 | test("Remote - different origin, allow parent authority") { 32 | CorsComplianceCheck[IO](Remote(fooOrigin, null), 33 | Remote(barOrigin, null), 34 | Headers.of(Header("Access-Control-Allow-Origin", "http://foo.org")) 35 | ).unsafeRunSync() 36 | } 37 | 38 | test("Remote - different origin".fail) { 39 | CorsComplianceCheck[IO](Remote(fooOrigin, null), Remote(barOrigin, null), Headers.empty).unsafeRunSync() 40 | } 41 | 42 | test("Remote - different origin, cors parent different authority".fail) { 43 | CorsComplianceCheck[IO](Remote(fooOrigin, null), 44 | Remote(barOrigin, null), 45 | Headers.of(Header("Access-Control-Allow-Origin", "http://bar.org")) 46 | ).unsafeRunSync() 47 | } 48 | 49 | test("Remote - different origin, cors parent different scheme".fail) { 50 | CorsComplianceCheck[IO](Remote(fooOrigin, null), 51 | Remote(barOrigin, null), 52 | Headers.of(Header("Access-Control-Allow-Origin", "https://foo.org")) 53 | ).unsafeRunSync() 54 | } 55 | 56 | test("Remote - different origin, cors parent different port".fail) { 57 | CorsComplianceCheck[IO](Remote(fooOrigin, null), 58 | Remote(barOrigin, null), 59 | Headers.of(Header("Access-Control-Allow-Origin", "http://foo.org:8080")) 60 | ).unsafeRunSync() 61 | } 62 | 63 | test("Remote - different origin, cors parent different port 2".fail) { 64 | CorsComplianceCheck[IO](Remote(fooOrigin8080, null), 65 | Remote(barOrigin, null), 66 | Headers.of(Header("Access-Control-Allow-Origin", "http://foo.org")) 67 | ).unsafeRunSync() 68 | } 69 | 70 | test("Local") { 71 | CorsComplianceCheck[IO](Local(localPath), Remote(fooOrigin2, null), Headers.empty).unsafeRunSync() 72 | } 73 | 74 | test("Env") { 75 | CorsComplianceCheck[IO](Env("foo"), Remote(fooOrigin2, null), Headers.empty).unsafeRunSync() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/core/normalization/BetaNormalizeTextLiteral.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.core.normalization; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Iterator; 5 | import java.util.List; 6 | import org.dhallj.core.Expr; 7 | import org.dhallj.core.ExternalVisitor; 8 | 9 | final class BetaNormalizeTextLiteral { 10 | static final Expr apply(String[] parts, List interpolated) { 11 | if (parts.length == 1) { 12 | return Expr.makeTextLiteral(parts[0]); 13 | } else { 14 | int partsSize = 0; 15 | for (String part : parts) { 16 | partsSize += part.length(); 17 | } 18 | int c = 0; 19 | if (partsSize == 0) { 20 | Expr notEmptyString = null; 21 | boolean tooMany = false; 22 | Iterator it = interpolated.iterator(); 23 | 24 | while (it.hasNext() && !tooMany) { 25 | Expr next = it.next(); 26 | String nextAsSimpleTextLiteral = Expr.Util.asSimpleTextLiteral(next); 27 | 28 | if (nextAsSimpleTextLiteral == null || nextAsSimpleTextLiteral.length() != 0) { 29 | if (notEmptyString == null) { 30 | notEmptyString = next; 31 | } else { 32 | tooMany = true; 33 | } 34 | } 35 | } 36 | 37 | if (!tooMany && notEmptyString != null) { 38 | return notEmptyString; 39 | } 40 | } 41 | 42 | List newParts = new ArrayList<>(parts.length); 43 | List newInterpolated = new ArrayList<>(parts.length - 1); 44 | newParts.add(parts[0]); 45 | 46 | boolean wasInlined = false; 47 | int partIndex = 1; 48 | 49 | for (Expr expr : interpolated) { 50 | wasInlined = expr.accept(new InlineInterpolatedTextLiteral(newParts, newInterpolated)); 51 | 52 | if (!wasInlined) { 53 | newInterpolated.add(expr); 54 | newParts.add(parts[partIndex++]); 55 | } else { 56 | int lastIndex = newParts.size() - 1; 57 | String lastPart = newParts.get(lastIndex); 58 | newParts.set(lastIndex, lastPart + parts[partIndex++]); 59 | } 60 | } 61 | 62 | return Expr.makeTextLiteral(newParts.toArray(new String[newParts.size()]), newInterpolated); 63 | } 64 | } 65 | 66 | private static final class InlineInterpolatedTextLiteral 67 | extends ExternalVisitor.Constant { 68 | private final List newParts; 69 | private final List newInterpolated; 70 | 71 | InlineInterpolatedTextLiteral(List newParts, List newInterpolated) { 72 | super(false); 73 | this.newParts = newParts; 74 | this.newInterpolated = newInterpolated; 75 | } 76 | 77 | @Override 78 | public Boolean onText(String[] parts, Iterable interpolated) { 79 | int lastIndex = newParts.size() - 1; 80 | String lastPart = newParts.get(lastIndex); 81 | newParts.set(lastIndex, lastPart + parts[0]); 82 | 83 | Iterator it = interpolated.iterator(); 84 | 85 | for (int i = 1; i < parts.length; i++) { 86 | newInterpolated.add(it.next()); 87 | newParts.add(parts[i]); 88 | } 89 | return true; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/src/test/scala/org/dhallj/tests/acceptance/AcceptanceSuites.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.tests.acceptance 2 | 3 | class NormalizationSimpleSuite extends NormalizationUSuite("normalization/success/simple") 4 | class NormalizationRegressionSuite extends NormalizationSuite("normalization/success/regression") 5 | class NormalizationUnitSuite extends NormalizationUSuite("normalization/success/unit") 6 | class NormalizationSimplificationsSuite extends NormalizationSuite("normalization/success/simplifications") 7 | class NormalizationOtherSuite extends NormalizationSuite("normalization/success") 8 | class NormalizationHaskellTutorialSuite extends NormalizationSuite("normalization/success/haskell-tutorial", true) 9 | 10 | class HashingSimpleSuite extends HashingSuite("semantic-hash/success/simple") 11 | class HashingSimplificationsSuite extends HashingSuite("semantic-hash/success/simplifications") 12 | class HashingOtherSuite extends HashingSuite("semantic-hash/success") 13 | class HashingHaskellTutorialSuite extends HashingSuite("semantic-hash/success/haskell-tutorial", true) 14 | class HashingPreludeSuite extends HashingSuite("semantic-hash/success/prelude", true) 15 | 16 | class AlphaNormalizationUnitSuite extends AlphaNormalizationSuite("alpha-normalization/success/unit") 17 | class AlphaNormalizationRegressionSuite extends AlphaNormalizationSuite("alpha-normalization/success/regression") 18 | 19 | class TypeCheckingSimpleSuite extends ParsingTypeCheckingSuite("type-inference/success/simple") 20 | class TypeCheckingUnitSuite extends ParsingTypeCheckingSuite("type-inference/success/unit") 21 | class TypeCheckingRegressionSuite extends TypeCheckingSuite("type-inference/success/regression") 22 | class TypeCheckingOtherSuite extends TypeCheckingSuite("type-inference/success") { 23 | override def slow = Set("prelude") 24 | // Depends on http://csrng.net/, which is rate-limited (and also currently entirely down). 25 | override def ignored = Set("CacheImports", "CacheImportsCanonicalize") 26 | } 27 | class TypeCheckingFailureUnitSuite extends TypeCheckingFailureSuite("type-inference/failure/unit") 28 | class TypeCheckingPreludeSuite extends TypeCheckingSuite("type-inference/success/prelude", true) 29 | 30 | class ParsingUnitSuite extends ParsingSuite("parser/success/unit") 31 | class ParsingTextSuite extends ParsingSuite("parser/success/text") 32 | class ParsingOtherSuite extends ParsingSuite("parser/success") 33 | 34 | class ParsingFailureUnitSuite extends ParsingFailureSuite("parser/failure/unit") 35 | class ParsingFailureSpacingSuite extends ParsingFailureSuite("parser/failure/spacing") 36 | class ParsingFailureOtherSuite extends ParsingFailureSuite("parser/failure") 37 | 38 | class BinaryDecodingUnitSuite extends BinaryDecodingSuite("binary-decode/success/unit") 39 | class BinaryDecodingImportsUnitSuite extends BinaryDecodingSuite("binary-decode/success/unit/imports") 40 | class BinaryDecodingFailureUnitSuite extends BinaryDecodingFailureSuite("binary-decode/failure/unit") 41 | 42 | class ImportResolutionSuccessSuite extends ImportResolutionSuite("import/success") 43 | class ImportResolutionSuccessUnitSuite extends ImportResolutionSuite("import/success/unit") 44 | class ImportResolutionSuccessUnitAsLocationSuite extends ImportResolutionSuite("import/success/unit/asLocation") 45 | -------------------------------------------------------------------------------- /modules/yaml/src/test/scala/org/dhallj/yaml/YamlConverterSuite.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.yaml 2 | 3 | import munit.ScalaCheckSuite 4 | import org.dhallj.ast._ 5 | import org.dhallj.core.Expr 6 | import org.dhallj.parser.DhallParser 7 | import org.scalacheck.Prop 8 | import org.yaml.snakeyaml.DumperOptions 9 | 10 | class YamlConverterSuite extends ScalaCheckSuite { 11 | property("convert integers") { 12 | Prop.forAll { (value: BigInt) => 13 | val asDhall = IntegerLiteral(value.underlying) 14 | Option(YamlConverter.toYamlString(asDhall)) == Some(value.toString + "\n") 15 | } 16 | } 17 | 18 | test("convert nested lists") { 19 | val expr = DhallParser.parse("[[]: List Bool]") 20 | 21 | assert(clue(Option(YamlConverter.toYamlString(expr))) == Some("- []\n")) 22 | } 23 | 24 | test("convert None") { 25 | val expr = DhallParser.parse("None Bool") 26 | 27 | assert(clue(Option(YamlConverter.toYamlString(expr))) == Some("null\n")) 28 | } 29 | 30 | test("convert Some") { 31 | val expr = DhallParser.parse("""Some "foo"""") 32 | 33 | assert(clue(Option(YamlConverter.toYamlString(expr))) == Some("foo\n")) 34 | } 35 | 36 | test("convert records") { 37 | val expr1 = DhallParser.parse("{foo = [{bar = [1]}, {bar = [1, 2, 3]}]}") 38 | val expected = 39 | """|foo: 40 | |- bar: 41 | | - 1 42 | |- bar: 43 | | - 1 44 | | - 2 45 | | - 3 46 | |""".stripMargin 47 | 48 | assert(clue(Option(YamlConverter.toYamlString(expr1))) == Some(expected)) 49 | } 50 | 51 | test("convert unions (nullary constructors)") { 52 | val expr1 = DhallParser.parse("[((\\(x: Natural) -> ) 1).bar]").normalize() 53 | 54 | assert(clue(Option(YamlConverter.toYamlString(expr1))) == Some("- bar\n")) 55 | } 56 | 57 | test("convert unions") { 58 | val expr1 = DhallParser.parse("[.foo True]").normalize() 59 | 60 | assert(clue(Option(YamlConverter.toYamlString(expr1))) == Some("- true\n")) 61 | } 62 | 63 | test("convert text containing newlines") { 64 | val expr1 = DhallParser.parse(""" { a = "foo\nbar" } """).normalize() 65 | 66 | assert(clue(Option(YamlConverter.toYamlString(expr1))) == Some("a: |-\n foo\n bar\n")) 67 | } 68 | 69 | test("convert test containing quotes") { 70 | val expr1 = DhallParser.parse(""" { a = "\"" } """).normalize() 71 | val expr2 = DhallParser.parse(""" { a = "\"\n" } """).normalize() 72 | 73 | assert(clue(Option(YamlConverter.toYamlString(expr1))) == Some("a: '\"'\n")) 74 | assert(clue(Option(YamlConverter.toYamlString(expr2))) == Some("a: |\n \"\n")) 75 | } 76 | 77 | test("convert text containing newlines and respect SnakeYAML configuration") { 78 | val expr1 = DhallParser.parse(""" { a = "foo\nbar" } """).normalize() 79 | val expected = "\"a\": \"foo\\nbar\"\n" 80 | 81 | val options = new DumperOptions() 82 | options.setDefaultScalarStyle(DumperOptions.ScalarStyle.DOUBLE_QUOTED) 83 | 84 | assert(clue(Option(YamlConverter.toYamlString(expr1, options))) == clue(Some(expected))) 85 | } 86 | 87 | test("fail safely on unconvertible expressions") { 88 | val expr1 = Lambda("x", Expr.Constants.NATURAL, Identifier("x")) 89 | 90 | assert(Option(YamlConverter.toYamlString(expr1)) == None) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/core/normalization/BetaNormalizeProjection.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.core.normalization; 2 | 3 | import java.util.Arrays; 4 | import java.util.HashSet; 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.Map.Entry; 8 | import java.util.Set; 9 | import java.util.TreeMap; 10 | import java.util.TreeSet; 11 | import org.dhallj.core.Expr; 12 | import org.dhallj.core.ExternalVisitor; 13 | import org.dhallj.core.Operator; 14 | 15 | final class BetaNormalizeProjection extends ExternalVisitor.Constant { 16 | private final String[] fieldNames; 17 | 18 | private BetaNormalizeProjection(String[] fieldNames) { 19 | super(null); 20 | this.fieldNames = fieldNames; 21 | } 22 | 23 | static final Expr apply(Expr base, final String[] fieldNames) { 24 | if (fieldNames.length == 0) { 25 | return Expr.Constants.EMPTY_RECORD_LITERAL; 26 | } 27 | 28 | Expr result = base.accept(new BetaNormalizeProjection(fieldNames)); 29 | 30 | if (result != null) { 31 | return result; 32 | } else { 33 | // We have to sort the field names if we can't reduce. 34 | String[] newFieldNames = new String[fieldNames.length]; 35 | System.arraycopy(fieldNames, 0, newFieldNames, 0, fieldNames.length); 36 | Arrays.sort(newFieldNames); 37 | return Expr.makeProjection(base, newFieldNames); 38 | } 39 | } 40 | 41 | @Override 42 | public Expr onRecord(Iterable> fields, int size) { 43 | Set fieldNameSet = new HashSet(this.fieldNames.length); 44 | 45 | for (String fieldName : this.fieldNames) { 46 | fieldNameSet.add(fieldName); 47 | } 48 | 49 | Map selected = new TreeMap(); 50 | 51 | for (Entry entry : fields) { 52 | if (fieldNameSet.contains(entry.getKey())) { 53 | selected.put(entry.getKey(), entry.getValue()); 54 | } 55 | } 56 | return Expr.makeRecordLiteral(selected.entrySet()); 57 | } 58 | 59 | @Override 60 | public Expr onProjection(Expr base, String[] fieldNames) { 61 | return Expr.makeProjection(base, this.fieldNames).accept(BetaNormalize.instance); 62 | } 63 | 64 | @Override 65 | public Expr onOperatorApplication(Operator operator, Expr lhs, Expr rhs) { 66 | if (operator.equals(Operator.PREFER)) { 67 | List> rhsFields = Expr.Util.asRecordLiteral(rhs); 68 | if (rhsFields != null) { 69 | 70 | Set rhsKeys = new HashSet(); 71 | Set leftFields = new TreeSet(); 72 | Set rightFields = new TreeSet(); 73 | 74 | for (Entry entry : rhsFields) { 75 | rhsKeys.add(entry.getKey()); 76 | } 77 | 78 | for (String fieldName : this.fieldNames) { 79 | if (rhsKeys.contains(fieldName)) { 80 | rightFields.add(fieldName); 81 | } else { 82 | leftFields.add(fieldName); 83 | } 84 | } 85 | 86 | return Expr.makeOperatorApplication( 87 | Operator.PREFER, 88 | Expr.makeProjection(lhs, leftFields.toArray(new String[leftFields.size()])), 89 | Expr.makeProjection(rhs, rightFields.toArray(new String[leftFields.size()]))) 90 | .accept(BetaNormalize.instance); 91 | } 92 | } 93 | return null; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /modules/imports/src/main/scala/org/dhallj/imports/ImportCache.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.imports 2 | 3 | import java.nio.file.{Files, Path, Paths} 4 | 5 | import cats.effect.Sync 6 | import cats.implicits._ 7 | 8 | trait ImportCache[F[_]] { 9 | def get(key: Array[Byte]): F[Option[Array[Byte]]] 10 | 11 | def put(key: Array[Byte], value: Array[Byte]): F[Unit] 12 | } 13 | 14 | object ImportCache { 15 | 16 | /* 17 | * Improves the ergonomics when resolving imports if we don't have to check 18 | * if the cache exists. So if we fail to construct an imports cache, 19 | * we warn and return this instead. 20 | */ 21 | class NoopImportCache[F[_]](implicit F: Sync[F]) extends ImportCache[F] { 22 | override def get(key: Array[Byte]): F[Option[Array[Byte]]] = F.pure(None) 23 | 24 | override def put(key: Array[Byte], value: Array[Byte]): F[Unit] = F.unit 25 | } 26 | 27 | private class Impl[F[_]](rootDir: Path)(implicit F: Sync[F]) extends ImportCache[F] { 28 | override def get(key: Array[Byte]): F[Option[Array[Byte]]] = { 29 | val p = path(key) 30 | if (Files.exists(p)) { 31 | F.delay(Some(Files.readAllBytes(p))) 32 | } else { 33 | F.pure(None) 34 | } 35 | } 36 | 37 | override def put(key: Array[Byte], value: Array[Byte]): F[Unit] = 38 | F.delay(Files.write(path(key), value)) 39 | 40 | private def path(key: Array[Byte]): Path = rootDir.resolve(s"1220${toHex(key)}") 41 | 42 | private def toHex(bs: Array[Byte]): String = { 43 | val sb = new StringBuilder 44 | for (b <- bs) { 45 | sb.append(String.format("%02x", Byte.box(b))) 46 | } 47 | sb.toString 48 | } 49 | } 50 | 51 | def apply[F[_] <: AnyRef](rootDir: Path)(implicit F: Sync[F]): F[Option[ImportCache[F]]] = 52 | for { 53 | _ <- if (!Files.exists(rootDir)) F.delay(Files.createDirectories(rootDir)) else F.unit 54 | perms <- F.delay(Files.isReadable(rootDir) && Files.isWritable(rootDir)) 55 | } yield (if (perms) Some(new Impl[F](rootDir)) else None) 56 | 57 | def apply[F[_] <: AnyRef](cacheName: String)(implicit F: Sync[F]): F[ImportCache[F]] = { 58 | def makeCacheFromEnvVar(env: String, relativePath: String): F[Option[ImportCache[F]]] = 59 | for { 60 | envValO <- F.delay(sys.env.get(env)) 61 | cache <- envValO.fold(F.pure(Option.empty[ImportCache[F]]))(envVal => 62 | for { 63 | rootDir <- F.pure(Paths.get(envVal, relativePath, cacheName)) 64 | c <- apply(rootDir) 65 | } yield c 66 | ) 67 | } yield cache 68 | 69 | def backupCache = 70 | for { 71 | cacheO <- 72 | if (isWindows) 73 | makeCacheFromEnvVar("LOCALAPPDATA", "") 74 | else makeCacheFromEnvVar("HOME", ".cache") 75 | cache <- cacheO.fold[F[ImportCache[F]]](F.as(warnCacheNotCreated, new NoopImportCache[F]))(F.pure) 76 | } yield cache 77 | 78 | def isWindows = System.getProperty("os.name").toLowerCase.contains("Windows") 79 | 80 | def warnCacheNotCreated: F[Unit] = 81 | F.delay( 82 | println( 83 | "WARNING: failed to create cache at either $XDG_CACHE_HOME}/dhall or $HOME/.cache/dhall. Are these locations writable?" 84 | ) 85 | ) 86 | 87 | for { 88 | xdgCache <- makeCacheFromEnvVar("XDG_CACHE_HOME", "") 89 | cache <- xdgCache.fold(backupCache)(F.pure) 90 | } yield cache 91 | 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /modules/jawn/src/test/scala/org/dhallj/jawn/JawnConverterSuite.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.jawn 2 | 3 | import io.circe.Json 4 | import io.circe.jawn.CirceSupportParser 5 | import io.circe.syntax._ 6 | import munit.ScalaCheckSuite 7 | import org.dhallj.ast._ 8 | import org.dhallj.core.Expr 9 | import org.dhallj.parser.DhallParser 10 | import org.scalacheck.Prop 11 | 12 | class JawnConverterSuite extends ScalaCheckSuite { 13 | val converter = new JawnConverter(CirceSupportParser.facade) 14 | 15 | property("convert integers") { 16 | Prop.forAll { (value: BigInt) => 17 | val asDhall = IntegerLiteral(value.underlying) 18 | converter(asDhall) == Some(value.asJson) 19 | } 20 | } 21 | 22 | property("convert lists of integers") { 23 | Prop.forAll { (values: Vector[BigInt]) => 24 | val asDhall = if (values.isEmpty) { 25 | EmptyListLiteral(Expr.Constants.INTEGER) 26 | } else { 27 | val exprs = values.map(value => IntegerLiteral(value.underlying)) 28 | NonEmptyListLiteral(exprs.head, exprs.tail) 29 | } 30 | converter(asDhall) == Some(values.asJson) 31 | } 32 | } 33 | 34 | property("convert lists of doubles") { 35 | Prop.forAll { (values: Vector[Double]) => 36 | val asDhall = if (values.isEmpty) { 37 | EmptyListLiteral(Expr.Constants.DOUBLE) 38 | } else { 39 | val exprs = values.map(DoubleLiteral(_)) 40 | NonEmptyListLiteral(exprs.head, exprs.tail) 41 | } 42 | converter(asDhall) == Some(values.asJson) 43 | } 44 | } 45 | 46 | property("convert lists of booleans") { 47 | Prop.forAll { (values: Vector[Boolean]) => 48 | val asDhall = if (values.isEmpty) { 49 | EmptyListLiteral(Expr.Constants.BOOL) 50 | } else { 51 | val exprs = values.map(BoolLiteral(_)) 52 | NonEmptyListLiteral(exprs.head, exprs.tail) 53 | } 54 | converter(asDhall) == Some(values.asJson) 55 | } 56 | } 57 | 58 | test("convert nested lists") { 59 | val expr = DhallParser.parse("[[]: List Bool]") 60 | 61 | assert(converter(expr) == Some(Json.arr(Json.arr()))) 62 | } 63 | 64 | test("convert None") { 65 | val expr = DhallParser.parse("None Bool") 66 | 67 | assert(converter(expr) == Some(Json.Null)) 68 | } 69 | 70 | test("convert Some") { 71 | val expr = DhallParser.parse("""Some "foo"""") 72 | 73 | assert(converter(expr) == Some(Json.fromString("foo"))) 74 | } 75 | 76 | test("convert records") { 77 | val expr1 = DhallParser.parse("{foo = [{bar = [1]}, {bar = [1, 2, 3]}]}") 78 | val Right(json1) = io.circe.jawn.parse("""{"foo": [{"bar": [1]}, {"bar": [1, 2, 3]}]}""") 79 | 80 | assert(converter(expr1) == Some(json1)) 81 | } 82 | 83 | test("convert unions (nullary constructors)") { 84 | val expr1 = DhallParser.parse("[((\\(x: Natural) -> ) 1).bar]").normalize() 85 | val Right(json1) = io.circe.jawn.parse("""["bar"]""") 86 | 87 | assert(converter(expr1) == Some(json1)) 88 | } 89 | 90 | test("convert unions") { 91 | val expr1 = DhallParser.parse("[.foo True]").normalize() 92 | val Right(json1) = io.circe.jawn.parse("""[true]""") 93 | 94 | assert(converter(expr1) == Some(json1)) 95 | } 96 | 97 | test("fail safely on unconvertible expressions") { 98 | val expr1 = Lambda("x", Expr.Constants.NATURAL, Identifier("x")) 99 | 100 | assert(converter(expr1) == None) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/core/converters/JsonHandler.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.core.converters; 2 | 3 | import java.io.PrintWriter; 4 | import java.math.BigInteger; 5 | 6 | public interface JsonHandler { 7 | void onNull(); 8 | 9 | void onBoolean(boolean value); 10 | 11 | void onNumber(BigInteger value); 12 | 13 | void onDouble(double value); 14 | 15 | void onString(String value); 16 | 17 | void onArrayStart(); 18 | 19 | void onArrayEnd(); 20 | 21 | void onArrayElementGap(); 22 | 23 | void onObjectStart(); 24 | 25 | void onObjectEnd(); 26 | 27 | void onObjectField(String name); 28 | 29 | void onObjectFieldGap(); 30 | 31 | public static final class CompactPrinter implements JsonHandler { 32 | private final PrintWriter writer; 33 | 34 | public CompactPrinter(PrintWriter writer) { 35 | this.writer = writer; 36 | } 37 | 38 | public void onNull() { 39 | this.writer.print("null"); 40 | } 41 | 42 | public void onBoolean(boolean value) { 43 | this.writer.print(value); 44 | } 45 | 46 | public void onNumber(BigInteger value) { 47 | this.writer.print(value.toString()); 48 | } 49 | 50 | public void onDouble(double value) { 51 | this.writer.print(value); 52 | } 53 | 54 | public void onString(String value) { 55 | this.writer.printf("\"%s\"", value); 56 | } 57 | 58 | public void onArrayStart() { 59 | this.writer.print("["); 60 | } 61 | 62 | public void onArrayEnd() { 63 | this.writer.print("]"); 64 | } 65 | 66 | public void onArrayElementGap() { 67 | this.writer.print(","); 68 | } 69 | 70 | public void onObjectStart() { 71 | this.writer.print("{"); 72 | } 73 | 74 | public void onObjectEnd() { 75 | this.writer.print("}"); 76 | } 77 | 78 | public void onObjectField(String name) { 79 | this.writer.printf("\"%s\":", name); 80 | } 81 | 82 | public void onObjectFieldGap() { 83 | this.writer.print(","); 84 | } 85 | } 86 | 87 | public static final class CompactStringPrinter implements JsonHandler { 88 | private final StringBuilder builder; 89 | 90 | public CompactStringPrinter() { 91 | this.builder = new StringBuilder(); 92 | } 93 | 94 | public String toString() { 95 | return this.builder.toString(); 96 | } 97 | 98 | public void onNull() { 99 | this.builder.append("null"); 100 | } 101 | 102 | public void onBoolean(boolean value) { 103 | this.builder.append(value); 104 | } 105 | 106 | public void onNumber(BigInteger value) { 107 | this.builder.append(value.toString()); 108 | } 109 | 110 | public void onDouble(double value) { 111 | this.builder.append(value); 112 | } 113 | 114 | public void onString(String value) { 115 | this.builder.append(String.format("\"%s\"", value)); 116 | } 117 | 118 | public void onArrayStart() { 119 | this.builder.append("["); 120 | } 121 | 122 | public void onArrayEnd() { 123 | this.builder.append("]"); 124 | } 125 | 126 | public void onArrayElementGap() { 127 | this.builder.append(","); 128 | } 129 | 130 | public void onObjectStart() { 131 | this.builder.append("{"); 132 | } 133 | 134 | public void onObjectEnd() { 135 | this.builder.append("}"); 136 | } 137 | 138 | public void onObjectField(String name) { 139 | this.builder.append(String.format("\"%s\":", name)); 140 | } 141 | 142 | public void onObjectFieldGap() { 143 | this.builder.append(","); 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/core/normalization/AlphaNormalize.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.core.normalization; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.LinkedList; 6 | import java.util.List; 7 | import java.util.Map; 8 | import org.dhallj.core.Expr; 9 | import org.dhallj.core.Visitor; 10 | 11 | /** 12 | * Performs alpha normalization. 13 | * 14 | *

Morally this is equivalent to the following (on non-underscore bindings): 15 | * 16 | *

 17 |  * input.increment("_").substitute(name, 0, Expr.Constants.UNDERSCORE).decrement(name);
 18 |  * 
19 | * 20 | * The implementation here is necessary to fit the visitor API. 21 | * 22 | *

Note that this visitor maintains internal state and instances should not be reused. 23 | */ 24 | public final class AlphaNormalize extends Visitor.Identity { 25 | // We interpret any underscores as implicitly having this index added to their own. 26 | private int newUnderscoreDepth = 0; 27 | 28 | // The total number of underscores. 29 | private int underscoreDepth = 0; 30 | 31 | // We change any other name to an underscore whose index we compute from the depth we track here. 32 | private final Map> nameDepths = 33 | new HashMap>(); 34 | 35 | @Override 36 | public Expr onIdentifier(Expr self, String name, long index) { 37 | if (name.equals("_")) { 38 | return Expr.makeIdentifier(name, index + this.newUnderscoreDepth); 39 | } else { 40 | LinkedList depths = this.nameDepths.get(name); 41 | 42 | if (depths == null) { 43 | return self; 44 | } else if (index < depths.size()) { 45 | return Expr.makeIdentifier("_", underscoreDepth - depths.get((int) index)); 46 | } else { 47 | return Expr.makeIdentifier(name, index - depths.size()); 48 | } 49 | } 50 | } 51 | 52 | @Override 53 | public void bind(String name, Expr type) { 54 | this.underscoreDepth += 1; 55 | if (!name.equals("_")) { 56 | this.newUnderscoreDepth += 1; 57 | 58 | LinkedList nameDepth = this.nameDepths.get(name); 59 | if (nameDepth == null) { 60 | nameDepth = new LinkedList(); 61 | nameDepths.put(name, nameDepth); 62 | } 63 | nameDepth.push(this.underscoreDepth); 64 | } 65 | } 66 | 67 | @Override 68 | public Expr onLambda(String name, Expr type, Expr result) { 69 | this.underscoreDepth -= 1; 70 | if (!name.equals("_")) { 71 | this.newUnderscoreDepth -= 1; 72 | this.nameDepths.get(name).pop(); 73 | } 74 | return Expr.makeLambda("_", type, result); 75 | } 76 | 77 | @Override 78 | public Expr onPi(String name, Expr type, Expr result) { 79 | this.underscoreDepth -= 1; 80 | if (!name.equals("_")) { 81 | this.newUnderscoreDepth -= 1; 82 | this.nameDepths.get(name).pop(); 83 | } 84 | return Expr.makePi("_", type, result); 85 | } 86 | 87 | @Override 88 | public Expr onLet(List> bindings, Expr body) { 89 | List> newBindings = new ArrayList<>(bindings.size()); 90 | 91 | for (Expr.LetBinding binding : bindings) { 92 | String name = binding.getName(); 93 | 94 | this.underscoreDepth -= 1; 95 | if (!name.equals("_")) { 96 | this.newUnderscoreDepth -= 1; 97 | this.nameDepths.get(name).pop(); 98 | } 99 | 100 | newBindings.add(new Expr.LetBinding<>("_", binding.getType(), binding.getValue())); 101 | } 102 | 103 | return Expr.makeLet(newBindings, body); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /modules/circe/src/test/scala/org/dhallj/circe/CirceConverterSuite.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.circe 2 | 3 | import io.circe.Json 4 | import io.circe.syntax._ 5 | import io.circe.testing.instances._ 6 | import munit.ScalaCheckSuite 7 | import org.dhallj.ast._ 8 | import org.dhallj.core.Expr 9 | import org.dhallj.parser.DhallParser 10 | import org.scalacheck.Prop 11 | 12 | class CirceConverterSuite extends ScalaCheckSuite { 13 | property("round-trip Json values through Dhall expressions") { 14 | Prop.forAll { (value: Json) => 15 | val cleanedJson = value.foldWith(JsonCleaner) 16 | val asDhall = Converter(cleanedJson) 17 | Converter(asDhall) == Some(cleanedJson) 18 | } 19 | } 20 | 21 | property("convert integers") { 22 | Prop.forAll { (value: BigInt) => 23 | val asDhall = IntegerLiteral(value.underlying) 24 | Converter(asDhall) == Some(value.asJson) 25 | } 26 | } 27 | 28 | property("convert lists of integers") { 29 | Prop.forAll { (values: Vector[BigInt]) => 30 | val asDhall = if (values.isEmpty) { 31 | EmptyListLiteral(Expr.Constants.INTEGER) 32 | } else { 33 | val exprs = values.map(value => IntegerLiteral(value.underlying)) 34 | NonEmptyListLiteral(exprs.head, exprs.tail) 35 | } 36 | Converter(asDhall) == Some(values.asJson) 37 | } 38 | } 39 | 40 | property("convert lists of doubles") { 41 | Prop.forAll { (values: Vector[Double]) => 42 | val asDhall = if (values.isEmpty) { 43 | EmptyListLiteral(Expr.Constants.DOUBLE) 44 | } else { 45 | val exprs = values.map(DoubleLiteral(_)) 46 | NonEmptyListLiteral(exprs.head, exprs.tail) 47 | } 48 | Converter(asDhall) == Some(values.asJson) 49 | } 50 | } 51 | 52 | property("convert lists of booleans") { 53 | Prop.forAll { (values: Vector[Boolean]) => 54 | val asDhall = if (values.isEmpty) { 55 | EmptyListLiteral(Expr.Constants.BOOL) 56 | } else { 57 | val exprs = values.map(BoolLiteral(_)) 58 | NonEmptyListLiteral(exprs.head, exprs.tail) 59 | } 60 | Converter(asDhall) == Some(values.asJson) 61 | } 62 | } 63 | 64 | test("convert nested lists") { 65 | val expr = DhallParser.parse("[[]: List Bool]") 66 | 67 | assert(Converter(expr) == Some(Json.arr(Json.arr()))) 68 | } 69 | 70 | test("convert None") { 71 | val expr = DhallParser.parse("None Bool") 72 | 73 | assert(Converter(expr) == Some(Json.Null)) 74 | } 75 | 76 | test("convert Some") { 77 | val expr = DhallParser.parse("""Some "foo"""") 78 | 79 | assert(Converter(expr) == Some(Json.fromString("foo"))) 80 | } 81 | 82 | test("convert records") { 83 | val expr1 = DhallParser.parse("{foo = [{bar = [1]}, {bar = [1, 2, 3]}]}") 84 | val Right(json1) = io.circe.jawn.parse("""{"foo": [{"bar": [1]}, {"bar": [1, 2, 3]}]}""") 85 | 86 | assert(Converter(expr1) == Some(json1)) 87 | } 88 | 89 | test("convert unions (nullary constructors)") { 90 | val expr1 = DhallParser.parse("[((\\(x: Natural) -> ) 1).bar]").normalize() 91 | val Right(json1) = io.circe.jawn.parse("""["bar"]""") 92 | 93 | assert(Converter(expr1) == Some(json1)) 94 | } 95 | 96 | test("convert unions") { 97 | val expr1 = DhallParser.parse("[.foo True]").normalize() 98 | val Right(json1) = io.circe.jawn.parse("""[true]""") 99 | 100 | assert(Converter(expr1) == Some(json1)) 101 | } 102 | 103 | test("fail safely on unconvertible expressions") { 104 | val expr1 = Lambda("x", Expr.Constants.NATURAL, Identifier("x")) 105 | 106 | assert(Converter(expr1) == None) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /modules/parser/src/test/scala/org/dhallj/parser/DhallParserSuite.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.parser 2 | 3 | import java.net.URI 4 | import java.nio.file.Paths 5 | 6 | import munit.{FunSuite, Ignore} 7 | import org.dhallj.core.DhallException.ParsingFailure 8 | import org.dhallj.core.Expr 9 | import org.dhallj.core.Expr.ImportMode 10 | 11 | class DhallParserSuite extends FunSuite() { 12 | test("parse empty list with annotation on element type") { 13 | val expected = Expr.makeEmptyListLiteral( 14 | Expr.makeAnnotated(Expr.makeApplication(Expr.Constants.LIST, Expr.Constants.NATURAL), Expr.Constants.TYPE) 15 | ) 16 | // Output from dhall-haskell. 17 | val expectedBytes: Array[Byte] = Array(-126, 24, 28, -125, 24, 26, -125, 0, 100, 76, 105, 115, 116, 103, 78, 97, 18 | 116, 117, 114, 97, 108, 100, 84, 121, 112, 101) 19 | val parsed = DhallParser.parse("[]: List Natural: Type") 20 | 21 | assert(parsed == expected) 22 | assert(parsed.getEncodedBytes.sameElements(expectedBytes)) 23 | } 24 | 25 | test("parse toMap with empty record with annotation on type") { 26 | // Output from dhall-haskell. 27 | val expectedBytes: Array[Byte] = Array(-125, 24, 27, -126, 7, -96, -125, 24, 26, -125, 0, 100, 76, 105, 115, 116, 28 | -126, 7, -94, 102, 109, 97, 112, 75, 101, 121, 100, 84, 101, 120, 116, 104, 109, 97, 112, 86, 97, 108, 117, 101, 29 | 100, 66, 111, 111, 108, 100, 84, 121, 112, 101) 30 | val parsed = DhallParser.parse("toMap {}: List { mapKey : Text, mapValue: Bool }: Type") 31 | 32 | assert(parsed.getEncodedBytes.sameElements(expectedBytes)) 33 | } 34 | 35 | test("parse toMap with empty non-record with annotation on type") { 36 | // Output from dhall-haskell. 37 | val expectedBytes: Array[Byte] = Array(-125, 24, 27, -126, 8, -95, 97, 97, -11, -125, 24, 26, -125, 0, 100, 76, 105, 38 | 115, 116, -126, 7, -94, 102, 109, 97, 112, 75, 101, 121, 100, 84, 101, 120, 116, 104, 109, 97, 112, 86, 97, 108, 39 | 117, 101, 100, 66, 111, 111, 108, 100, 84, 121, 112, 101) 40 | val parsed = DhallParser.parse("toMap {a=True}: List { mapKey : Text, mapValue: Bool }: Type") 41 | 42 | assert(parsed.getEncodedBytes.sameElements(expectedBytes)) 43 | } 44 | 45 | test("parse merge with annotation on type") { 46 | // Output from dhall-haskell. 47 | val expectedBytes: Array[Byte] = Array(-124, 6, -126, 8, -95, 97, 97, -126, 15, 1, -125, 9, -126, 11, -95, 97, 97, 48 | -10, 97, 97, -125, 24, 26, 103, 78, 97, 116, 117, 114, 97, 108, 100, 84, 121, 112, 101) 49 | val parsed = DhallParser.parse("merge {a=1} .a: Natural: Type") 50 | 51 | assert(clue(parsed.getEncodedBytes).sameElements(clue(expectedBytes))) 52 | } 53 | 54 | test("parse IPv6 address") { 55 | val expected = Expr.makeRemoteImport(new URI("https://[0:0:0:0:0:0:0:1]/"), null, Expr.ImportMode.CODE, null) 56 | 57 | assert(DhallParser.parse("https://[0:0:0:0:0:0:0:1]/") == expected) 58 | } 59 | 60 | test("parse $ in double-quoted text literals") { 61 | val expected = Expr.makeTextLiteral("$ $ $100 $ $") 62 | 63 | assert(DhallParser.parse("""let x = "100" in "$ $ $${x} $ $" """) == expected) 64 | } 65 | 66 | test("parse # in double-quoted text literals") { 67 | val expected = Expr.makeTextLiteral("# # # $ % ^ #") 68 | 69 | assert(DhallParser.parse(""""# # # $ % ^ #"""") == expected) 70 | } 71 | 72 | test("parse classpath import") { 73 | val expected = Expr.makeClasspathImport(Paths.get("/foo/bar.dhall"), ImportMode.RAW_TEXT, null) 74 | 75 | assert(DhallParser.parse("classpath:/foo/bar.dhall as Text") == expected) 76 | } 77 | 78 | test("fail on URLs with quoted paths") { 79 | intercept[ParsingFailure](DhallParser.parse("https://example.com/foo/\"bar?baz\"?qux")) 80 | } 81 | 82 | test("handle single-quoted escape sequences") { 83 | val expected = Expr.makeTextLiteral("foo '' bar ${ baz '''' qux") 84 | 85 | val input = """'' 86 | foo ''' bar ''${ baz '''''' qux''""" 87 | 88 | assertEquals(DhallParser.parse(input): Expr, expected) 89 | 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/core/normalization/BetaNormalizeFieldAccess.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.core.normalization; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Map.Entry; 6 | import org.dhallj.core.Expr; 7 | import org.dhallj.core.ExternalVisitor; 8 | import org.dhallj.core.Operator; 9 | 10 | final class BetaNormalizeFieldAccess extends ExternalVisitor.Constant { 11 | private final String fieldName; 12 | 13 | public BetaNormalizeFieldAccess(String fieldName) { 14 | super(null); 15 | this.fieldName = fieldName; 16 | } 17 | 18 | static final Expr apply(Expr base, String fieldName) { 19 | Expr result = base.accept(new BetaNormalizeFieldAccess(fieldName)); 20 | 21 | if (result != null) { 22 | return result; 23 | } else { 24 | return Expr.makeFieldAccess(base, fieldName); 25 | } 26 | } 27 | 28 | @Override 29 | public Expr onRecord(Iterable> fields, int size) { 30 | return NormalizationUtilities.lookup(fields, fieldName); 31 | } 32 | 33 | @Override 34 | public Expr onProjection(Expr base, String[] fieldNames0) { 35 | return Expr.makeFieldAccess(base, fieldName).accept(BetaNormalize.instance); 36 | } 37 | 38 | @Override 39 | public Expr onOperatorApplication(Operator operator, Expr lhs, Expr rhs) { 40 | if (operator.equals(Operator.PREFER)) { 41 | List> lhsFields = Expr.Util.asRecordLiteral(lhs); 42 | if (lhsFields != null) { 43 | Entry lhsFound = NormalizationUtilities.lookupEntry(lhsFields, fieldName); 44 | 45 | if (lhsFound != null) { 46 | List> singleton = new ArrayList(); 47 | singleton.add(lhsFound); 48 | 49 | return Expr.makeFieldAccess( 50 | Expr.makeOperatorApplication(Operator.PREFER, Expr.makeRecordLiteral(singleton), rhs), 51 | fieldName); 52 | } else { 53 | return Expr.makeFieldAccess(rhs, fieldName); 54 | } 55 | } else { 56 | List> rhsFields = Expr.Util.asRecordLiteral(rhs); 57 | if (rhsFields != null) { 58 | Expr rhsFound = NormalizationUtilities.lookup(rhsFields, fieldName); 59 | 60 | if (rhsFound != null) { 61 | return rhsFound; 62 | } else { 63 | return Expr.makeFieldAccess(lhs, fieldName).accept(BetaNormalize.instance); 64 | } 65 | } 66 | } 67 | } else if (operator.equals(Operator.COMBINE)) { 68 | List> lhsFields = Expr.Util.asRecordLiteral(lhs); 69 | if (lhsFields != null) { 70 | Entry lhsFound = NormalizationUtilities.lookupEntry(lhsFields, fieldName); 71 | 72 | if (lhsFound != null) { 73 | List> singleton = new ArrayList<>(); 74 | singleton.add(lhsFound); 75 | 76 | return Expr.makeFieldAccess( 77 | Expr.makeOperatorApplication( 78 | Operator.COMBINE, Expr.makeRecordLiteral(singleton), rhs), 79 | fieldName); 80 | } else { 81 | return Expr.makeFieldAccess(rhs, fieldName); 82 | } 83 | } else { 84 | List> rhsFields = Expr.Util.asRecordLiteral(rhs); 85 | if (rhsFields != null) { 86 | Entry rhsFound = NormalizationUtilities.lookupEntry(rhsFields, fieldName); 87 | 88 | if (rhsFound != null) { 89 | List> singleton = new ArrayList<>(); 90 | singleton.add(rhsFound); 91 | 92 | return Expr.makeFieldAccess( 93 | Expr.makeOperatorApplication( 94 | Operator.COMBINE, lhs, Expr.makeRecordLiteral(singleton)), 95 | fieldName); 96 | } else { 97 | return Expr.makeFieldAccess(lhs, fieldName).accept(BetaNormalize.instance); 98 | } 99 | } 100 | } 101 | } 102 | return null; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /modules/javagen/src/main/java/org/dhallj/javagen/Code.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.javagen 2 | 3 | case class Code(content: String, defs: Vector[Code] = Vector.empty) { 4 | final private def replace(in: String, target: Int, newName: String): String = { 5 | var last = 0 6 | var next = in.indexOf(Code.marker, last) 7 | val builder = new StringBuilder() 8 | 9 | while (next >= 0) { 10 | builder.append(in.substring(last, next)) 11 | val index = in.substring(next + Code.marker.length, next + Code.marker.length + Code.indexLength).toInt 12 | if (index == target) { 13 | builder.append(newName) 14 | } else { 15 | builder.append(in.substring(next, next + Code.marker.length + Code.indexLength)) 16 | } 17 | 18 | last = next + Code.marker.length + Code.indexLength 19 | next = in.indexOf(Code.marker, last) 20 | } 21 | 22 | builder.append(in.substring(last)) 23 | return builder.toString() 24 | } 25 | 26 | def mapContent(f: String => String): Code = Code(f(Code.makeIdentifier(0)), Vector(this)) 27 | 28 | def merge(other: Code)(f: (String, String) => String): Code = 29 | Option(other) match { 30 | case Some(otherValue) => Code(f(Code.makeIdentifier(0), Code.makeIdentifier(1)), Vector(this, other)) 31 | case None => this.copy(content = f(content, "null")) 32 | } 33 | 34 | def merge(other0: Code, other1: Code)(f: (String, String, String) => String): Code = 35 | Option(other1) match { 36 | case Some(otherValue) => 37 | Code(f(Code.makeIdentifier(0), Code.makeIdentifier(1), Code.makeIdentifier(2)), Vector(this, other0, other1)) 38 | case None => Code(f(Code.makeIdentifier(0), Code.makeIdentifier(1), "null"), Vector(this, other0)) 39 | } 40 | 41 | protected def toFields(known: Map[Code, (String, String)]): (String, Map[Code, (String, String)]) = 42 | known.get(this) match { 43 | case Some((name, impl)) => (name, known) 44 | case None => 45 | val (newContent, newKnown) = this.defs.zipWithIndex.foldLeft((this.content, known)) { 46 | case ((accContent, accKnown), (code, i)) => 47 | val (childName, newKnown) = code.toFields(accKnown) 48 | 49 | (this.replace(accContent, i, childName), newKnown) 50 | } 51 | 52 | val nextName = Code.makeFieldName(newKnown.size) 53 | (nextName, newKnown.updated(this, (nextName, newContent))) 54 | } 55 | 56 | def toClassDef(packageName: String, className: String): String = { 57 | val (topLevelFieldName, fields) = toFields(Map.empty) 58 | 59 | val fieldDefs = fields.values.toList 60 | .sortBy(_._1) 61 | .map { case (name, impl) => 62 | s" private static final Expr $name = $impl;" 63 | } 64 | .mkString("\n") 65 | 66 | s"""package $packageName; 67 | | 68 | |import java.math.BigInteger; 69 | |import java.util.AbstractMap.SimpleImmutableEntry; 70 | |import java.util.ArrayList; 71 | |import java.util.List; 72 | |import java.util.Map.Entry; 73 | |import org.dhallj.core.Expr; 74 | |import org.dhallj.core.Operator; 75 | | 76 | |public final class $className { 77 | |$fieldDefs 78 | | 79 | |public static final Expr instance = $topLevelFieldName; 80 | |} 81 | |""".stripMargin 82 | } 83 | } 84 | 85 | object Code { 86 | private[javagen] val marker: String = "__" 87 | private[javagen] val indexLength: Int = 4 88 | private[javagen] def makeIdentifier(n: Int): String = 89 | String.format(s"%s%0${indexLength}d", marker, Int.box(n)) 90 | private[javagen] def makeFieldName(n: Int): String = f"f$n%06d" 91 | 92 | def mergeAll(other: Vector[Code])(f: Vector[String] => String): Code = 93 | Code(f(0.until(other.size).map(makeIdentifier).toVector), other) 94 | 95 | def mergeAll[A](other: Vector[A])(extract: A => Option[Code])(f: Vector[(A, String)] => String): Code = { 96 | var i = -1 97 | val values = other.map { value => 98 | if (extract(value).isDefined) { 99 | i += 1 100 | (value, makeIdentifier(i)) 101 | } else { 102 | (value, "null") 103 | } 104 | } 105 | 106 | Code(f(values), other.flatMap(extract(_))) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /scalastyle-config.xml: -------------------------------------------------------------------------------- 1 | 2 | Circe Configuration 3 | 4 | 5 | FOR 6 | IF 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | true 77 | 78 | 79 | 80 | 81 | all 82 | .+ 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /modules/imports/src/main/scala/org/dhallj/imports/Canonicalization.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.imports 2 | 3 | import java.nio.file.{Path, Paths} 4 | 5 | import cats.implicits._ 6 | import cats.effect.Sync 7 | import org.dhallj.core.DhallException.ResolutionFailure 8 | import org.dhallj.imports.ImportContext._ 9 | 10 | import scala.collection.JavaConverters._ 11 | 12 | object Canonicalization { 13 | 14 | def canonicalize[F[_]](imp: ImportContext)(implicit F: Sync[F]): F[ImportContext] = imp match { 15 | case Remote(uri, headers) => F.delay(Remote(uri.normalize, headers)) 16 | case Local(path) => LocalFile[F](path).map(_.canonicalize.toPath).map(Local) 17 | case Classpath(path) => LocalFile[F](path).map(_.canonicalize.toPath).map(Classpath) 18 | case i => F.pure(i) 19 | } 20 | 21 | def canonicalize[F[_]](parent: ImportContext, child: ImportContext)(implicit 22 | F: Sync[F] 23 | ): F[ImportContext] = 24 | parent match { 25 | case Remote(uri, headers) => 26 | child match { 27 | //A transitive relative import is parsed as local but is resolved as a remote import 28 | // eg https://github.com/dhall-lang/dhall-lang/blob/master/Prelude/Integer/add has a local import but we still 29 | //need to resolve this as a remote import 30 | //Also note that if the path is absolute then this violates referential sanity but we handle that elsewhere 31 | case Local(path) => 32 | if (path.isAbsolute) canonicalize(child) 33 | else canonicalize(Remote(uri.resolve(path.toString), headers)) 34 | case _ => canonicalize(child) 35 | } 36 | case Local(path) => 37 | child match { 38 | case Local(path2) => 39 | for { 40 | parent <- LocalFile[F](path) 41 | c <- LocalFile[F](path2) 42 | } yield Local(parent.chain(c).canonicalize.toPath) 43 | case _ => canonicalize(child) 44 | } 45 | //TODO - determine semantics of classpath imports 46 | case Classpath(path) => 47 | child match { 48 | //Also note that if the path is absolute then this violates referential sanity but we handle that elsewhere 49 | case Local(path2) => 50 | for { 51 | parent <- LocalFile[F](path) 52 | c <- LocalFile[F](path2) 53 | } yield Classpath(parent.chain(c).canonicalize.toPath) 54 | case _ => canonicalize(child) 55 | } 56 | case _ => canonicalize(child) 57 | } 58 | 59 | private case class LocalFile(dirs: LocalDirs, filename: String) { 60 | def toPath: Path = { 61 | def toPath(l: List[String]) = "/" + l.intercalate("/") 62 | 63 | val s: String = dirs.ds match { 64 | case Nil => "" 65 | case l @ (h :: t) => 66 | h match { 67 | case h if h == "." || h == ".." || h == "~" => s"$h${toPath(t)}" 68 | case _ => toPath(l) 69 | } 70 | } 71 | Paths.get(s"$s/$filename") 72 | } 73 | 74 | def chain(other: LocalFile): LocalFile = LocalFile.chain(this, other) 75 | 76 | def canonicalize: LocalFile = LocalFile.canonicalize(this) 77 | } 78 | 79 | private object LocalFile { 80 | def apply[F[_]](path: Path)(implicit F: Sync[F]): F[LocalFile] = 81 | path.iterator().asScala.toList.map(_.toString) match { 82 | case Nil => F.raiseError(new ResolutionFailure("This shouldn't happen - / can't import a dhall expression")) 83 | case l => F.pure(LocalFile(LocalDirs(l.take(l.length - 1)), l.last)) 84 | } 85 | 86 | def canonicalize(f: LocalFile): LocalFile = 87 | LocalFile(f.dirs.canonicalize, f.filename.stripPrefix("\"").stripSuffix("\"")) 88 | 89 | def chain(lhs: LocalFile, rhs: LocalFile): LocalFile = LocalFile(LocalDirs.chain(lhs.dirs, rhs.dirs), rhs.filename) 90 | } 91 | 92 | private case class LocalDirs(ds: List[String]) { 93 | def isRelative: Boolean = ds.nonEmpty && (ds.head == "." || ds.head == "..") 94 | 95 | def canonicalize: LocalDirs = LocalDirs.canonicalize(this) 96 | 97 | def chain(other: LocalDirs): LocalDirs = LocalDirs.chain(this, other) 98 | } 99 | 100 | private object LocalDirs { 101 | def chain(lhs: LocalDirs, rhs: LocalDirs): LocalDirs = if (rhs.isRelative) LocalDirs(lhs.ds ++ rhs.ds) else rhs 102 | 103 | def canonicalize(d: LocalDirs): LocalDirs = d.ds match { 104 | case Nil => d 105 | case l => LocalDirs(canonicalize(l.map(_.stripPrefix("\"").stripSuffix("\"")))) 106 | } 107 | 108 | def canonicalize(l: List[String]): List[String] = 109 | l.tail 110 | .foldLeft(List(l.head)) { (acc, next) => 111 | next match { 112 | case "." => acc 113 | case ".." => acc.tail 114 | case o => o :: acc 115 | } 116 | } 117 | .reverse 118 | 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/core/typechecking/BuiltInTypes.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.core.typechecking; 2 | 3 | import java.util.AbstractMap.SimpleImmutableEntry; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import java.util.Map.Entry; 7 | import org.dhallj.core.Expr; 8 | import org.dhallj.core.Expr.Constants; 9 | 10 | final class BuiltInTypes { 11 | static final Expr getType(String name) { 12 | return mappings.get(name); 13 | } 14 | 15 | private static final int SIZE = 34; 16 | private static final Map mappings = new HashMap<>(SIZE); 17 | 18 | static { 19 | Expr typeToType = Expr.makePi(Constants.TYPE, Constants.TYPE); 20 | Expr naturalToBool = Expr.makePi(Constants.NATURAL, Constants.BOOL); 21 | Expr naturalToNatural = Expr.makePi(Constants.NATURAL, Constants.NATURAL); 22 | Expr _natural = Expr.makeIdentifier("natural"); 23 | Expr naturalType = 24 | Expr.makePi( 25 | "natural", 26 | Constants.TYPE, 27 | Expr.makePi( 28 | "succ", Expr.makePi(_natural, _natural), Expr.makePi("zero", _natural, _natural))); 29 | Expr _a = Expr.makeIdentifier("a"); 30 | Expr listA = Expr.makeApplication(Constants.LIST, _a); 31 | Expr optionalA = Expr.makeApplication(Constants.OPTIONAL, _a); 32 | Expr listAToOptionalA = Expr.makePi("a", Expr.Constants.TYPE, Expr.makePi(listA, optionalA)); 33 | 34 | Expr _list = Expr.makeIdentifier("list"); 35 | Expr listType = 36 | Expr.makePi( 37 | "list", 38 | Constants.TYPE, 39 | Expr.makePi( 40 | "cons", 41 | Expr.makePi(_a, Expr.makePi(_list, _list)), 42 | Expr.makePi("nil", _list, _list))); 43 | 44 | mappings.put("Kind", Constants.SORT); 45 | mappings.put("Type", Constants.KIND); 46 | mappings.put("Bool", Constants.TYPE); 47 | mappings.put("True", Constants.BOOL); 48 | mappings.put("False", Constants.BOOL); 49 | mappings.put("Natural", Constants.TYPE); 50 | mappings.put("Integer", Constants.TYPE); 51 | mappings.put("Text", Constants.TYPE); 52 | mappings.put("Double", Constants.TYPE); 53 | mappings.put("List", typeToType); 54 | mappings.put("Optional", typeToType); 55 | mappings.put( 56 | "None", 57 | Expr.makePi( 58 | "A", 59 | Constants.TYPE, 60 | Expr.makeApplication(Constants.OPTIONAL, Expr.makeIdentifier("A")))); 61 | 62 | mappings.put( 63 | "Text/replace", 64 | Expr.makePi( 65 | "needle", 66 | Constants.TEXT, 67 | Expr.makePi( 68 | "replacement", 69 | Constants.TEXT, 70 | Expr.makePi("haystack", Constants.TEXT, Constants.TEXT)))); 71 | mappings.put("Text/show", Expr.makePi(Constants.TEXT, Constants.TEXT)); 72 | 73 | mappings.put("Natural/build", Expr.makePi(naturalType, Constants.NATURAL)); 74 | mappings.put("Natural/fold", Expr.makePi(Constants.NATURAL, naturalType)); 75 | mappings.put("Natural/isZero", naturalToBool); 76 | mappings.put("Natural/even", naturalToBool); 77 | mappings.put("Natural/odd", naturalToBool); 78 | mappings.put("Natural/toInteger", Expr.makePi(Constants.NATURAL, Constants.INTEGER)); 79 | mappings.put("Natural/show", Expr.makePi(Constants.NATURAL, Constants.TEXT)); 80 | mappings.put("Natural/subtract", Expr.makePi(Constants.NATURAL, naturalToNatural)); 81 | 82 | mappings.put("Integer/show", Expr.makePi(Constants.INTEGER, Constants.TEXT)); 83 | mappings.put("Integer/toDouble", Expr.makePi(Constants.INTEGER, Constants.DOUBLE)); 84 | mappings.put("Integer/negate", Expr.makePi(Constants.INTEGER, Constants.INTEGER)); 85 | mappings.put("Integer/clamp", Expr.makePi(Constants.INTEGER, Constants.NATURAL)); 86 | 87 | mappings.put("Double/show", Expr.makePi(Constants.DOUBLE, Constants.TEXT)); 88 | 89 | mappings.put("List/build", Expr.makePi("a", Constants.TYPE, Expr.makePi(listType, listA))); 90 | mappings.put("List/fold", Expr.makePi("a", Constants.TYPE, Expr.makePi(listA, listType))); 91 | mappings.put( 92 | "List/length", 93 | Expr.makePi("a", Expr.Constants.TYPE, Expr.makePi(listA, Constants.NATURAL))); 94 | mappings.put("List/head", listAToOptionalA); 95 | mappings.put("List/last", listAToOptionalA); 96 | 97 | Entry[] indexedRecordFields = { 98 | new SimpleImmutableEntry<>("index", Constants.NATURAL), 99 | new SimpleImmutableEntry<>("value", _a) 100 | }; 101 | 102 | mappings.put( 103 | "List/indexed", 104 | Expr.makePi( 105 | "a", 106 | Constants.TYPE, 107 | Expr.makePi( 108 | listA, 109 | Expr.makeApplication(Constants.LIST, Expr.makeRecordType(indexedRecordFields))))); 110 | mappings.put("List/reverse", Expr.makePi("a", Constants.TYPE, Expr.makePi(listA, listA))); 111 | 112 | mappings.put("Some", Constants.SOME); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/core/normalization/BetaNormalize.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.core.normalization; 2 | 3 | import java.math.BigInteger; 4 | import java.net.URI; 5 | import java.nio.file.Path; 6 | import java.util.Collections; 7 | import java.util.List; 8 | import java.util.Map.Entry; 9 | import java.util.Set; 10 | import java.util.TreeSet; 11 | import org.dhallj.core.Expr; 12 | import org.dhallj.core.Operator; 13 | import org.dhallj.core.Source; 14 | import org.dhallj.core.Visitor; 15 | 16 | /** 17 | * Performs beta normalization. 18 | * 19 | *

This is a stateless visitor intended for use as a singleton. 20 | */ 21 | public final class BetaNormalize extends Visitor.NoPrepareEvents { 22 | public static final Visitor instance = new BetaNormalize(); 23 | 24 | public void bind(String name, Expr type) {} 25 | 26 | public Expr onNote(Expr base, Source source) { 27 | return base; 28 | } 29 | 30 | public Expr onNatural(Expr self, BigInteger value) { 31 | return self; 32 | } 33 | 34 | public Expr onInteger(Expr self, BigInteger value) { 35 | return self; 36 | } 37 | 38 | public Expr onDouble(Expr self, double value) { 39 | return self; 40 | } 41 | 42 | public Expr onBuiltIn(Expr self, String name) { 43 | return self; 44 | } 45 | 46 | public Expr onIdentifier(Expr self, String name, long index) { 47 | return self; 48 | } 49 | 50 | public Expr onLambda(String name, Expr type, Expr result) { 51 | return Expr.makeLambda(name, type, result); 52 | } 53 | 54 | public Expr onPi(String name, Expr type, Expr result) { 55 | return Expr.makePi(name, type, result); 56 | } 57 | 58 | public Expr onLet(List> bindings, Expr body) { 59 | Expr result = body; 60 | 61 | for (int i = bindings.size() - 1; i >= 0; i--) { 62 | Expr.LetBinding binding = bindings.get(i); 63 | String name = binding.getName(); 64 | 65 | result = result.substitute(name, binding.getValue()); 66 | } 67 | 68 | return result.accept(this); 69 | } 70 | 71 | public Expr onText(String[] parts, List interpolated) { 72 | return BetaNormalizeTextLiteral.apply(parts, interpolated); 73 | } 74 | 75 | public Expr onNonEmptyList(List values) { 76 | return Expr.makeNonEmptyListLiteral(values); 77 | } 78 | 79 | public Expr onEmptyList(Expr type) { 80 | return Expr.makeEmptyListLiteral(type); 81 | } 82 | 83 | public Expr onRecord(List> fields) { 84 | Collections.sort(fields, NormalizationUtilities.entryComparator); 85 | return Expr.makeRecordLiteral(fields); 86 | } 87 | 88 | public Expr onRecordType(List> fields) { 89 | Collections.sort(fields, NormalizationUtilities.entryComparator); 90 | return Expr.makeRecordType(fields); 91 | } 92 | 93 | public Expr onUnionType(List> fields) { 94 | Collections.sort(fields, NormalizationUtilities.entryComparator); 95 | return Expr.makeUnionType(fields); 96 | } 97 | 98 | public Expr onFieldAccess(Expr base, String fieldName) { 99 | return BetaNormalizeFieldAccess.apply(base, fieldName); 100 | } 101 | 102 | public Expr onProjection(Expr base, String[] fieldNames) { 103 | return BetaNormalizeProjection.apply(base, fieldNames); 104 | } 105 | 106 | public Expr onProjectionByType(Expr base, Expr arg) { 107 | List> argAsRecordType = Expr.Util.asRecordType(arg); 108 | Set keys = new TreeSet<>(); 109 | for (Entry entry : argAsRecordType) { 110 | keys.add(entry.getKey()); 111 | } 112 | 113 | return Expr.makeProjection(base, keys.toArray(new String[keys.size()])).accept(this); 114 | } 115 | 116 | public Expr onApplication(Expr base, List args) { 117 | return BetaNormalizeApplication.apply(base, args); 118 | } 119 | 120 | public Expr onOperatorApplication(Operator operator, Expr lhs, Expr rhs) { 121 | return BetaNormalizeOperatorApplication.apply(operator, lhs, rhs); 122 | } 123 | 124 | public Expr onIf(Expr predicate, Expr thenValue, Expr elseValue) { 125 | return BetaNormalizeIf.apply(predicate, thenValue, elseValue); 126 | } 127 | 128 | public Expr onAnnotated(Expr base, Expr type) { 129 | return base; 130 | } 131 | 132 | public Expr onAssert(Expr type) { 133 | return Expr.makeAssert(type); 134 | } 135 | 136 | public Expr onMerge(Expr handlers, Expr union, Expr type) { 137 | return BetaNormalizeMerge.apply(handlers, union, type); 138 | } 139 | 140 | public Expr onToMap(Expr base, Expr type) { 141 | return BetaNormalizeToMap.apply(base, type); 142 | } 143 | 144 | public Expr onWith(Expr base, String[] path, Expr value) { 145 | return BetaNormalizeWith.apply(base, path, value); 146 | } 147 | 148 | public Expr onMissingImport(Expr.ImportMode mode, byte[] hash) { 149 | return Expr.makeMissingImport(mode, hash); 150 | } 151 | 152 | public Expr onEnvImport(String value, Expr.ImportMode mode, byte[] hash) { 153 | return Expr.makeEnvImport(value, mode, hash); 154 | } 155 | 156 | public Expr onLocalImport(Path path, Expr.ImportMode mode, byte[] hash) { 157 | return Expr.makeLocalImport(path, mode, hash); 158 | } 159 | 160 | @Override 161 | public Expr onClasspathImport(Path path, Expr.ImportMode mode, byte[] hash) { 162 | return Expr.makeClasspathImport(path, mode, hash); 163 | } 164 | 165 | public Expr onRemoteImport(URI url, Expr using, Expr.ImportMode mode, byte[] hash) { 166 | return Expr.makeRemoteImport(url, using, mode, hash); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /tests/src/main/scala/org/dhallj/tests/acceptance/AcceptanceSuccessSuite.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.tests.acceptance 2 | 3 | import cats.effect.{ContextShift, IO} 4 | import java.nio.file.{Path, Paths} 5 | 6 | import org.dhallj.core.Expr 7 | import org.dhallj.core.binary.Decode.decode 8 | import org.dhallj.parser.DhallParser 9 | 10 | import org.dhallj.imports.{ImportCache, Resolver} 11 | import org.http4s.client._ 12 | import org.http4s.client.blaze._ 13 | 14 | import scala.concurrent.ExecutionContext 15 | 16 | trait SuccessSuite[A, B] extends AcceptanceSuite { 17 | self: Input[A] => 18 | 19 | def makeExpectedFilename(input: String): String 20 | final private def makeExpectedPath(inputPath: Path): Path = 21 | inputPath.resolveSibling(makeExpectedFilename(inputPath.getFileName.toString)) 22 | 23 | def transform(input: A): B 24 | def loadExpected(input: Array[Byte]): B 25 | def compare(result: B, expected: B): Boolean 26 | 27 | def testPairs: List[(String, Path, (String, B))] = testInputs.map { case (name, path) => 28 | (name, path, (readString(path), loadExpected(readBytes(makeExpectedPath(path))))) 29 | } 30 | 31 | testPairs.foreach { case (name, path, (input, expected)) => 32 | test(name) { 33 | assert(compare(clue(transform(parseInput(path.toString, clue(input)))), clue(expected))) 34 | } 35 | } 36 | } 37 | 38 | trait Input[A] { 39 | def parseInput(path: String, input: String): A 40 | 41 | } 42 | 43 | trait ParsingInput extends Input[Expr] { 44 | 45 | override def parseInput(path: String, input: String): Expr = DhallParser.parse(input) 46 | 47 | } 48 | 49 | trait CachedResolvingInput extends Input[Expr] { 50 | 51 | override def parseInput(path: String, input: String): Expr = { 52 | //TODO this should only be for import tests (I think) 53 | val parsed = DhallParser.parse(s"./$path") 54 | 55 | if (parsed.isResolved) parsed 56 | else { 57 | implicit val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.global) 58 | BlazeClientBuilder[IO](ExecutionContext.global).resource 59 | .use { client => 60 | implicit val c: Client[IO] = client 61 | Resolver.resolve[IO](parsed) 62 | } 63 | .unsafeRunSync() 64 | } 65 | } 66 | 67 | } 68 | 69 | trait ResolvingInput extends Input[Expr] { 70 | def parseInput(path: String, input: String): Expr = { 71 | //TODO this should only be for import tests (I think) 72 | val parsed = DhallParser.parse(s"./$path") 73 | 74 | if (parsed.isResolved) parsed 75 | else { 76 | implicit val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.global) 77 | BlazeClientBuilder[IO](ExecutionContext.global).resource 78 | .use { client => 79 | implicit val c: Client[IO] = client 80 | Resolver.resolve[IO](new ImportCache.NoopImportCache[IO], new ImportCache.NoopImportCache[IO])(parsed) 81 | } 82 | .unsafeRunSync() 83 | } 84 | } 85 | } 86 | 87 | abstract class ExprOperationAcceptanceSuite(transformation: Expr => Expr) extends SuccessSuite[Expr, Expr] { 88 | self: Input[Expr] => 89 | 90 | def makeExpectedFilename(input: String): String = input.dropRight(7) + "B.dhall" 91 | 92 | def transform(input: Expr): Expr = transformation(input) 93 | 94 | def loadExpected(input: Array[Byte]): Expr = DhallParser.parse(new String(input)) 95 | def compare(result: Expr, expected: Expr): Boolean = result.sameStructure(expected) && result.equivalent(expected) 96 | } 97 | 98 | class ParsingTypeCheckingSuite(val base: String, override val recurse: Boolean = false) 99 | extends ExprOperationAcceptanceSuite(Expr.Util.typeCheck(_)) 100 | with ParsingInput 101 | class TypeCheckingSuite(val base: String, override val recurse: Boolean = false) 102 | extends ExprOperationAcceptanceSuite(Expr.Util.typeCheck(_)) 103 | with ResolvingInput 104 | class AlphaNormalizationSuite(val base: String) extends ExprOperationAcceptanceSuite(_.alphaNormalize) with ParsingInput 105 | class NormalizationSuite(val base: String, override val recurse: Boolean = false) 106 | extends ExprOperationAcceptanceSuite(_.normalize) 107 | with CachedResolvingInput 108 | class NormalizationUSuite(val base: String) extends ExprOperationAcceptanceSuite(_.normalize) with ParsingInput 109 | 110 | class HashingSuite(val base: String, override val recurse: Boolean = false) 111 | extends SuccessSuite[Expr, String] 112 | with ResolvingInput { 113 | def makeExpectedFilename(input: String): String = input.dropRight(7) + "B.hash" 114 | 115 | def transform(input: Expr): String = input.normalize.alphaNormalize.hash 116 | def loadExpected(input: Array[Byte]): String = new String(input).trim.drop(7) 117 | def compare(result: String, expected: String): Boolean = result == expected 118 | } 119 | 120 | class ParsingSuite(val base: String) extends SuccessSuite[Expr, Array[Byte]] with ParsingInput { 121 | def makeExpectedFilename(input: String): String = input.dropRight(7) + "B.dhallb" 122 | 123 | def transform(input: Expr): Array[Byte] = input.getEncodedBytes 124 | def loadExpected(input: Array[Byte]): Array[Byte] = input 125 | def compare(result: Array[Byte], expected: Array[Byte]): Boolean = result.sameElements(expected) 126 | } 127 | 128 | class BinaryDecodingSuite(val base: String) extends SuccessSuite[Expr, Expr] with ParsingInput { 129 | def makeExpectedFilename(input: String): String = input.dropRight(8) + "B.dhall" 130 | 131 | override def isInputFileName(fileName: String): Boolean = fileName.endsWith("A.dhallb") 132 | 133 | override def parseInput(path: String, input: String): Expr = 134 | decode(readBytes(Paths.get(path))) 135 | 136 | def transform(input: Expr): Expr = input 137 | def loadExpected(input: Array[Byte]): Expr = DhallParser.parse(new String(input)) 138 | def compare(result: Expr, expected: Expr): Boolean = result.sameStructure(expected) && result.equivalent(expected) 139 | } 140 | -------------------------------------------------------------------------------- /modules/scala-codec/src/main/scala/org/dhallj/codec/Encoder.scala: -------------------------------------------------------------------------------- 1 | package org.dhallj.codec 2 | 3 | import org.dhallj.core.Expr 4 | import org.dhallj.ast._ 5 | 6 | trait Encoder[A] { self => 7 | def encode(value: A, target: Option[Expr]): Expr 8 | def dhallType(value: Option[A], target: Option[Expr]): Expr 9 | 10 | final def encode(value: A): Expr = encode(value, None) 11 | final def isExactType(typeExpr: Expr): Boolean = typeExpr == dhallType(None, Some(typeExpr)) 12 | 13 | def contramap[B](f: B => A): Encoder[B] = new Encoder[B] { 14 | def encode(value: B, target: Option[Expr]): Expr = self.encode(f(value), target) 15 | def dhallType(value: Option[B], target: Option[Expr]): Expr = 16 | self.dhallType(value.map(f), target) 17 | } 18 | } 19 | 20 | object Encoder { 21 | def apply[A](implicit A: Encoder[A]): Encoder[A] = A 22 | 23 | implicit val encodeLong: Encoder[Long] = new Encoder[Long] { 24 | def encode(value: Long, target: Option[Expr]): Expr = { 25 | val asBigInt = BigInt(value) 26 | 27 | target match { 28 | case Some(Expr.Constants.INTEGER) => IntegerLiteral(asBigInt) 29 | case _ => NaturalLiteral(asBigInt).getOrElse(IntegerLiteral(asBigInt)) 30 | } 31 | } 32 | 33 | def dhallType(value: Option[Long], target: Option[Expr]): Expr = target match { 34 | case Some(Expr.Constants.INTEGER) => Expr.Constants.INTEGER 35 | case _ => 36 | value match { 37 | case Some(v) if v < 0 => Expr.Constants.INTEGER 38 | case _ => Expr.Constants.NATURAL 39 | } 40 | } 41 | } 42 | 43 | implicit val encodeInt: Encoder[Int] = encodeLong.contramap(_.toLong) 44 | 45 | implicit val encodeBigInt: Encoder[BigInt] = new Encoder[BigInt] { 46 | def encode(value: BigInt, target: Option[Expr]): Expr = target match { 47 | case Some(Expr.Constants.INTEGER) => IntegerLiteral(value) 48 | case _ => NaturalLiteral(value).getOrElse(IntegerLiteral(value)) 49 | } 50 | 51 | def dhallType(value: Option[BigInt], target: Option[Expr]): Expr = target match { 52 | case Some(Expr.Constants.INTEGER) => Expr.Constants.INTEGER 53 | case _ => 54 | value match { 55 | case Some(v) if v < 0 => Expr.Constants.INTEGER 56 | case _ => Expr.Constants.NATURAL 57 | } 58 | } 59 | } 60 | 61 | implicit val encodeDouble: Encoder[Double] = new Encoder[Double] { 62 | def encode(value: Double, target: Option[Expr]): Expr = DoubleLiteral(value) 63 | def dhallType(value: Option[Double], target: Option[Expr]): Expr = Expr.Constants.DOUBLE 64 | } 65 | 66 | implicit val encodeString: Encoder[String] = new Encoder[String] { 67 | def encode(value: String, target: Option[Expr]): Expr = TextLiteral(value) 68 | def dhallType(value: Option[String], target: Option[Expr]): Expr = Expr.Constants.TEXT 69 | } 70 | 71 | implicit val encodeBoolean: Encoder[Boolean] = new Encoder[Boolean] { 72 | def encode(value: Boolean, target: Option[Expr]): Expr = BoolLiteral(value) 73 | def dhallType(value: Option[Boolean], target: Option[Expr]): Expr = Expr.Constants.BOOL 74 | } 75 | 76 | implicit def encodeOption[A: Encoder]: Encoder[Option[A]] = new Encoder[Option[A]] { 77 | def encode(value: Option[A], target: Option[Expr]): Expr = target match { 78 | case Some(Application(Expr.Constants.OPTIONAL, elementType)) => 79 | value match { 80 | case Some(a) => Application(Expr.Constants.SOME, Encoder[A].encode(a, Some(elementType))) 81 | case None => 82 | Application(Expr.Constants.NONE, Encoder[A].dhallType(None, Some(elementType))) 83 | } 84 | case _ => 85 | value match { 86 | case Some(a) => Application(Expr.Constants.SOME, Encoder[A].encode(a)) 87 | case None => 88 | Application(Expr.Constants.NONE, Encoder[A].dhallType(None, None)) 89 | } 90 | } 91 | 92 | def dhallType(value: Option[Option[A]], target: Option[Expr]): Expr = { 93 | val targetElementType = target.flatMap { 94 | case Application(Expr.Constants.OPTIONAL, elementType) => Some(elementType) 95 | case _ => None 96 | } 97 | 98 | Application(Expr.Constants.OPTIONAL, Encoder[A].dhallType(value.flatten, targetElementType)) 99 | } 100 | } 101 | 102 | implicit def encodeVector[A: Encoder]: Encoder[Vector[A]] = new Encoder[Vector[A]] { 103 | def encode(value: Vector[A], target: Option[Expr]): Expr = { 104 | val tpe = dhallElementType(Some(value), target) 105 | 106 | value match { 107 | case Vector() => EmptyListLiteral(Application(Expr.Constants.LIST, tpe)) 108 | case h +: t => 109 | NonEmptyListLiteral( 110 | Encoder[A].encode(h, Some(tpe)), 111 | t.map(Encoder[A].encode(_, Some(tpe))) 112 | ) 113 | } 114 | } 115 | 116 | private def dhallElementType(value: Option[Vector[A]], target: Option[Expr]): Expr = { 117 | val targetElementType = target.flatMap { 118 | case Application(Expr.Constants.LIST, elementType) => Some(elementType) 119 | case _ => None 120 | } 121 | 122 | value match { 123 | case None => Encoder[A].dhallType(None, targetElementType) 124 | case Some(Vector()) => Encoder[A].dhallType(None, targetElementType) 125 | case Some(h +: t) => 126 | t.foldLeft(Encoder[A].dhallType(Some(h), targetElementType)) { case (acc, a) => 127 | Encoder[A].dhallType(Some(a), Some(acc)) 128 | } 129 | } 130 | } 131 | 132 | def dhallType(value: Option[Vector[A]], target: Option[Expr]): Expr = 133 | Application(Expr.Constants.LIST, dhallElementType(value, target)) 134 | } 135 | 136 | implicit def encodeList[A: Encoder]: Encoder[List[A]] = encodeVector[A].contramap(_.toVector) 137 | } 138 | -------------------------------------------------------------------------------- /modules/core/src/main/java/org/dhallj/core/converters/JsonConverter.java: -------------------------------------------------------------------------------- 1 | package org.dhallj.core.converters; 2 | 3 | import java.math.BigInteger; 4 | import java.util.List; 5 | import java.util.Map.Entry; 6 | import org.dhallj.core.Expr; 7 | import org.dhallj.core.Visitor; 8 | 9 | public final class JsonConverter extends Visitor.Constant { 10 | private final JsonHandler handler; 11 | private final boolean escapeStrings; 12 | 13 | public JsonConverter(JsonHandler handler, boolean escapeStrings) { 14 | super(false); 15 | this.handler = handler; 16 | this.escapeStrings = escapeStrings; 17 | } 18 | 19 | public JsonConverter(JsonHandler handler) { 20 | this(handler, true); 21 | } 22 | 23 | public static final String toCompactString(Expr expr) { 24 | JsonHandler.CompactStringPrinter handler = new JsonHandler.CompactStringPrinter(); 25 | boolean isConverted = expr.accept(new JsonConverter(handler)); 26 | if (isConverted) { 27 | return handler.toString(); 28 | } else { 29 | return null; 30 | } 31 | } 32 | 33 | private static final String escape(String input) { 34 | StringBuilder builder = new StringBuilder(); 35 | 36 | for (int i = 0; i < input.length(); i++) { 37 | char c = input.charAt(i); 38 | 39 | if (c == '\\') { 40 | char next = input.charAt(++i); 41 | 42 | if (next == '"') { 43 | builder.append("\\\""); 44 | } else if (next == '$') { 45 | builder.append("$"); 46 | } else { 47 | builder.append(c); 48 | builder.append(next); 49 | } 50 | } else if (c == '"') { 51 | builder.append("\\\""); 52 | } else { 53 | builder.append(c); 54 | } 55 | } 56 | 57 | return builder.toString(); 58 | } 59 | 60 | @Override 61 | public boolean sortFields() { 62 | return false; 63 | } 64 | 65 | @Override 66 | public boolean flattenToMapLists() { 67 | return true; 68 | } 69 | 70 | @Override 71 | public Boolean onNatural(Expr self, BigInteger value) { 72 | this.handler.onNumber(value); 73 | return true; 74 | } 75 | 76 | @Override 77 | public Boolean onInteger(Expr self, BigInteger value) { 78 | this.handler.onNumber(value); 79 | return true; 80 | } 81 | 82 | @Override 83 | public Boolean onDouble(Expr self, double value) { 84 | this.handler.onDouble(value); 85 | return true; 86 | } 87 | 88 | @Override 89 | public Boolean onBuiltIn(Expr self, String name) { 90 | if (name.equals("True")) { 91 | this.handler.onBoolean(true); 92 | return true; 93 | } else if (name.equals("False")) { 94 | this.handler.onBoolean(false); 95 | return true; 96 | } else { 97 | return false; 98 | } 99 | } 100 | 101 | @Override 102 | public Boolean onText(String[] parts, List interpolated) { 103 | if (parts.length == 1) { 104 | if (this.escapeStrings) { 105 | this.handler.onString(escape(parts[0])); 106 | } else { 107 | this.handler.onString(parts[0]); 108 | } 109 | return true; 110 | } else { 111 | return false; 112 | } 113 | } 114 | 115 | @Override 116 | public boolean prepareNonEmptyList(int size) { 117 | this.handler.onArrayStart(); 118 | return true; 119 | } 120 | 121 | @Override 122 | public boolean prepareNonEmptyListElement(int index) { 123 | if (index > 0) { 124 | this.handler.onArrayElementGap(); 125 | } 126 | return true; 127 | } 128 | 129 | @Override 130 | public Boolean onNonEmptyList(List values) { 131 | for (boolean value : values) { 132 | if (!value) { 133 | return false; 134 | } 135 | } 136 | this.handler.onArrayEnd(); 137 | return true; 138 | } 139 | 140 | @Override 141 | public Boolean onEmptyList(Boolean type) { 142 | this.handler.onArrayStart(); 143 | this.handler.onArrayEnd(); 144 | return true; 145 | } 146 | 147 | @Override 148 | public boolean prepareRecord(int size) { 149 | this.handler.onObjectStart(); 150 | return true; 151 | } 152 | 153 | @Override 154 | public boolean prepareRecordField(String name, Expr type, int index) { 155 | if (index > 0) { 156 | this.handler.onObjectFieldGap(); 157 | } 158 | if (this.escapeStrings) { 159 | this.handler.onObjectField(escape(name)); 160 | } else { 161 | this.handler.onObjectField(name); 162 | } 163 | return true; 164 | } 165 | 166 | @Override 167 | public Boolean onRecord(final List> fields) { 168 | for (Entry field : fields) { 169 | if (!field.getValue()) { 170 | return false; 171 | } 172 | } 173 | this.handler.onObjectEnd(); 174 | return true; 175 | } 176 | 177 | @Override 178 | public boolean prepareFieldAccess(Expr base, String fieldName) { 179 | List> asUnion = Expr.Util.asUnionType(base); 180 | 181 | if (asUnion != null) { 182 | for (Entry field : asUnion) { 183 | if (field.getKey().equals(fieldName) && field.getValue() == null) { 184 | this.handler.onString(escape(fieldName)); 185 | return false; 186 | } 187 | } 188 | } 189 | return true; 190 | } 191 | 192 | @Override 193 | public Boolean onFieldAccess(Boolean base, String fieldName) { 194 | return base == null || base; 195 | } 196 | 197 | @Override 198 | public boolean prepareApplication(Expr base, int size) { 199 | String asBuiltIn = Expr.Util.asBuiltIn(base); 200 | 201 | if (asBuiltIn != null && size == 1) { 202 | if (asBuiltIn.equals("Some")) { 203 | return false; 204 | } else if (asBuiltIn.equals("None")) { 205 | this.handler.onNull(); 206 | return false; 207 | } 208 | } else { 209 | Entry asFieldAccess = Expr.Util.asFieldAccess(base); 210 | 211 | if (asFieldAccess != null) { 212 | List> asUnion = Expr.Util.asUnionType(asFieldAccess.getKey()); 213 | 214 | if (asUnion != null) { 215 | for (Entry field : asUnion) { 216 | if (field.getKey().equals(asFieldAccess.getValue()) && field.getValue() != null) { 217 | return false; 218 | } 219 | } 220 | } 221 | } 222 | } 223 | 224 | return true; 225 | } 226 | 227 | @Override 228 | public Boolean onApplication(Boolean base, List args) { 229 | return base == null || base; 230 | } 231 | } 232 | --------------------------------------------------------------------------------