Compare commits

..

90 Commits

Author SHA1 Message Date
Astrash 5f3a2bb579 Autoformat 2023-10-27 14:26:48 +11:00
Astrash 9a94b26126 Optimize imports 2023-10-27 14:22:35 +11:00
Astrash c3909ca1e0 Make error handler only handle compilation exceptions 2023-10-27 14:21:50 +11:00
Astrash ff031df903 Function -> Callee & Add callee exception 2023-10-27 13:57:53 +11:00
Astrash ceba9512c7 Make functions first class 2023-10-27 13:37:07 +11:00
Astrash 77dbe92ef7 Merge branch 'ver/6.4.0' into dev/terrascript-2 2023-10-23 13:29:57 +11:00
Astrash 1623a4f958 Move terrascript 2 to separate module 2023-10-23 13:28:26 +11:00
Astrash 375f0ba60f Change Type enum to interface
Also removes legacy v1 code from v2
2023-10-23 13:12:35 +11:00
Astrash 433d695e8b Re-add terrascript v1 module 2023-10-23 13:10:32 +11:00
Astrash d139019e01 Validate returns inside if statements 2023-10-20 13:56:52 +11:00
Astrash 805f99f57a Make ore algorithm readable 2023-10-20 10:55:23 +11:00
Astrash 4e5b02ef42 Pass correct coords for ore block checks 2023-10-20 10:30:25 +11:00
Astrash e80e998cec Only log warnings with debug logging 2023-10-20 10:03:04 +11:00
Zoë fde29220af Merge pull request #419 from PolyhedralDev/dev/disable-quilt
Dev/disable quilt
2023-10-19 17:31:40 +00:00
Zoë Gidiere d3a9b57872 disable quilt 2023-10-18 19:53:59 -06:00
Zoë 4671ec5bd3 Merge pull request #417 from PolyhedralDev/dev/folia
Dev/folia
2023-10-19 01:18:45 +00:00
Zoë 9f4a8e06e1 Update DependencyConfig.kt 2023-10-17 21:00:28 -06:00
Zoë c8f2871aaa Update build.gradle.kts 2023-10-17 20:59:27 -06:00
Zoë 4a537a56aa Merge pull request #415 from PolyhedralDev/dev/ore-v2
Add updated ore-v2
2023-10-18 00:33:15 +00:00
Zoë 4917160123 Update README.md 2023-10-17 18:32:00 -06:00
Zoë b3f80dcb64 Merge pull request #410 from PolyhedralDev/dev/1.20.2
1.20.2 + update libs
2023-10-15 19:45:43 +00:00
Zoë Gidiere d49b9ccad5 Merge branch 'dev/1.20.2' into dev/folia 2023-10-14 16:02:54 -06:00
Zoë Gidiere a8387ce419 Merge remote-tracking branch 'origin/ver/6.4.0' into dev/1.20.2 2023-10-14 16:02:40 -06:00
Zoë Gidiere 94854f2bdb update versions 2023-10-14 15:55:43 -06:00
Astrashh 47f531089e Add slant locator (#413)
* Add slant locator addon

* Bump slant locator noise3d dependency version

* Fix slant locator dependency version range

* Actually fix slant locator dependency version range
2023-10-10 00:35:47 +00:00
Astrashh abd83e8278 Add number predicate addon (#412) 2023-10-10 00:35:26 +00:00
Astrash b5e7c7c112 Fix TOP locator 2023-10-10 09:44:53 +11:00
Zoë Gidiere d71f7d4c36 clarify value and change default 2023-10-03 21:03:44 -06:00
Zoë Gidiere 84898a7a6b fixes 2023-10-03 20:03:46 -06:00
Zoë Gidiere 0c1a6efc72 refractor classes 2023-10-03 19:42:15 -06:00
Zoë Gidiere 200281f140 dep orev1 2023-10-03 11:58:32 -06:00
Zoë Gidiere f1ea8074de config-ore-v2 2023-10-03 11:54:18 -06:00
Astrash f896a9a278 Bump noise3d addon minor version 2023-10-03 11:01:50 +11:00
Astrash 79b3b34669 Add slant API for noise3d 2023-10-03 09:51:39 +11:00
Zoë Gidiere ce2b964ce3 Merge branch 'dev/1.20.2' into dev/folia 2023-10-02 01:38:24 -06:00
Zoë Gidiere 0ee5f49972 quilt 2023-10-02 01:38:01 -06:00
Zoë Gidiere 3bc10cdb6a Merge branch 'dev/1.20.2' into dev/folia 2023-10-02 00:42:11 -06:00
Zoë Gidiere 86ba52850d update strata 2023-10-02 00:41:54 -06:00
Zoë Gidiere 27eebf6a47 Folia support
Co-authored-by: Janet Blackquill <uhhadd@gmail.com>
2023-10-02 00:23:19 -06:00
Zoë Gidiere 2d2bba20b6 found an untrue in read me 2023-10-01 23:22:44 -06:00
Zoë Gidiere eb3994005c fix me being fucking stupid 2023-10-01 22:46:41 -06:00
Zoë Gidiere 0a7cdb82a3 update deps + 1.20.2 2023-10-01 22:18:54 -06:00
Zoë 158deaa971 Merge pull request #408 from PolyhedralDev/dev/physics
Allows you to create a block with physics enabled
2023-10-01 19:07:15 -06:00
Astrash e51e025e9c Implement cubic spline sampler 2023-10-02 11:14:43 +11:00
Zoë Gidiere 8e0d64dccd Make bukkit work 2023-09-30 10:49:28 -06:00
Zoë Gidiere 23f47de10a fix pack loading error 2023-09-29 23:35:17 -06:00
Zoë Gidiere 89651597c2 default to info logging 2023-09-29 23:32:40 -06:00
Zoë 5c0c833b70 Update LifecycleEntryPoint.java 2023-09-29 23:09:41 -06:00
Zoë Gidiere 33d654dc8e impl fabric 2023-09-29 23:05:05 -06:00
Zoë Gidiere f0c602d7e7 implement physics on the api side
we will see if platform changes are needed
2023-09-29 22:10:03 -06:00
Zoë Gidiere 0e37a839fe We do a little commonifying 2023-09-29 21:44:12 -06:00
Astrash 3f9ead0d66 Remove repeated code in cellular sampler 2023-09-27 13:39:51 +10:00
David W 5eeb5af6c4 Add cell center offset return to CELLULAR sampler (#407)
* Add offset lookup return to cellular sampler

* bump noise function plugin version

* revert version to 1.1.0

* rename OffsetNoiseLookup, switch axis orientation

* rename return type aswell in cellcampler
2023-09-21 22:23:49 +00:00
Astrash 08c1447967 Remove hardcoded print native java call 2023-09-12 14:22:37 +10:00
Astrash 37b5a2ec92 Use static ints instead of enum 2023-09-12 13:42:36 +10:00
Astrash defb31e309 Mangle bytecode method names according to declaration scope 2023-09-12 11:24:30 +10:00
Astrash 0a46e9050d Simplify some logic 2023-09-12 09:53:18 +10:00
Astrash 002da30fd5 Improve codegen readability 2023-09-11 19:09:08 +10:00
Astrash e177c9e792 Codegen implementation stuff & typed AST nodes 2023-09-11 18:09:35 +10:00
Astrash b1bfe00bf3 Implement function and variable codegen 2023-09-08 11:36:40 +10:00
Astrash 9a75ee78a1 Salvage TerraScript codegen code from dead laptop 2023-08-08 07:39:47 +00:00
Astrash 0e9cbd8e2f Initial terrascript 2 commit 2023-08-05 15:53:01 +10:00
Astrash 772675639e Better error handling + other changes 2023-07-29 20:29:16 +10:00
Astrash 13300861ee Parse precedence via grammar 2023-07-29 12:03:33 +10:00
Astrash 719b9a06f4 Simplify code 2023-07-29 08:57:43 +10:00
Astrash f5b115e618 Formatting & name changes 2023-07-27 12:52:15 +10:00
Astrash e1e4a63517 Add basic user defined function support 2023-07-27 11:27:15 +10:00
Astrash 0dc1492c4d Handle functions in scope 2023-07-25 14:08:09 +10:00
Astrash a184fe40d0 Name changes 2023-07-24 18:52:54 +10:00
Astrash f462b8198b Move inLoop flag to ScopeBuilder 2023-07-24 18:05:43 +10:00
Astrash de3b213deb Refactor some parsing logic 2023-07-24 17:31:06 +10:00
Astrash be444f75b7 Block -> executable 2023-07-24 17:30:37 +10:00
Astrash d98238c262 Remove statement class 2023-07-23 19:33:17 +10:00
Astrash 8e96745a85 checkReturnType -> ensureReturnType 2023-07-23 19:17:50 +10:00
Astrash 802bce40c8 Move statement end handling to parseExpression 2023-07-23 19:17:08 +10:00
Astrash 76728fe593 More refactoring 2023-07-23 17:55:29 +10:00
Astrash f3d1751c87 Terrascript refactor 2023-07-23 16:11:56 +10:00
Astrash 81e354f91c Use tectonic properly 2023-07-18 22:06:08 +10:00
Astrash aab28ff4f9 Bump version to 6.4.0 2023-07-18 14:32:12 +10:00
Astrash 0e3a756011 Bump config-noise-function to v1.1.0 2023-07-18 14:29:54 +10:00
Astrash 02198e1b88 Implement distance sampler 2023-07-18 14:29:28 +10:00
Astrash 00aeb98419 Implement translation sampler 2023-07-18 14:27:36 +10:00
Astrash 1a784b51ac Implement expression normalizer sampler 2023-07-18 14:25:07 +10:00
Astrash 34c0895c1f Make metalist injection error more user friendly 2023-07-16 22:46:23 +10:00
Astrash 379fa601a3 Meta annotate LINEAR_HEIGHTMAP sampler 2023-07-16 17:04:51 +10:00
Astrash fcbf51d80b Allow Range keys to be meta annotated 2023-07-16 11:51:51 +10:00
Astrash 9d83dfd164 Bump version to 6.3.2 2023-07-16 11:49:14 +10:00
Astrashh 72686601ee Merge pull request #406 from PolyhedralDev/ver/6.3.1
Ver/6.3.1
2023-07-15 07:04:10 +10:00
Astrashh 0be7213ee5 Merge pull request #401 from PolyhedralDev/dev/reduce-pipeline-caching
Reduce pipeline v2 caching
2023-06-20 10:10:35 +10:00
Astrash 3f3e2fe97c Reduce pipeline v2 caching 2023-06-20 09:57:43 +10:00
144 changed files with 6332 additions and 606 deletions
+1 -8
View File
@@ -47,14 +47,7 @@ JARs are produced in `platforms/<platform>/build/libs`.
To run Minecraft with Terra in the IDE (for testing) use the following tasks:
* Bukkit
* `installPaper` - Install a [Paper](https://github.com/PaperMC/Paper) test
server. (Only needs to be run once).
* `installPurpur` - Install a [Purpur](https://github.com/pl3xgaming/Purpur)
test server. (Only needs to be run once).
* `runPaper` - Run the Paper test server with Terra (`installPaper` must
have been run previously).
* `runPurpur` - Run the Purpur test server with Terra (`installPurpur` must
have been run previously).
* `runServer` - Run the Paper test server with Terra installed.
* Fabric
* `runClient` - Run a Minecraft Fabric client with Terra installed.
* `runServer` - Run a Minecraft Fabric server with Terra installed.
+3 -3
View File
@@ -1,8 +1,8 @@
preRelease(true)
versionProjects(":common:api", version("6.3.1"))
versionProjects(":common:implementation", version("6.3.1"))
versionProjects(":platforms", version("6.3.1"))
versionProjects(":common:api", version("6.4.0"))
versionProjects(":common:implementation", version("6.4.0"))
versionProjects(":platforms", version("6.4.0"))
allprojects {
+7 -15
View File
@@ -3,15 +3,6 @@ plugins {
kotlin("jvm") version embeddedKotlinVersion
}
buildscript {
configurations.all {
resolutionStrategy {
force("org.ow2.asm:asm:9.3") // TODO: remove when ShadowJar updates ASM version
force("org.ow2.asm:asm-commons:9.3")
}
}
}
repositories {
mavenCentral()
gradlePluginPortal()
@@ -24,11 +15,12 @@ repositories {
}
dependencies {
implementation("gradle.plugin.com.github.jengelman.gradle.plugins:shadow:+")
implementation("io.papermc.paperweight.userdev:io.papermc.paperweight.userdev.gradle.plugin:1.3.5")
//TODO Allow pulling from Versions.kt
implementation("com.github.johnrengelman", "shadow", "8.1.1")
implementation("io.papermc.paperweight.userdev", "io.papermc.paperweight.userdev.gradle.plugin","1.5.6")
implementation("org.ow2.asm:asm:9.3")
implementation("org.ow2.asm:asm-tree:9.3")
implementation("com.dfsek.tectonic:common:4.2.0")
implementation("org.yaml:snakeyaml:1.27")
implementation("org.ow2.asm", "asm", "9.5")
implementation("org.ow2.asm", "asm-tree", "9.5")
implementation("com.dfsek.tectonic", "common", "4.2.0")
implementation("org.yaml", "snakeyaml", "2.2")
}
+4 -1
View File
@@ -48,6 +48,9 @@ fun Project.configureDependencies() {
maven("https://jitpack.io") {
name = "JitPack"
}
maven("https://nexuslite.gcnt.net/repos/other/") {
name = "GCNT"
}
}
dependencies {
@@ -58,4 +61,4 @@ fun Project.configureDependencies() {
compileOnly("com.google.guava:guava:30.0-jre")
testImplementation("com.google.guava:guava:30.0-jre")
}
}
}
+40 -37
View File
@@ -1,68 +1,71 @@
object Versions {
object Libraries {
const val tectonic = "4.2.0"
const val paralithic = "0.7.0"
const val strata = "1.1.1"
const val tectonic = "4.2.1"
const val paralithic = "0.7.1"
const val strata = "1.3.2"
const val cloud = "1.8.0"
const val cloud = "1.8.4"
const val slf4j = "1.7.36"
const val log4j_slf4j_impl = "2.14.1"
const val slf4j = "2.0.9"
const val log4j_slf4j_impl = "2.20.0"
object Internal {
const val apacheText = "1.9"
const val shadow = "8.1.1"
const val apacheText = "1.10.0"
const val jafama = "2.3.2"
const val apacheIO = "2.6"
const val fastutil = "8.5.6"
const val apacheIO = "2.14.0"
const val guava = "32.1.3-jre"
const val asm = "9.5"
const val snakeYml = "2.2"
}
}
object Fabric {
const val fabricLoader = "0.14.8"
const val fabricAPI = "0.83.1+1.20.1"
}
object Quilt {
const val quiltLoader = "0.17.0"
const val fabricApi = "6.0.0-beta.3+0.76.0-1.19.4"
const val fabricAPI = "0.90.0+${Mod.minecraft}"
}
//
// object Quilt {
// const val quiltLoader = "0.20.2"
// const val fabricApi = "7.3.1+0.89.3-1.20.1"
// }
object Mod {
const val mixin = "0.11.2+mixin.0.8.5"
const val mixin = "0.12.5+mixin.0.8.5"
const val minecraft = "1.20.1"
const val yarn = "$minecraft+build.2"
const val fabricLoader = "0.14.21"
const val minecraft = "1.20.2"
const val yarn = "$minecraft+build.4"
const val fabricLoader = "0.14.23"
const val architecuryLoom = "0.12.0.290"
const val architecturyPlugin = "3.4-SNAPSHOT"
const val architecuryLoom = "1.3.357"
const val architecturyPlugin = "3.4.146"
const val loomQuiltflower = "1.7.1"
const val loomVineflower = "1.11.0"
}
object Forge {
const val forge = "${Mod.minecraft}-47.0.3"
const val burningwave = "12.53.0"
const val forge = "${Mod.minecraft}-48.0.13"
const val burningwave = "12.63.0"
}
object Bukkit {
const val paper = "1.18.2-R0.1-SNAPSHOT"
const val paperLib = "1.0.5"
const val minecraft = "1.20.1"
const val foliaLib = "0.2.5"
const val minecraft = "1.20.2"
const val reflectionRemapper = "0.1.0-SNAPSHOT"
const val paperDevBundle = "1.20.1-R0.1-SNAPSHOT"
const val paperDevBundle = "1.20.2-R0.1-SNAPSHOT"
const val runPaper = "2.2.0"
const val paperWeight = "1.5.6"
}
object Sponge {
const val sponge = "9.0.0-SNAPSHOT"
const val mixin = "0.8.2"
const val minecraft = "1.17.1"
}
//
// object Sponge {
// const val sponge = "9.0.0-SNAPSHOT"
// const val mixin = "0.8.2"
// const val minecraft = "1.17.1"
// }
//
object CLI {
const val nbt = "6.1"
const val logback = "1.2.9"
const val commonsIO = "2.7"
const val guava = "31.0.1-jre"
const val logback = "1.4.11"
}
}
@@ -50,6 +50,7 @@ public class ImageBiomeProviderAddon implements AddonInitializer {
() -> new ImageProviderTemplate(event.getPack().getRegistry(Biome.class)));
})
.failThrough();
logger.warn("The biome-provider-image addon is deprecated and scheduled for removal in Terra 7.0. It is recommended to use the biome-provider-image-v2 addon for future pack development instead.");
if(platform.getTerraConfig().isDebugLog())
logger.warn("The biome-provider-image addon is deprecated and scheduled for removal in Terra 7.0. It is recommended to use the biome-provider-image-v2 addon for future pack development instead.");
}
}
@@ -37,7 +37,7 @@ public class PipelineBiomeProvider implements BiomeProvider {
this.noiseAmp = noiseAmp;
this.chunkSize = pipeline.getChunkSize();
this.biomeChunkCache = Caffeine.newBuilder()
.maximumSize(1024)
.maximumSize(64)
.build(pipeline::generateChunk);
Set<PipelineBiome> biomeSet = new HashSet<>();
@@ -54,6 +54,6 @@ public class BiomePipelineTemplate implements ObjectTemplate<BiomeProvider> {
@Override
public BiomeProvider get() {
return new PipelineBiomeProvider(new PipelineImpl(source, stages, resolution, 500), resolution, blendSampler, blendAmplitude);
return new PipelineBiomeProvider(new PipelineImpl(source, stages, resolution, 128), resolution, blendSampler, blendAmplitude);
}
}
@@ -90,6 +90,7 @@ public class BiomePipelineAddon implements AddonInitializer {
event.getPack().applyLoader(BiomeDelegate.class, new BiomeDelegateLoader(biomeRegistry));
});
logger.warn("The biome-provider-pipeline addon is deprecated and scheduled for removal in Terra 7.0. It is recommended to use the biome-provider-pipeline-v2 addon for future pack development instead.");
if(platform.getTerraConfig().isDebugLog())
logger.warn("The biome-provider-pipeline addon is deprecated and scheduled for removal in Terra 7.0. It is recommended to use the biome-provider-pipeline-v2 addon for future pack development instead.");
}
}
@@ -1,4 +1,4 @@
version = version("1.1.0")
version = version("1.2.0")
dependencies {
compileOnlyApi(project(":common:addons:manifest-addon-loader"))
@@ -155,6 +155,16 @@ public class NoiseChunkGenerator3D implements ChunkGenerator {
return biomeProvider.getBiome(x, y, z, world.getSeed()).getContext().get(paletteInfoPropertyKey).paletteHolder().getPalette(y);
}
public double getSlant(int x, int y, int z, WorldProperties world, BiomeProvider biomeProvider) {
int fdX = FastMath.floorMod(x, 16);
int fdZ = FastMath.floorMod(z, 16);
return biomeProvider.getBiome(x, y, z, world.getSeed())
.getContext()
.get(paletteInfoPropertyKey)
.slantHolder()
.calculateSlant(samplerCache.get(x, z, world, biomeProvider), fdX, y, fdZ);
}
public SamplerProvider samplerProvider() {
return samplerCache;
}
@@ -22,7 +22,7 @@ public class TopLocator implements Locator {
@Override
public BinaryColumn getSuitableCoordinates(Column<?> column) {
for(int y : search) {
for(int y = search.getMax(); y >= search.getMin(); y--) {
if(column.getBlock(y).isAir() && !column.getBlock(y - 1).isAir()) {
return new BinaryColumn(y, y + 1, yi -> true);
}
@@ -1,6 +1,6 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
version = version("1.0.0")
version = version("1.1.0")
dependencies {
compileOnlyApi(project(":common:addons:manifest-addon-loader"))
@@ -14,6 +14,7 @@ import java.util.Map;
import java.util.function.Supplier;
import com.dfsek.terra.addons.manifest.api.AddonInitializer;
import com.dfsek.terra.addons.noise.config.CubicSplinePointTemplate;
import com.dfsek.terra.addons.noise.config.DimensionApplicableNoiseSampler;
import com.dfsek.terra.addons.noise.config.templates.BinaryArithmeticTemplate;
import com.dfsek.terra.addons.noise.config.templates.DomainWarpTemplate;
@@ -21,8 +22,10 @@ import com.dfsek.terra.addons.noise.config.templates.FunctionTemplate;
import com.dfsek.terra.addons.noise.config.templates.ImageSamplerTemplate;
import com.dfsek.terra.addons.noise.config.templates.KernelTemplate;
import com.dfsek.terra.addons.noise.config.templates.LinearHeightmapSamplerTemplate;
import com.dfsek.terra.addons.noise.config.templates.TranslateSamplerTemplate;
import com.dfsek.terra.addons.noise.config.templates.noise.CellularNoiseTemplate;
import com.dfsek.terra.addons.noise.config.templates.noise.ConstantNoiseTemplate;
import com.dfsek.terra.addons.noise.config.templates.noise.DistanceSamplerTemplate;
import com.dfsek.terra.addons.noise.config.templates.noise.ExpressionFunctionTemplate;
import com.dfsek.terra.addons.noise.config.templates.noise.GaborNoiseTemplate;
import com.dfsek.terra.addons.noise.config.templates.noise.SimpleNoiseTemplate;
@@ -30,11 +33,14 @@ import com.dfsek.terra.addons.noise.config.templates.noise.fractal.BrownianMotio
import com.dfsek.terra.addons.noise.config.templates.noise.fractal.PingPongTemplate;
import com.dfsek.terra.addons.noise.config.templates.noise.fractal.RidgedFractalTemplate;
import com.dfsek.terra.addons.noise.config.templates.normalizer.ClampNormalizerTemplate;
import com.dfsek.terra.addons.noise.config.templates.normalizer.CubicSplineNormalizerTemplate;
import com.dfsek.terra.addons.noise.config.templates.normalizer.ExpressionNormalizerTemplate;
import com.dfsek.terra.addons.noise.config.templates.normalizer.LinearNormalizerTemplate;
import com.dfsek.terra.addons.noise.config.templates.normalizer.NormalNormalizerTemplate;
import com.dfsek.terra.addons.noise.config.templates.normalizer.PosterizationNormalizerTemplate;
import com.dfsek.terra.addons.noise.config.templates.normalizer.ProbabilityNormalizerTemplate;
import com.dfsek.terra.addons.noise.config.templates.normalizer.ScaleNormalizerTemplate;
import com.dfsek.terra.addons.noise.math.CubicSpline;
import com.dfsek.terra.addons.noise.samplers.arithmetic.AdditionSampler;
import com.dfsek.terra.addons.noise.samplers.arithmetic.DivisionSampler;
import com.dfsek.terra.addons.noise.samplers.arithmetic.MaxSampler;
@@ -42,6 +48,7 @@ import com.dfsek.terra.addons.noise.samplers.arithmetic.MinSampler;
import com.dfsek.terra.addons.noise.samplers.arithmetic.MultiplicationSampler;
import com.dfsek.terra.addons.noise.samplers.arithmetic.SubtractionSampler;
import com.dfsek.terra.addons.noise.samplers.noise.CellularSampler;
import com.dfsek.terra.addons.noise.samplers.noise.DistanceSampler;
import com.dfsek.terra.addons.noise.samplers.noise.random.GaussianNoiseSampler;
import com.dfsek.terra.addons.noise.samplers.noise.random.PositiveWhiteNoiseSampler;
import com.dfsek.terra.addons.noise.samplers.noise.random.WhiteNoiseSampler;
@@ -83,8 +90,11 @@ public class NoiseAddon implements AddonInitializer {
(type, o, loader, depthTracker) -> CellularSampler.DistanceFunction.valueOf((String) o))
.applyLoader(CellularSampler.ReturnType.class,
(type, o, loader, depthTracker) -> CellularSampler.ReturnType.valueOf((String) o))
.applyLoader(DistanceSampler.DistanceFunction.class,
(type, o, loader, depthTracker) -> DistanceSampler.DistanceFunction.valueOf((String) o))
.applyLoader(DimensionApplicableNoiseSampler.class, DimensionApplicableNoiseSampler::new)
.applyLoader(FunctionTemplate.class, FunctionTemplate::new);
.applyLoader(FunctionTemplate.class, FunctionTemplate::new)
.applyLoader(CubicSpline.Point.class, CubicSplinePointTemplate::new);
noiseRegistry.register(addon.key("LINEAR"), LinearNormalizerTemplate::new);
noiseRegistry.register(addon.key("NORMAL"), NormalNormalizerTemplate::new);
@@ -92,6 +102,7 @@ public class NoiseAddon implements AddonInitializer {
noiseRegistry.register(addon.key("PROBABILITY"), ProbabilityNormalizerTemplate::new);
noiseRegistry.register(addon.key("SCALE"), ScaleNormalizerTemplate::new);
noiseRegistry.register(addon.key("POSTERIZATION"), PosterizationNormalizerTemplate::new);
noiseRegistry.register(addon.key("CUBIC_SPLINE"), CubicSplineNormalizerTemplate::new);
noiseRegistry.register(addon.key("IMAGE"), ImageSamplerTemplate::new);
@@ -116,12 +127,15 @@ public class NoiseAddon implements AddonInitializer {
noiseRegistry.register(addon.key("WHITE_NOISE"), () -> new SimpleNoiseTemplate(WhiteNoiseSampler::new));
noiseRegistry.register(addon.key("POSITIVE_WHITE_NOISE"), () -> new SimpleNoiseTemplate(PositiveWhiteNoiseSampler::new));
noiseRegistry.register(addon.key("GAUSSIAN"), () -> new SimpleNoiseTemplate(GaussianNoiseSampler::new));
noiseRegistry.register(addon.key("DISTANCE"), DistanceSamplerTemplate::new);
noiseRegistry.register(addon.key("CONSTANT"), ConstantNoiseTemplate::new);
noiseRegistry.register(addon.key("KERNEL"), KernelTemplate::new);
noiseRegistry.register(addon.key("LINEAR_HEIGHTMAP"), LinearHeightmapSamplerTemplate::new);
noiseRegistry.register(addon.key("TRANSLATE"), TranslateSamplerTemplate::new);
noiseRegistry.register(addon.key("ADD"), () -> new BinaryArithmeticTemplate<>(AdditionSampler::new));
noiseRegistry.register(addon.key("SUB"), () -> new BinaryArithmeticTemplate<>(SubtractionSampler::new));
@@ -134,7 +148,7 @@ public class NoiseAddon implements AddonInitializer {
Map<String, DimensionApplicableNoiseSampler> packSamplers = new LinkedHashMap<>();
Map<String, FunctionTemplate> packFunctions = new LinkedHashMap<>();
noiseRegistry.register(addon.key("EXPRESSION"), () -> new ExpressionFunctionTemplate(packSamplers, packFunctions));
noiseRegistry.register(addon.key("EXPRESSION_NORMALIZER"), () -> new ExpressionNormalizerTemplate(packSamplers, packFunctions));
NoiseConfigPackTemplate template = event.loadTemplate(new NoiseConfigPackTemplate());
packSamplers.putAll(template.getSamplers());
@@ -0,0 +1,25 @@
package com.dfsek.terra.addons.noise.config;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import com.dfsek.terra.addons.noise.math.CubicSpline.Point;
import com.dfsek.terra.api.config.meta.Meta;
public class CubicSplinePointTemplate implements ObjectTemplate<Point> {
@Value("from")
private @Meta double from;
@Value("to")
private @Meta double to;
@Value("gradient")
private @Meta double gradient;
@Override
public Point get() {
return new Point(from, to, gradient);
}
}
@@ -4,6 +4,7 @@ import com.dfsek.tectonic.api.config.template.annotations.Default;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.terra.addons.noise.samplers.LinearHeightmapSampler;
import com.dfsek.terra.api.config.meta.Meta;
import com.dfsek.terra.api.noise.NoiseSampler;
@@ -11,14 +12,14 @@ import com.dfsek.terra.api.noise.NoiseSampler;
public class LinearHeightmapSamplerTemplate extends SamplerTemplate<LinearHeightmapSampler> {
@Value("sampler")
@Default
private NoiseSampler sampler = NoiseSampler.zero();
private @Meta NoiseSampler sampler = NoiseSampler.zero();
@Value("base")
private double base;
private @Meta double base;
@Value("scale")
@Default
private double scale = 1;
private @Meta double scale = 1;
@Override
public NoiseSampler get() {
@@ -0,0 +1,32 @@
package com.dfsek.terra.addons.noise.config.templates;
import com.dfsek.tectonic.api.config.template.annotations.Default;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.terra.addons.noise.samplers.TranslateSampler;
import com.dfsek.terra.api.config.meta.Meta;
import com.dfsek.terra.api.noise.NoiseSampler;
public class TranslateSamplerTemplate extends SamplerTemplate<TranslateSampler> {
@Value("sampler")
private NoiseSampler sampler;
@Value("x")
@Default
private @Meta double x = 0;
@Value("y")
@Default
private @Meta double y = 0;
@Value("z")
@Default
private @Meta double z = 0;
@Override
public NoiseSampler get() {
return new TranslateSampler(sampler, x, y ,z);
}
}
@@ -0,0 +1,42 @@
package com.dfsek.terra.addons.noise.config.templates.noise;
import com.dfsek.tectonic.api.config.template.annotations.Default;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.terra.addons.noise.config.templates.SamplerTemplate;
import com.dfsek.terra.addons.noise.samplers.noise.DistanceSampler;
import com.dfsek.terra.addons.noise.samplers.noise.DistanceSampler.DistanceFunction;
import com.dfsek.terra.api.config.meta.Meta;
public class DistanceSamplerTemplate extends SamplerTemplate<DistanceSampler> {
@Value("distance-function")
@Default
private DistanceSampler.@Meta DistanceFunction distanceFunction = DistanceFunction.Euclidean;
@Value("point.x")
@Default
private @Meta double x = 0;
@Value("point.y")
@Default
private @Meta double y = 0;
@Value("point.z")
@Default
private @Meta double z = 0;
@Value("normalize")
@Default
private @Meta boolean normalize = false;
@Value("radius")
@Default
private @Meta double normalizeRadius = 100;
@Override
public DistanceSampler get() {
return new DistanceSampler(distanceFunction, x, y, z, normalize, normalizeRadius);
}
}
@@ -8,7 +8,6 @@
package com.dfsek.terra.addons.noise.config.templates.noise;
import com.dfsek.paralithic.eval.tokenizer.ParseException;
import com.dfsek.paralithic.functions.Function;
import com.dfsek.tectonic.api.config.template.annotations.Default;
import com.dfsek.tectonic.api.config.template.annotations.Value;
@@ -19,17 +18,16 @@ import java.util.Map;
import com.dfsek.terra.addons.noise.config.DimensionApplicableNoiseSampler;
import com.dfsek.terra.addons.noise.config.templates.FunctionTemplate;
import com.dfsek.terra.addons.noise.config.templates.SamplerTemplate;
import com.dfsek.terra.addons.noise.paralithic.defined.UserDefinedFunction;
import com.dfsek.terra.addons.noise.paralithic.noise.NoiseFunction2;
import com.dfsek.terra.addons.noise.paralithic.noise.NoiseFunction3;
import com.dfsek.terra.addons.noise.samplers.noise.ExpressionFunction;
import com.dfsek.terra.api.config.meta.Meta;
import com.dfsek.terra.api.noise.NoiseSampler;
import static com.dfsek.terra.addons.noise.paralithic.FunctionUtil.convertFunctionsAndSamplers;
@SuppressWarnings({ "FieldMayBeFinal", "unused" })
public class ExpressionFunctionTemplate extends SamplerTemplate<ExpressionFunction> {
private final Map<String, DimensionApplicableNoiseSampler> otherFunctions;
private final Map<String, DimensionApplicableNoiseSampler> globalSamplers;
private final Map<String, FunctionTemplate> globalFunctions;
@Value("variables")
@Default
@@ -43,44 +41,19 @@ public class ExpressionFunctionTemplate extends SamplerTemplate<ExpressionFuncti
@Default
private @Meta LinkedHashMap<String, @Meta FunctionTemplate> functions = new LinkedHashMap<>();
public ExpressionFunctionTemplate(Map<String, DimensionApplicableNoiseSampler> otherFunctions, Map<String, FunctionTemplate> samplers) {
this.otherFunctions = otherFunctions;
this.globalFunctions = samplers;
public ExpressionFunctionTemplate(Map<String, DimensionApplicableNoiseSampler> globalSamplers, Map<String, FunctionTemplate> globalFunctions) {
this.globalSamplers = globalSamplers;
this.globalFunctions = globalFunctions;
}
@Override
public NoiseSampler get() {
var mergedFunctions = new HashMap<>(globalFunctions); mergedFunctions.putAll(functions);
var mergedSamplers = new HashMap<>(globalSamplers); mergedSamplers.putAll(samplers);
try {
Map<String, Function> noiseFunctionMap = generateFunctions();
return new ExpressionFunction(noiseFunctionMap, expression, vars);
return new ExpressionFunction(convertFunctionsAndSamplers(mergedFunctions, mergedSamplers), expression, vars);
} catch(ParseException e) {
throw new RuntimeException("Failed to parse expression.", e);
}
}
private Map<String, Function> generateFunctions() throws ParseException {
Map<String, Function> noiseFunctionMap = new HashMap<>();
for(Map.Entry<String, FunctionTemplate> entry : globalFunctions.entrySet()) {
noiseFunctionMap.put(entry.getKey(), UserDefinedFunction.newInstance(entry.getValue()));
}
for(Map.Entry<String, FunctionTemplate> entry : functions.entrySet()) {
noiseFunctionMap.put(entry.getKey(), UserDefinedFunction.newInstance(entry.getValue()));
}
otherFunctions.forEach((id, function) -> {
if(function.getDimensions() == 2) {
noiseFunctionMap.put(id, new NoiseFunction2(function.getSampler()));
} else noiseFunctionMap.put(id, new NoiseFunction3(function.getSampler()));
});
samplers.forEach((id, function) -> {
if(function.getDimensions() == 2) {
noiseFunctionMap.put(id, new NoiseFunction2(function.getSampler()));
} else noiseFunctionMap.put(id, new NoiseFunction3(function.getSampler()));
});
return noiseFunctionMap;
}
}
@@ -0,0 +1,23 @@
package com.dfsek.terra.addons.noise.config.templates.normalizer;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.terra.addons.noise.math.CubicSpline;
import com.dfsek.terra.addons.noise.math.CubicSpline.Point;
import com.dfsek.terra.addons.noise.normalizer.CubicSplineNoiseSampler;
import com.dfsek.terra.api.config.meta.Meta;
import com.dfsek.terra.api.noise.NoiseSampler;
import java.util.List;
public class CubicSplineNormalizerTemplate extends NormalizerTemplate<CubicSplineNoiseSampler> {
@Value("points")
private @Meta List<@Meta Point> points;
@Override
public NoiseSampler get() {
return new CubicSplineNoiseSampler(function, new CubicSpline(points));
}
}
@@ -0,0 +1,63 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.noise.config.templates.normalizer;
import com.dfsek.paralithic.eval.tokenizer.ParseException;
import com.dfsek.tectonic.api.config.template.annotations.Default;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.terra.addons.noise.config.DimensionApplicableNoiseSampler;
import com.dfsek.terra.addons.noise.config.templates.FunctionTemplate;
import com.dfsek.terra.addons.noise.normalizer.ExpressionNormalizer;
import com.dfsek.terra.api.config.meta.Meta;
import com.dfsek.terra.api.noise.NoiseSampler;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import static com.dfsek.terra.addons.noise.paralithic.FunctionUtil.convertFunctionsAndSamplers;
@SuppressWarnings({ "unused", "FieldMayBeFinal" })
public class ExpressionNormalizerTemplate extends NormalizerTemplate<ExpressionNormalizer> {
private final Map<String, DimensionApplicableNoiseSampler> globalSamplers;
private final Map<String, FunctionTemplate> globalFunctions;
@Value("expression")
private @Meta String expression;
@Value("variables")
@Default
private @Meta Map<String, @Meta Double> vars = new HashMap<>();
@Value("samplers")
@Default
private @Meta LinkedHashMap<String, @Meta DimensionApplicableNoiseSampler> samplers = new LinkedHashMap<>();
@Value("functions")
@Default
private @Meta LinkedHashMap<String, @Meta FunctionTemplate> functions = new LinkedHashMap<>();
public ExpressionNormalizerTemplate(Map<String, DimensionApplicableNoiseSampler> globalSamplers, Map<String, FunctionTemplate> globalFunctions) {
this.globalSamplers = globalSamplers;
this.globalFunctions = globalFunctions;
}
@Override
public NoiseSampler get() {
var mergedFunctions = new HashMap<>(globalFunctions); mergedFunctions.putAll(functions);
var mergedSamplers = new HashMap<>(globalSamplers); mergedSamplers.putAll(samplers);
try {
return new ExpressionNormalizer(function, convertFunctionsAndSamplers(mergedFunctions, mergedSamplers), expression, vars);
} catch(ParseException e) {
throw new RuntimeException("Failed to parse expression.", e);
}
}
}
@@ -0,0 +1,89 @@
package com.dfsek.terra.addons.noise.math;
import org.jetbrains.annotations.NotNull;
import java.util.Collections;
import java.util.List;
public class CubicSpline {
private final double[] fromValues;
private final double[] toValues;
private final double[] gradients;
public CubicSpline(List<Point> points) {
Collections.sort(points);
this.fromValues = new double[points.size()];
this.toValues = new double[points.size()];
this.gradients = new double[points.size()];
for(int i = 0; i < points.size(); i++) {
fromValues[i] = points.get(i).from;
toValues[i] = points.get(i).to;
gradients[i] = points.get(i).gradient;
}
}
public double apply(double in) {
return calculate(in, fromValues, toValues, gradients);
}
public static double calculate(double in, double[] fromValues, double[] toValues, double[] gradients) {
int pointIdx = floorBinarySearch(in, fromValues) - 1;
int pointIdxLast = fromValues.length - 1;
if (pointIdx < 0) { // If to left of first point return linear function intersecting said point using point's gradient
return gradients[0] * (in - fromValues[0]) + toValues[0];
} else if (pointIdx == pointIdxLast) { // Do same if to right of last point
return gradients[pointIdxLast] * (in - fromValues[pointIdxLast]) + toValues[pointIdxLast];
} else {
double fromLeft = fromValues[pointIdx];
double fromRight = fromValues[pointIdx + 1];
double toLeft = toValues[pointIdx];
double toRight = toValues[pointIdx + 1];
double gradientLeft = gradients[pointIdx];
double gradientRight = gradients[pointIdx + 1];
double fromDelta = fromRight - fromLeft;
double toDelta = toRight - toLeft;
double t = (in - fromLeft) / fromDelta;
return lerp(t, toLeft, toRight) + t * (1.0F - t) * lerp(t, gradientLeft * fromDelta - toDelta, -gradientRight * fromDelta + toDelta);
}
}
private static int floorBinarySearch(double targetValue, double[] values) {
int left = 0;
int right = values.length;
int idx = right - left;
while (idx > 0) {
int halfDelta = idx / 2;
int mid = left + halfDelta;
if (targetValue < values[mid]) {
idx = halfDelta;
} else {
left = mid + 1;
idx -= halfDelta + 1;
}
}
return left;
}
private static double lerp(double t, double a, double b) {
return a + t * (b - a);
}
public record Point(double from, double to, double gradient) implements Comparable<Point> {
@Override
public int compareTo(@NotNull CubicSpline.Point o) {
return Double.compare(from, o.from);
}
}
}
@@ -0,0 +1,20 @@
package com.dfsek.terra.addons.noise.normalizer;
import com.dfsek.terra.addons.noise.math.CubicSpline;
import com.dfsek.terra.api.noise.NoiseSampler;
public class CubicSplineNoiseSampler extends Normalizer {
private final CubicSpline spline;
public CubicSplineNoiseSampler(NoiseSampler sampler, CubicSpline spline) {
super(sampler);
this.spline = spline;
}
@Override
public double normalize(double in) {
return spline.apply(in);
}
}
@@ -0,0 +1,33 @@
package com.dfsek.terra.addons.noise.normalizer;
import com.dfsek.paralithic.Expression;
import com.dfsek.paralithic.eval.parser.Parser;
import com.dfsek.paralithic.eval.parser.Scope;
import com.dfsek.paralithic.eval.tokenizer.ParseException;
import com.dfsek.paralithic.functions.Function;
import com.dfsek.terra.api.noise.NoiseSampler;
import java.util.Map;
public class ExpressionNormalizer extends Normalizer {
private final Expression expression;
public ExpressionNormalizer(NoiseSampler sampler, Map<String, Function> functions, String eq, Map<String, Double> vars)
throws ParseException {
super(sampler);
Parser p = new Parser();
Scope scope = new Scope();
scope.addInvocationVariable("in");
vars.forEach(scope::create);
functions.forEach(p::registerFunction);
expression = p.parse(eq, scope);
}
@Override
public double normalize(double in) {
return expression.evaluate(in);
}
}
@@ -0,0 +1,31 @@
package com.dfsek.terra.addons.noise.paralithic;
import com.dfsek.paralithic.eval.tokenizer.ParseException;
import com.dfsek.paralithic.functions.Function;
import com.dfsek.terra.addons.noise.config.DimensionApplicableNoiseSampler;
import com.dfsek.terra.addons.noise.config.templates.FunctionTemplate;
import com.dfsek.terra.addons.noise.paralithic.defined.UserDefinedFunction;
import com.dfsek.terra.addons.noise.paralithic.noise.NoiseFunction2;
import com.dfsek.terra.addons.noise.paralithic.noise.NoiseFunction3;
import java.util.HashMap;
import java.util.Map;
public class FunctionUtil {
private FunctionUtil() {}
public static Map<String, Function> convertFunctionsAndSamplers(Map<String, FunctionTemplate> functions,
Map<String, DimensionApplicableNoiseSampler> samplers) throws ParseException {
Map<String, Function> functionMap = new HashMap<>();
for(Map.Entry<String, FunctionTemplate> entry : functions.entrySet()) {
functionMap.put(entry.getKey(), UserDefinedFunction.newInstance(entry.getValue()));
}
samplers.forEach((id, sampler) -> functionMap.put(id,
sampler.getDimensions() == 2 ?
new NoiseFunction2(sampler.getSampler()) :
new NoiseFunction3(sampler.getSampler())));
return functionMap;
}
}
@@ -0,0 +1,27 @@
package com.dfsek.terra.addons.noise.samplers;
import com.dfsek.terra.api.noise.NoiseSampler;
public class TranslateSampler implements NoiseSampler {
private final NoiseSampler sampler;
private final double dx, dy, dz;
public TranslateSampler(NoiseSampler sampler, double dx, double dy, double dz) {
this.sampler = sampler;
this.dx = dx;
this.dy = dy;
this.dz = dz;
}
@Override
public double noise(long seed, double x, double y) {
return sampler.noise(seed, x - dx, y - dz);
}
@Override
public double noise(long seed, double x, double y, double z) {
return sampler.noise(seed, x - dx, y - dy, z - dz);
}
}
@@ -240,99 +240,37 @@ public class CellularSampler extends NoiseFunction {
double centerX = x;
double centerY = y;
switch(distanceFunction) {
default:
case Euclidean:
case EuclideanSq:
for(int xi = xr - 1; xi <= xr + 1; xi++) {
int yPrimed = yPrimedBase;
for(int yi = yr - 1; yi <= yr + 1; yi++) {
int hash = hash(seed, xPrimed, yPrimed);
int idx = hash & (255 << 1);
double vecX = (xi - x) + RAND_VECS_2D[idx] * cellularJitter;
double vecY = (yi - y) + RAND_VECS_2D[idx | 1] * cellularJitter;
double newDistance = vecX * vecX + vecY * vecY;
distance1 = fastMax(fastMin(distance1, newDistance), distance0);
if(newDistance < distance0) {
distance0 = newDistance;
closestHash = hash;
centerX = ((xi + RAND_VECS_2D[idx] * cellularJitter) / frequency);
centerY = ((yi + RAND_VECS_2D[idx | 1] * cellularJitter) / frequency);
} else if(newDistance < distance1) {
distance2 = distance1;
distance1 = newDistance;
} else if(newDistance < distance2) {
distance2 = newDistance;
}
yPrimed += PRIME_Y;
}
xPrimed += PRIME_X;
for(int xi = xr - 1; xi <= xr + 1; xi++) {
int yPrimed = yPrimedBase;
for(int yi = yr - 1; yi <= yr + 1; yi++) {
int hash = hash(seed, xPrimed, yPrimed);
int idx = hash & (255 << 1);
double vecX = (xi - x) + RAND_VECS_2D[idx] * cellularJitter;
double vecY = (yi - y) + RAND_VECS_2D[idx | 1] * cellularJitter;
double newDistance = switch(distanceFunction) {
case Manhattan -> fastAbs(vecX) + fastAbs(vecY);
case Hybrid -> (fastAbs(vecX) + fastAbs(vecY)) + (vecX * vecX + vecY * vecY);
default -> vecX * vecX + vecY * vecY;
};
distance1 = fastMax(fastMin(distance1, newDistance), distance0);
if(newDistance < distance0) {
distance0 = newDistance;
closestHash = hash;
centerX = ((xi + RAND_VECS_2D[idx] * cellularJitter) / frequency);
centerY = ((yi + RAND_VECS_2D[idx | 1] * cellularJitter) / frequency);
} else if(newDistance < distance1) {
distance2 = distance1;
distance1 = newDistance;
} else if(newDistance < distance2) {
distance2 = newDistance;
}
break;
case Manhattan:
for(int xi = xr - 1; xi <= xr + 1; xi++) {
int yPrimed = yPrimedBase;
for(int yi = yr - 1; yi <= yr + 1; yi++) {
int hash = hash(seed, xPrimed, yPrimed);
int idx = hash & (255 << 1);
double vecX = (xi - x) + RAND_VECS_2D[idx] * cellularJitter;
double vecY = (yi - y) + RAND_VECS_2D[idx | 1] * cellularJitter;
double newDistance = fastAbs(vecX) + fastAbs(vecY);
distance1 = fastMax(fastMin(distance1, newDistance), distance0);
if(newDistance < distance0) {
distance0 = newDistance;
closestHash = hash;
centerX = ((xi + RAND_VECS_2D[idx] * cellularJitter) / frequency);
centerY = ((yi + RAND_VECS_2D[idx | 1] * cellularJitter) / frequency);
} else if(newDistance < distance1) {
distance2 = distance1;
distance1 = newDistance;
} else if(newDistance < distance2) {
distance2 = newDistance;
}
yPrimed += PRIME_Y;
}
xPrimed += PRIME_X;
}
break;
case Hybrid:
for(int xi = xr - 1; xi <= xr + 1; xi++) {
int yPrimed = yPrimedBase;
for(int yi = yr - 1; yi <= yr + 1; yi++) {
int hash = hash(seed, xPrimed, yPrimed);
int idx = hash & (255 << 1);
double vecX = (xi - x) + RAND_VECS_2D[idx] * cellularJitter;
double vecY = (yi - y) + RAND_VECS_2D[idx | 1] * cellularJitter;
double newDistance = (fastAbs(vecX) + fastAbs(vecY)) + (vecX * vecX + vecY * vecY);
distance1 = fastMax(fastMin(distance1, newDistance), distance0);
if(newDistance < distance0) {
distance0 = newDistance;
closestHash = hash;
centerX = ((xi + RAND_VECS_2D[idx] * cellularJitter) / frequency);
centerY = ((yi + RAND_VECS_2D[idx | 1] * cellularJitter) / frequency);
} else if(newDistance < distance1) {
distance2 = distance1;
distance1 = newDistance;
} else if(newDistance < distance2) {
distance2 = newDistance;
}
yPrimed += PRIME_Y;
}
xPrimed += PRIME_X;
}
break;
yPrimed += PRIME_Y;
}
xPrimed += PRIME_X;
}
if(distanceFunction == DistanceFunction.Euclidean && returnType != ReturnType.CellValue) {
@@ -351,6 +289,7 @@ public class CellularSampler extends NoiseFunction {
case Distance2Mul -> distance1 * distance0 * 0.5 - 1;
case Distance2Div -> distance0 / distance1 - 1;
case NoiseLookup -> noiseLookup.noise(sl, centerX, centerY);
case LocalNoiseLookup -> noiseLookup.noise(sl, x / frequency - centerX, y / frequency - centerY);
case Distance3 -> distance2 - 1;
case Distance3Add -> (distance2 + distance0) * 0.5 - 1;
case Distance3Sub -> distance2 - distance0 - 1;
@@ -382,120 +321,47 @@ public class CellularSampler extends NoiseFunction {
double centerY = y;
double centerZ = z;
switch(distanceFunction) {
case Euclidean:
case EuclideanSq:
for(int xi = xr - 1; xi <= xr + 1; xi++) {
int yPrimed = yPrimedBase;
for(int xi = xr - 1; xi <= xr + 1; xi++) {
int yPrimed = yPrimedBase;
for(int yi = yr - 1; yi <= yr + 1; yi++) {
int zPrimed = zPrimedBase;
for(int zi = zr - 1; zi <= zr + 1; zi++) {
int hash = hash(seed, xPrimed, yPrimed, zPrimed);
int idx = hash & (255 << 2);
for(int yi = yr - 1; yi <= yr + 1; yi++) {
int zPrimed = zPrimedBase;
for(int zi = zr - 1; zi <= zr + 1; zi++) {
int hash = hash(seed, xPrimed, yPrimed, zPrimed);
int idx = hash & (255 << 2);
double vecX = (xi - x) + RAND_VECS_3D[idx] * cellularJitter;
double vecY = (yi - y) + RAND_VECS_3D[idx | 1] * cellularJitter;
double vecZ = (zi - z) + RAND_VECS_3D[idx | 2] * cellularJitter;
double newDistance = vecX * vecX + vecY * vecY + vecZ * vecZ;
if(newDistance < distance0) {
distance0 = newDistance;
closestHash = hash;
centerX = ((xi + RAND_VECS_3D[idx] * cellularJitter) / frequency);
centerY = ((yi + RAND_VECS_3D[idx | 1] * cellularJitter) / frequency);
centerZ = ((zi + RAND_VECS_3D[idx | 2] * cellularJitter) / frequency);
} else if(newDistance < distance1) {
distance2 = distance1;
distance1 = newDistance;
} else if(newDistance < distance2) {
distance2 = newDistance;
}
zPrimed += PRIME_Z;
}
yPrimed += PRIME_Y;
}
xPrimed += PRIME_X;
}
break;
case Manhattan:
for(int xi = xr - 1; xi <= xr + 1; xi++) {
int yPrimed = yPrimedBase;
double vecX = (xi - x) + RAND_VECS_3D[idx] * cellularJitter;
double vecY = (yi - y) + RAND_VECS_3D[idx | 1] * cellularJitter;
double vecZ = (zi - z) + RAND_VECS_3D[idx | 2] * cellularJitter;
for(int yi = yr - 1; yi <= yr + 1; yi++) {
int zPrimed = zPrimedBase;
for(int zi = zr - 1; zi <= zr + 1; zi++) {
int hash = hash(seed, xPrimed, yPrimed, zPrimed);
int idx = hash & (255 << 2);
double vecX = (xi - x) + RAND_VECS_3D[idx] * cellularJitter;
double vecY = (yi - y) + RAND_VECS_3D[idx | 1] * cellularJitter;
double vecZ = (zi - z) + RAND_VECS_3D[idx | 2] * cellularJitter;
double newDistance = fastAbs(vecX) + fastAbs(vecY) + fastAbs(vecZ);
if(newDistance < distance0) {
distance0 = newDistance;
closestHash = hash;
centerX = ((xi + RAND_VECS_3D[idx] * cellularJitter) / frequency);
centerY = ((yi + RAND_VECS_3D[idx | 1] * cellularJitter) / frequency);
centerZ = ((zi + RAND_VECS_3D[idx | 2] * cellularJitter) / frequency);
} else if(newDistance < distance1) {
distance2 = distance1;
distance1 = newDistance;
} else if(newDistance < distance2) {
distance2 = newDistance;
}
zPrimed += PRIME_Z;
}
yPrimed += PRIME_Y;
}
xPrimed += PRIME_X;
}
break;
case Hybrid:
for(int xi = xr - 1; xi <= xr + 1; xi++) {
int yPrimed = yPrimedBase;
for(int yi = yr - 1; yi <= yr + 1; yi++) {
int zPrimed = zPrimedBase;
for(int zi = zr - 1; zi <= zr + 1; zi++) {
int hash = hash(seed, xPrimed, yPrimed, zPrimed);
int idx = hash & (255 << 2);
double vecX = (xi - x) + RAND_VECS_3D[idx] * cellularJitter;
double vecY = (yi - y) + RAND_VECS_3D[idx | 1] * cellularJitter;
double vecZ = (zi - z) + RAND_VECS_3D[idx | 2] * cellularJitter;
double newDistance = (fastAbs(vecX) + fastAbs(vecY) + fastAbs(vecZ)) +
(vecX * vecX + vecY * vecY + vecZ * vecZ);
double newDistance = 0;
switch(distanceFunction) {
case Euclidean, EuclideanSq -> newDistance = vecX * vecX + vecY * vecY + vecZ * vecZ;
case Manhattan -> newDistance = fastAbs(vecX) + fastAbs(vecY) + fastAbs(vecZ);
case Hybrid -> {
newDistance = (fastAbs(vecX) + fastAbs(vecY) + fastAbs(vecZ)) + (vecX * vecX + vecY * vecY + vecZ * vecZ);
distance1 = fastMax(fastMin(distance1, newDistance), distance0);
if(newDistance < distance0) {
distance0 = newDistance;
closestHash = hash;
centerX = ((xi + RAND_VECS_3D[idx] * cellularJitter) / frequency);
centerY = ((yi + RAND_VECS_3D[idx | 1] * cellularJitter) / frequency);
centerZ = ((zi + RAND_VECS_3D[idx | 2] * cellularJitter) / frequency);
} else if(newDistance < distance1) {
distance2 = distance1;
distance1 = newDistance;
} else if(newDistance < distance2) {
distance2 = newDistance;
}
zPrimed += PRIME_Z;
}
yPrimed += PRIME_Y;
}
xPrimed += PRIME_X;
if(newDistance < distance0) {
distance0 = newDistance;
closestHash = hash;
centerX = ((xi + RAND_VECS_3D[idx] * cellularJitter) / frequency);
centerY = ((yi + RAND_VECS_3D[idx | 1] * cellularJitter) / frequency);
centerZ = ((zi + RAND_VECS_3D[idx | 2] * cellularJitter) / frequency);
} else if(newDistance < distance1) {
distance2 = distance1;
distance1 = newDistance;
} else if(newDistance < distance2) {
distance2 = newDistance;
}
zPrimed += PRIME_Z;
}
break;
default:
break;
yPrimed += PRIME_Y;
}
xPrimed += PRIME_X;
}
if(distanceFunction == DistanceFunction.Euclidean && returnType != ReturnType.CellValue) {
@@ -514,6 +380,7 @@ public class CellularSampler extends NoiseFunction {
case Distance2Mul -> distance1 * distance0 * 0.5 - 1;
case Distance2Div -> distance0 / distance1 - 1;
case NoiseLookup -> noiseLookup.noise(sl, centerX, centerY, centerZ);
case LocalNoiseLookup -> noiseLookup.noise(sl, x / frequency - centerX, y / frequency - centerY, z / frequency - centerZ);
case Distance3 -> distance2 - 1;
case Distance3Add -> (distance2 + distance0) * 0.5 - 1;
case Distance3Sub -> distance2 - distance0 - 1;
@@ -540,6 +407,7 @@ public class CellularSampler extends NoiseFunction {
Distance2Mul,
Distance2Div,
NoiseLookup,
LocalNoiseLookup,
Distance3,
Distance3Add,
Distance3Sub,
@@ -0,0 +1,66 @@
package com.dfsek.terra.addons.noise.samplers.noise;
public class DistanceSampler extends NoiseFunction {
private final DistanceFunction distanceFunction;
private final double ox, oy, oz;
private final boolean normalize;
private final double radius;
private final double distanceAtRadius;
public DistanceSampler(DistanceFunction distanceFunction, double ox, double oy, double oz, boolean normalize, double radius) {
frequency = 1;
this.distanceFunction = distanceFunction;
this.ox = ox;
this.oy = oy;
this.oz = oz;
this.normalize = normalize;
this.radius = radius;
this.distanceAtRadius = distance2d(distanceFunction, radius, 0); // distance2d and distance3d should return the same value
}
@Override
public double getNoiseRaw(long seed, double x, double y) {
double dx = x - ox;
double dy = y - oz;
if (normalize && (fastAbs(dx) > radius || fastAbs(dy) > radius)) return 1;
double dist = distance2d(distanceFunction, dx, dy);
if (normalize) return fastMin(((2*dist)/distanceAtRadius)-1, 1);
return dist;
}
@Override
public double getNoiseRaw(long seed, double x, double y, double z) {
double dx = x - ox;
double dy = y - oy;
double dz = z - oz;
if(normalize && (fastAbs(dx) > radius || fastAbs(dy) > radius || fastAbs(dz) > radius)) return 1;
double dist = distance3d(distanceFunction, dx, dy, dz);
if (normalize) return fastMin(((2*dist)/distanceAtRadius)-1, 1);
return dist;
}
private static double distance2d(DistanceFunction distanceFunction, double x, double z) {
return switch(distanceFunction) {
case Euclidean -> fastSqrt(x*x + z*z);
case EuclideanSq -> x*x + z*z;
case Manhattan -> fastAbs(x) + fastAbs(z);
};
}
private static double distance3d(DistanceFunction distanceFunction, double x, double y, double z) {
return switch(distanceFunction) {
case Euclidean -> fastSqrt(x*x + y*y + z*z);
case EuclideanSq -> x*x + y*y + z*z;
case Manhattan -> fastAbs(x) + fastAbs(y) + fastAbs(z);
};
}
public enum DistanceFunction {
Euclidean,
EuclideanSq,
Manhattan
}
}
@@ -0,0 +1,13 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
version = version("1.0.0")
dependencies {
compileOnlyApi(project(":common:addons:manifest-addon-loader"))
api("com.dfsek", "paralithic", Versions.Libraries.paralithic)
}
tasks.named<ShadowJar>("shadowJar") {
relocate("com.dfsek.paralithic", "com.dfsek.terra.addons.numberpredicate.lib.paralithic")
}
@@ -0,0 +1,34 @@
package com.dfsek.terra.addons.numberpredicate;
import com.dfsek.paralithic.Expression;
import com.dfsek.paralithic.eval.parser.Parser;
import com.dfsek.paralithic.eval.parser.Scope;
import com.dfsek.paralithic.eval.tokenizer.ParseException;
import com.dfsek.tectonic.api.depth.DepthTracker;
import com.dfsek.tectonic.api.exception.LoadException;
import com.dfsek.tectonic.api.loader.ConfigLoader;
import com.dfsek.tectonic.api.loader.type.TypeLoader;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.AnnotatedType;
import java.util.function.DoublePredicate;
public class DoublePredicateLoader implements TypeLoader<DoublePredicate> {
@Override
public DoublePredicate load(@NotNull AnnotatedType annotatedType, @NotNull Object o, @NotNull ConfigLoader configLoader,
DepthTracker depthTracker) throws LoadException {
if (o instanceof String expressionString) {
Scope scope = new Scope();
scope.addInvocationVariable("value");
try {
Expression expression = new Parser().parse(expressionString, scope);
return d -> expression.evaluate(d) != 0; // Paralithic expressions treat '!= 0' as true
} catch(ParseException e) {
throw new LoadException("Failed to parse double predicate expression", e, depthTracker);
}
} else {
throw new LoadException("Double predicates must be defined as a string. E.g. 'value > 3'", depthTracker);
}
}
}
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.numberpredicate;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.DoublePredicate;
import java.util.function.Supplier;
import com.dfsek.terra.addons.manifest.api.AddonInitializer;
import com.dfsek.terra.api.Platform;
import com.dfsek.terra.api.addon.BaseAddon;
import com.dfsek.terra.api.event.events.config.pack.ConfigPackPreLoadEvent;
import com.dfsek.terra.api.event.functional.FunctionalEventHandler;
import com.dfsek.terra.api.inject.annotations.Inject;
public class NumberPredicateAddon implements AddonInitializer {
@Inject
private Platform plugin;
@Inject
private BaseAddon addon;
@Override
public void initialize() {
plugin.getEventManager()
.getHandler(FunctionalEventHandler.class)
.register(addon, ConfigPackPreLoadEvent.class)
.then(event -> event.getPack().applyLoader(DoublePredicate.class, new DoublePredicateLoader()))
.priority(50)
.failThrough();
}
}
@@ -0,0 +1,12 @@
schema-version: 1
contributors:
- Terra contributors
id: config-number-predicate
version: @VERSION@
entrypoints:
- "com.dfsek.terra.addons.numberpredicate.NumberPredicateAddon"
website:
issues: https://github.com/PolyhedralDev/Terra/issues
source: https://github.com/PolyhedralDev/Terra
docs: https://terra.polydev.org
license: MIT License
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-2021 Polyhedral Development
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+3
View File
@@ -0,0 +1,3 @@
# config-ore-v2
Registers the default configuration for Terra Ores, `ORE`.
@@ -0,0 +1,11 @@
version = version("1.0.0")
dependencies {
compileOnlyApi(project(":common:addons:manifest-addon-loader"))
implementation("net.jafama", "jafama", Versions.Libraries.Internal.jafama)
testImplementation("net.jafama", "jafama", Versions.Libraries.Internal.jafama)
}
tasks.named<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("shadowJar") {
relocate("net.jafama", "com.dfsek.terra.addons.ore.lib.jafama")
}
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.ore.v2;
import com.dfsek.terra.addons.manifest.api.AddonInitializer;
import com.dfsek.terra.api.Platform;
import com.dfsek.terra.api.addon.BaseAddon;
import com.dfsek.terra.api.event.events.config.pack.ConfigPackPreLoadEvent;
import com.dfsek.terra.api.event.functional.FunctionalEventHandler;
import com.dfsek.terra.api.inject.annotations.Inject;
public class OreAddon implements AddonInitializer {
@Inject
private Platform platform;
@Inject
private BaseAddon addon;
@Override
public void initialize() {
platform.getEventManager()
.getHandler(FunctionalEventHandler.class)
.register(addon, ConfigPackPreLoadEvent.class)
.then(event -> event.getPack().registerConfigType(new OreConfigType(), addon.key("ORE"), 1))
.failThrough();
}
}
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.ore.v2;
import com.dfsek.terra.api.Platform;
import com.dfsek.terra.api.config.ConfigFactory;
import com.dfsek.terra.api.config.ConfigPack;
import com.dfsek.terra.api.config.ConfigType;
import com.dfsek.terra.api.structure.Structure;
import com.dfsek.terra.api.util.reflection.TypeKey;
public class OreConfigType implements ConfigType<OreTemplate, Structure> {
public static final TypeKey<Structure> ORE_TYPE_TOKEN = new TypeKey<>() {
};
private final OreFactory factory = new OreFactory();
@Override
public OreTemplate getTemplate(ConfigPack pack, Platform platform) {
return new OreTemplate();
}
@Override
public ConfigFactory<OreTemplate, Structure> getFactory() {
return factory;
}
@Override
public TypeKey<Structure> getTypeKey() {
return ORE_TYPE_TOKEN;
}
}
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.ore.v2;
import com.dfsek.terra.addons.ore.v2.ores.VanillaOre;
import com.dfsek.terra.api.Platform;
import com.dfsek.terra.api.block.state.BlockState;
import com.dfsek.terra.api.config.ConfigFactory;
import com.dfsek.terra.api.structure.Structure;
public class OreFactory implements ConfigFactory<OreTemplate, Structure> {
@Override
public VanillaOre build(OreTemplate config, Platform platform) {
BlockState m = config.getMaterial();
return new VanillaOre(m, config.getSize(), config.getReplaceable(), config.doPhysics(), config.isExposed(),
config.getMaterialOverrides());
}
}
@@ -0,0 +1,80 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.ore.v2;
import com.dfsek.tectonic.api.config.template.annotations.Default;
import com.dfsek.tectonic.api.config.template.annotations.Description;
import com.dfsek.tectonic.api.config.template.annotations.Final;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import java.util.HashMap;
import java.util.Map;
import com.dfsek.terra.api.block.BlockType;
import com.dfsek.terra.api.block.state.BlockState;
import com.dfsek.terra.api.config.AbstractableTemplate;
import com.dfsek.terra.api.config.meta.Meta;
import com.dfsek.terra.api.util.collection.MaterialSet;
@SuppressWarnings({ "unused", "FieldMayBeFinal" })
public class OreTemplate implements AbstractableTemplate {
@Value("id")
@Final
private String id;
@Value("material")
private @Meta BlockState material;
@Value("material-overrides")
@Default
private @Meta Map<@Meta BlockType, @Meta BlockState> materials = new HashMap<>();
@Value("replace")
private @Meta MaterialSet replaceable;
@Value("physics")
@Default
private @Meta boolean physics = false;
@Value("size")
private @Meta double size;
@Value("exposed")
@Default
@Description("The chance that ore blocks bordering air will be discarded as candidates for ore. 0 = 0%, 1 = 100%")
private @Meta double exposed = 0.0f;
public boolean doPhysics() {
return physics;
}
public double getSize() {
return size;
}
public BlockState getMaterial() {
return material;
}
public MaterialSet getReplaceable() {
return replaceable;
}
public String getID() {
return id;
}
public Map<BlockType, BlockState> getMaterialOverrides() {
return materials;
}
public double isExposed() {
return exposed;
}
}
@@ -0,0 +1,207 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.ore.v2.ores;
import net.jafama.FastMath;
import java.util.BitSet;
import java.util.Map;
import java.util.Random;
import com.dfsek.terra.api.block.BlockType;
import com.dfsek.terra.api.block.state.BlockState;
import com.dfsek.terra.api.structure.Structure;
import com.dfsek.terra.api.util.Rotation;
import com.dfsek.terra.api.util.collection.MaterialSet;
import com.dfsek.terra.api.util.vector.Vector3Int;
import com.dfsek.terra.api.world.WritableWorld;
public class VanillaOre implements Structure {
private final BlockState material;
private final double size;
private final MaterialSet replaceable;
private final boolean applyGravity;
private final double exposed;
private final Map<BlockType, BlockState> materials;
public VanillaOre(BlockState material, double size, MaterialSet replaceable, boolean applyGravity,
double exposed, Map<BlockType, BlockState> materials) {
this.material = material;
this.size = size;
this.replaceable = replaceable;
this.applyGravity = applyGravity;
this.exposed = exposed;
this.materials = materials;
}
protected static boolean shouldNotDiscard(Random random, double chance) {
if(chance <= 0.0F) {
return true;
} else if(chance >= 1.0F) {
return false;
} else {
return random.nextFloat() >= chance;
}
}
public static double lerp(double t, double v0, double v1) {
return v0 + t * (v1 - v0);
}
@Override
public boolean generate(Vector3Int location, WritableWorld world, Random random, Rotation rotation) {
float randomRadian = random.nextFloat() * (float) Math.PI;
double eigthSize = size / 8.0F;
// Place points to form a line segment
double startX = (double) location.getX() + FastMath.sin(randomRadian) * eigthSize;
double endX = (double) location.getX() - FastMath.sin(randomRadian) * eigthSize;
double startZ = (double) location.getZ() + FastMath.cos(randomRadian) * eigthSize;
double endZ = (double) location.getZ() - FastMath.cos(randomRadian) * eigthSize;
double startY = location.getY() + random.nextInt(3) - 2;
double endY = location.getY() + random.nextInt(3) - 2;
int sizeInt = (int) size;
double[] points = new double[sizeInt * 4];
// Compute initial point positions and radius
for(int i = 0; i < sizeInt; ++i) {
float t = (float) i / (float) sizeInt;
double xt = lerp(t, startX, endX);
double yt = lerp(t, startY, endY);
double zt = lerp(t, startZ, endZ);
double roll = random.nextDouble() * size / 16.0;
// Taper radius closer to line ends
double radius = ((FastMath.sin((float) Math.PI * t) + 1.0F) * roll + 1.0) / 2.0;
points[i * 4] = xt;
points[i * 4 + 1] = yt;
points[i * 4 + 2] = zt;
points[i * 4 + 3] = radius;
}
// Compare every point to every other point
for(int a = 0; a < sizeInt - 1; ++a) {
double radiusA = points[a * 4 + 3];
if(radiusA > 0.0) {
for(int b = a + 1; b < sizeInt; ++b) {
double radiusB = points[b * 4 + 3];
if(radiusB > 0.0) {
double dxt = points[a * 4] - points[b * 4];
double dyt = points[a * 4 + 1] - points[b * 4 + 1];
double dzt = points[a * 4 + 2] - points[b * 4 + 2];
double dRadius = radiusA - radiusB;
// If the radius difference is greater than the distance between the two points
if(dRadius * dRadius > dxt * dxt + dyt * dyt + dzt * dzt) {
// Set smaller of two radii to -1
if(dRadius > 0.0) {
points[b * 4 + 3] = -1.0;
} else {
points[a * 4 + 3] = -1.0;
}
}
}
}
}
}
int outset = (int) FastMath.ceil((size / 16.0F * 2.0F + 1.0F) / 2.0F);
int x = (int) (location.getX() - FastMath.ceil(eigthSize) - outset);
int y = location.getY() - 2 - outset;
int z = (int) (location.getZ() - FastMath.ceil(eigthSize) - outset);
int horizontalSize = (int) (2 * (FastMath.ceil(eigthSize) + outset));
int verticalSize = 2 * (2 + outset);
int sphereCount = 0;
BitSet visited = new BitSet(horizontalSize * verticalSize * horizontalSize);
// Generate a sphere at each point
for(int i = 0; i < sizeInt; ++i) {
double radius = points[i * 4 + 3];
if(radius > 0.0) {
double xt = points[i * 4];
double yt = points[i * 4 + 1];
double zt = points[i * 4 + 2];
int xLowerBound = (int) FastMath.max(FastMath.floor(xt - radius), x);
int xUpperBound = (int) FastMath.max(FastMath.floor(xt + radius), xLowerBound);
int yLowerBound = (int) FastMath.max(FastMath.floor(yt - radius), y);
int yUpperBound = (int) FastMath.max(FastMath.floor(yt + radius), yLowerBound);
int zLowerBound = (int) FastMath.max(FastMath.floor(zt - radius), z);
int zUpperBound = (int) FastMath.max(FastMath.floor(zt + radius), zLowerBound);
// Iterate over coordinates within bounds
for(int xi = xLowerBound; xi <= xUpperBound; ++xi) {
double dx = ((double) xi + 0.5 - xt) / radius;
if(dx * dx < 1.0) {
for(int yi = yLowerBound; yi <= yUpperBound; ++yi) {
double dy = ((double) yi + 0.5 - yt) / radius;
if(dx * dx + dy * dy < 1.0) {
for(int zi = zLowerBound; zi <= zUpperBound; ++zi) {
double dz = ((double) zi + 0.5 - zt) / radius;
// If position is inside the sphere
if(dx * dx + dy * dy + dz * dz < 1.0 && !(yi < world.getMinHeight() || yi >= world.getMaxHeight())) {
int index = xi - x + (yi - y) * horizontalSize + (zi - z) * horizontalSize * verticalSize;
if(!visited.get(index)) { // Skip blocks that have already been visited
visited.set(index);
BlockType block = world.getBlockState(xi, yi, zi).getBlockType();
if(shouldPlace(block, random, world, xi, yi, zi)) {
world.setBlockState(xi, yi, zi, getMaterial(block), isApplyGravity());
++sphereCount;
break;
}
}
}
}
}
}
}
}
}
}
return sphereCount > 0;
}
public boolean shouldPlace(BlockType type, Random random, WritableWorld world, int x, int y, int z) {
if(!getReplaceable().contains(type)) {
return false;
} else if(shouldNotDiscard(random, exposed)) {
return true;
} else {
return !(world.getBlockState(x, y, z - 1).isAir() ||
world.getBlockState(x, y, z + 1).isAir() ||
world.getBlockState(x, y - 1, z).isAir() ||
world.getBlockState(x, y + 1, z).isAir() ||
world.getBlockState(x - 1, y, z).isAir() ||
world.getBlockState(x + 1, y, z).isAir());
}
}
public BlockState getMaterial(BlockType replace) {
return materials.getOrDefault(replace, material);
}
public MaterialSet getReplaceable() {
return replaceable;
}
public boolean isApplyGravity() {
return applyGravity;
}
}
@@ -0,0 +1,12 @@
schema-version: 1
contributors:
- Terra contributors
id: config-ore-v2
version: @VERSION@
entrypoints:
- "com.dfsek.terra.addons.ore.v2.OreAddon"
website:
issues: https://github.com/PolyhedralDev/Terra/issues
source: https://github.com/PolyhedralDev/Terra
docs: https://terra.polydev.org
license: MIT License
@@ -14,8 +14,13 @@ import com.dfsek.terra.api.event.events.config.pack.ConfigPackPreLoadEvent;
import com.dfsek.terra.api.event.functional.FunctionalEventHandler;
import com.dfsek.terra.api.inject.annotations.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class OreAddon implements AddonInitializer {
private static final Logger logger = LoggerFactory.getLogger(OreAddon.class);
@Inject
private Platform platform;
@@ -29,5 +34,8 @@ public class OreAddon implements AddonInitializer {
.register(addon, ConfigPackPreLoadEvent.class)
.then(event -> event.getPack().registerConfigType(new OreConfigType(), addon.key("ORE"), 1))
.failThrough();
if(platform.getTerraConfig().isDebugLog())
logger.warn("The ore-config addon is deprecated and scheduled for removal in Terra 7.0. It is recommended to use the ore-config-v2 addon for future pack development instead.");
}
}
@@ -0,0 +1,6 @@
version = version("1.0.0")
dependencies {
compileOnlyApi(project(":common:addons:manifest-addon-loader"))
compileOnlyApi(project(":common:addons:chunk-generator-noise-3d"))
}
@@ -0,0 +1,32 @@
package com.dfsek.terra.addon.feature.locator.slant;
import com.dfsek.terra.addons.chunkgenerator.generation.NoiseChunkGenerator3D;
import com.dfsek.terra.api.structure.feature.BinaryColumn;
import com.dfsek.terra.api.structure.feature.Locator;
import com.dfsek.terra.api.world.World;
import com.dfsek.terra.api.world.biome.generation.BiomeProvider;
import com.dfsek.terra.api.world.chunk.generation.util.Column;
import java.util.function.DoublePredicate;
public class SlantLocator implements Locator {
private final DoublePredicate predicate;
public SlantLocator(DoublePredicate predicate) {
this.predicate = predicate;
}
@Override
public BinaryColumn getSuitableCoordinates(Column<?> column) {
return column.newBinaryColumn(y -> {
int x = column.getX();
int z = column.getZ();
World world = column.getWorld();
NoiseChunkGenerator3D generator = (NoiseChunkGenerator3D) world.getGenerator();
BiomeProvider biomeProvider = world.getBiomeProvider();
return predicate.test(generator.getSlant(x, y, z, world, biomeProvider));
});
}
}
@@ -0,0 +1,37 @@
package com.dfsek.terra.addon.feature.locator.slant;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import com.dfsek.terra.addons.manifest.api.AddonInitializer;
import com.dfsek.terra.api.Platform;
import com.dfsek.terra.api.addon.BaseAddon;
import com.dfsek.terra.api.event.events.config.pack.ConfigPackPreLoadEvent;
import com.dfsek.terra.api.event.functional.FunctionalEventHandler;
import com.dfsek.terra.api.inject.annotations.Inject;
import com.dfsek.terra.api.structure.feature.Locator;
import com.dfsek.terra.api.util.reflection.TypeKey;
import java.util.function.Supplier;
public class SlantLocatorAddon implements AddonInitializer {
public static final TypeKey<Supplier<ObjectTemplate<Locator>>> LOCATOR_TOKEN = new TypeKey<>() {
};
@Inject
private Platform platform;
@Inject
private BaseAddon addon;
@Override
public void initialize() {
platform.getEventManager()
.getHandler(FunctionalEventHandler.class)
.register(addon, ConfigPackPreLoadEvent.class)
.priority(1)
.then(event -> event.getPack().getOrCreateRegistry(LOCATOR_TOKEN).register(addon.key("SLANT"), SlantLocatorTemplate::new))
.failThrough();
}
}
@@ -0,0 +1,20 @@
package com.dfsek.terra.addon.feature.locator.slant;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import com.dfsek.terra.api.structure.feature.Locator;
import java.util.function.DoublePredicate;
public class SlantLocatorTemplate implements ObjectTemplate<Locator> {
@Value("condition")
private DoublePredicate predicate;
@Override
public Locator get() {
return new SlantLocator(predicate);
}
}
@@ -0,0 +1,14 @@
schema-version: 1
contributors:
- Terra contributors
id: locator-slant-noise-3d
version: @VERSION@
entrypoints:
- "com.dfsek.terra.addon.feature.locator.slant.SlantLocatorAddon"
website:
issues: https://github.com/PolyhedralDev/Terra/issues
source: https://github.com/PolyhedralDev/Terra
docs: https://terra.polydev.org
license: MIT License
depends:
chunk-generator-noise-3d: "[1.2.0,2.0.0)"
@@ -30,15 +30,17 @@ public class BlockFunctionBuilder implements FunctionBuilder<BlockFunction> {
@Override
public BlockFunction build(List<Returnable<?>> argumentList, Position position) {
if(argumentList.size() < 4) throw new ParseException("Expected data", position);
Returnable<Boolean> booleanReturnable = new BooleanConstant(true, position);
if(argumentList.size() == 5) booleanReturnable = (Returnable<Boolean>) argumentList.get(4);
Returnable<Boolean> overwrite = new BooleanConstant(true, position);
if(argumentList.size() >= 5) overwrite = (Returnable<Boolean>) argumentList.get(4);
Returnable<Boolean> physics = new BooleanConstant(false, position);
if(argumentList.size() == 6) physics = (Returnable<Boolean>) argumentList.get(5);
if(argumentList.get(3) instanceof StringConstant) {
return new BlockFunction.Constant((Returnable<Number>) argumentList.get(0), (Returnable<Number>) argumentList.get(1),
(Returnable<Number>) argumentList.get(2), (StringConstant) argumentList.get(3),
booleanReturnable, platform, position);
overwrite, physics, platform, position);
}
return new BlockFunction((Returnable<Number>) argumentList.get(0), (Returnable<Number>) argumentList.get(1),
(Returnable<Number>) argumentList.get(2), (Returnable<String>) argumentList.get(3), booleanReturnable,
(Returnable<Number>) argumentList.get(2), (Returnable<String>) argumentList.get(3), overwrite, physics,
platform, position);
}
@@ -52,7 +54,7 @@ public class BlockFunctionBuilder implements FunctionBuilder<BlockFunction> {
return switch(position) {
case 0, 1, 2 -> Returnable.ReturnType.NUMBER;
case 3 -> Returnable.ReturnType.STRING;
case 4 -> Returnable.ReturnType.BOOLEAN;
case 4, 5 -> Returnable.ReturnType.BOOLEAN;
default -> null;
};
}
@@ -35,10 +35,11 @@ public class BlockFunction implements Function<Void> {
protected final Platform platform;
private final Map<String, BlockState> data = new HashMap<>();
private final Returnable<Boolean> overwrite;
private final Returnable<Boolean> physics;
private final Position position;
public BlockFunction(Returnable<Number> x, Returnable<Number> y, Returnable<Number> z, Returnable<String> blockData,
Returnable<Boolean> overwrite, Platform platform, Position position) {
Returnable<Boolean> overwrite, Returnable<Boolean> physics, Platform platform, Position position) {
this.x = x;
this.y = y;
this.z = z;
@@ -46,6 +47,7 @@ public class BlockFunction implements Function<Void> {
this.overwrite = overwrite;
this.platform = platform;
this.position = position;
this.physics = physics;
}
@Override
@@ -76,7 +78,7 @@ public class BlockFunction implements Function<Void> {
FastMath.roundToInt(xz.getZ())).mutable().add(arguments.getOrigin());
BlockState current = arguments.getWorld().getBlockState(set);
if(overwrite.apply(implementationArguments, scope) || current.isAir()) {
arguments.getWorld().setBlockState(set, rot);
arguments.getWorld().setBlockState(set, rot, physics.apply(implementationArguments, scope));
}
} catch(RuntimeException e) {
logger.error("Failed to place block at location {}", arguments.getOrigin(), e);
@@ -92,8 +94,8 @@ public class BlockFunction implements Function<Void> {
private final BlockState state;
public Constant(Returnable<Number> x, Returnable<Number> y, Returnable<Number> z, StringConstant blockData,
Returnable<Boolean> overwrite, Platform platform, Position position) {
super(x, y, z, blockData, overwrite, platform, position);
Returnable<Boolean> overwrite, Returnable<Boolean> physics, Platform platform, Position position) {
super(x, y, z, blockData, overwrite, physics, platform, position);
this.state = platform.getWorldHandle().createBlockState(blockData.getConstant());
}
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-2021 Polyhedral Development
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,4 @@
# structure-terrascript-loader
Implements the TerraScript structure scripting language, and loads all `*.tesf`
files into the Structure registry.
@@ -0,0 +1,244 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
version = version("1.0.0")
dependencies {
api("commons-io:commons-io:2.7")
api("org.ow2.asm:asm:9.5")
api("org.ow2.asm:asm-commons:9.5")
compileOnlyApi(project(":common:addons:manifest-addon-loader"))
implementation("net.jafama", "jafama", Versions.Libraries.Internal.jafama)
testImplementation("net.jafama", "jafama", Versions.Libraries.Internal.jafama)
}
tasks.named<ShadowJar>("shadowJar") {
relocate("org.apache.commons", "com.dfsek.terra.addons.terrascript.v2.lib.commons")
relocate("net.jafama", "com.dfsek.terra.addons.terrascript.v2.lib.jafama")
}
val astSourceSet = buildDir.resolve("generated/ast")
val astPackage = astSourceSet.resolve("com/dfsek/terra/addons/terrascript/v2/ast")
data class ASTClass(
val name: String,
val imports: List<String>,
val nodes: List<ASTNode>,
val constructorFields: List<Pair<String, String>> = emptyList(),
)
data class ASTNode(
val name: String,
val constructorFields: List<Pair<String, String>>,
val mutableFields: List<Pair<String, String>> = emptyList() // TODO - Remove mutability from AST nodes
)
// Auto generate AST classes rather than writing them by hand
tasks.register("genTerrascriptAstClasses") {
val packageName = astPackage.toRelativeString(astSourceSet).replace('/', '.')
fun generateClass(clazz: ASTClass) {
val src = StringBuilder()
src.appendLine("package $packageName;\n");
for (imprt in clazz.imports) src.appendLine("import $imprt;")
src.appendLine("""
/**
* Auto-generated class via genTerrascriptAstClasses gradle task
*/
public abstract class ${clazz.name} {
""".trimIndent())
for (field in clazz.constructorFields) {
src.appendLine(" public final ${field.second} ${field.first};")
}
src.appendLine("""
|
| public ${clazz.name}(${clazz.constructorFields.joinToString { "${it.second} ${it.first}" }}) {
""".trimMargin())
for (field in clazz.constructorFields) {
src.appendLine(" this.${field.first} = ${field.first};")
}
src.appendLine("""
| }
|
| public interface Visitor<R> {
|
""".trimMargin())
for (node in clazz.nodes) {
src.appendLine(" R visit${node.name}${clazz.name}(${node.name} ${clazz.name.toLowerCase()});")
}
src.appendLine("""
|
| }
|
| public abstract <R> R accept(Visitor<R> visitor);
""".trimMargin())
for (node in clazz.nodes) {
src.appendLine()
// Inner class declaration
src.appendLine(" public static class ${node.name} extends ${clazz.name} {\n")
// Add fields
for (field in node.constructorFields) {
src.appendLine(" public final ${field.second} ${field.first};")
}
for (field in node.mutableFields) {
src.appendLine(" private ${field.second} ${field.first};")
}
src.appendLine()
// Add constructor
src.appendLine("""
| public ${node.name}(${node.constructorFields.plus(clazz.constructorFields).joinToString { "${it.second} ${it.first}" }}) {
| super(${clazz.constructorFields.joinToString { it.first }});
""".trimMargin())
for (field in node.constructorFields) {
src.appendLine(" this.${field.first} = ${field.first};")
}
src.appendLine(" }")
// Add getters and setters for mutable fields
for (field in node.mutableFields) {
src.appendLine("""
|
| public void set${field.first.capitalize()}(${field.second} value) {
| this.${field.first} = value;
| }
|
| public ${field.second} get${field.first.capitalize()}() {
| if (this.${field.first} == null) throw new RuntimeException("Compilation bug! Field ${field.first} has not been set yet");
| return this.${field.first};
| }
""".trimMargin())
}
src.appendLine("""
|
| @Override
| public <R> R accept(Visitor<R> visitor) {
| return visitor.visit${node.name}${clazz.name}(this);
| }
| }
""".trimMargin())
}
src.appendLine("}")
val outputFile = astPackage.resolve("${clazz.name}.java")
outputFile.writeText(src.toString())
}
doLast {
astSourceSet.deleteRecursively()
astPackage.mkdirs()
listOf(
ASTClass(
"Expr",
listOf(
"com.dfsek.terra.addons.terrascript.v2.Type",
"com.dfsek.terra.addons.terrascript.v2.parser.UnaryOperator",
"com.dfsek.terra.addons.terrascript.v2.parser.BinaryOperator",
"com.dfsek.terra.addons.terrascript.v2.Environment",
"com.dfsek.terra.addons.terrascript.v2.Environment.Symbol",
"com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition",
"java.util.List",
),
listOf(
ASTNode("Binary", listOf("left" to "Expr", "operator" to "BinaryOperator", "right" to "Expr")),
ASTNode("Grouping", listOf("expression" to "Expr")),
ASTNode("Literal", listOf("value" to "Object", "type" to "Type")),
ASTNode("Unary", listOf("operator" to "UnaryOperator", "operand" to "Expr")),
ASTNode("Call", listOf("callee" to "Expr", "arguments" to "List<Expr>")),
ASTNode("Variable", listOf("identifier" to "String"), listOf("symbol" to "Symbol", "scope" to "Environment")),
ASTNode("Assignment", listOf("lValue" to "Variable", "rValue" to "Expr")),
ASTNode("Void", listOf()),
),
listOf("position" to "SourcePosition")
),
ASTClass(
"Stmt",
listOf(
"com.dfsek.terra.addons.terrascript.v2.Type",
"com.dfsek.terra.api.util.generic.pair.Pair",
"com.dfsek.terra.addons.terrascript.v2.Environment",
"com.dfsek.terra.addons.terrascript.v2.Environment.Symbol",
"com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition",
"java.util.List",
"java.util.Optional",
),
listOf(
ASTNode("Expression", listOf("expression" to "Expr")),
ASTNode("Block", listOf("statements" to "List<Stmt>")),
ASTNode("FunctionDeclaration", listOf("identifier" to "String", "parameters" to "List<Pair<String, Type>>", "returnType" to "Type", "body" to "Block"), listOf("symbol" to "Symbol")),
ASTNode("VariableDeclaration", listOf("type" to "Type", "identifier" to "String", "value" to "Expr"), listOf("scope" to "Environment")),
ASTNode("Return", listOf("value" to "Expr"), listOf("type" to "Type")),
ASTNode("If", listOf("condition" to "Expr", "trueBody" to "Block", "elseIfClauses" to "List<Pair<Expr, Block>>", "elseBody" to "Optional<Block>")),
ASTNode("For", listOf("initializer" to "Stmt", "condition" to "Expr", "incrementer" to "Expr", "body" to "Block")),
ASTNode("While", listOf("condition" to "Expr", "body" to "Block")),
ASTNode("NoOp", listOf()),
ASTNode("Break", listOf()),
ASTNode("Continue", listOf()),
),
listOf("position" to "SourcePosition")
),
ASTClass(
"TypedExpr",
listOf(
"com.dfsek.terra.addons.terrascript.v2.Type",
"com.dfsek.terra.addons.terrascript.v2.parser.UnaryOperator",
"com.dfsek.terra.addons.terrascript.v2.parser.BinaryOperator",
"java.util.List",
),
listOf(
ASTNode("Binary", listOf("left" to "TypedExpr", "operator" to "BinaryOperator", "right" to "TypedExpr")),
ASTNode("Grouping", listOf("expression" to "TypedExpr")),
ASTNode("Literal", listOf("value" to "Object")),
ASTNode("Unary", listOf("operator" to "UnaryOperator", "operand" to "TypedExpr")),
ASTNode("Call", listOf("callee" to "TypedExpr", "arguments" to "List<TypedExpr>")),
ASTNode("Variable", listOf("identifier" to "String")),
ASTNode("Assignment", listOf("lValue" to "Variable", "rValue" to "TypedExpr")),
ASTNode("Void", listOf()),
),
listOf("type" to "Type")
),
ASTClass(
"TypedStmt",
listOf(
"com.dfsek.terra.addons.terrascript.v2.Type",
"com.dfsek.terra.api.util.generic.pair.Pair",
"java.util.List",
"java.util.Optional",
),
listOf(
ASTNode("Expression", listOf("expression" to "TypedExpr")),
ASTNode("Block", listOf("statements" to "List<TypedStmt>")),
ASTNode("FunctionDeclaration", listOf("identifier" to "String", "parameters" to "List<Pair<String, Type>>", "returnType" to "Type", "body" to "Block", "scopedIdentifier" to "String")),
ASTNode("VariableDeclaration", listOf("type" to "Type", "identifier" to "String", "value" to "TypedExpr")),
ASTNode("Return", listOf("value" to "TypedExpr")),
ASTNode("If", listOf("condition" to "TypedExpr", "trueBody" to "Block", "elseIfClauses" to "List<Pair<TypedExpr, Block>>", "elseBody" to "Optional<Block>")),
ASTNode("For", listOf("initializer" to "TypedStmt", "condition" to "TypedExpr", "incrementer" to "TypedExpr", "body" to "Block")),
ASTNode("While", listOf("condition" to "TypedExpr", "body" to "Block")),
ASTNode("NoOp", listOf()),
ASTNode("Break", listOf()),
ASTNode("Continue", listOf()),
),
),
).forEach(::generateClass)
}
}
tasks.getByName("compileJava") {
dependsOn("genTerrascriptAstClasses")
}
sourceSets.getByName("main") {
java {
srcDirs(astSourceSet)
}
}
@@ -0,0 +1,132 @@
package com.dfsek.terra.addons.terrascript.v2;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.dfsek.terra.addons.terrascript.v2.Environment.ScopeException.NonexistentSymbolException;
import com.dfsek.terra.addons.terrascript.v2.Environment.ScopeException.SymbolAlreadyExistsException;
import com.dfsek.terra.addons.terrascript.v2.codegen.NativeFunction;
public class Environment {
public final String name;
private final Environment outer;
private final boolean canAccessOuterVariables;
private final Map<String, Symbol> symbolTable = new HashMap<>();
private final boolean inLoop;
private final int index;
private int innerCount = 0;
private Environment(@Nullable Environment outer, boolean canAccessOuterVariables, boolean inLoop, int index) {
this.outer = outer;
this.canAccessOuterVariables = canAccessOuterVariables;
this.inLoop = inLoop;
this.index = index;
this.name = String.join("_", getNestedIndexes().stream().map(Object::toString).toList());
// Populate global scope with built-in Java implemented methods
// TODO - Replace with AST import nodes
if(index == 0) NativeFunction.BUILTIN_FUNCTIONS.forEach((name, function) ->
symbolTable.put(name, new Symbol.Variable(
new Type.Function.Native(function.getReturnType(),
function.getParameterTypes(), name,
this, function))));
}
public static Environment global() {
return new Environment(null, false, false, 0);
}
public Environment lexicalInner() {
return new Environment(this, true, inLoop, innerCount++);
}
public Environment loopInner() {
return new Environment(this, true, true, innerCount++);
}
public Environment functionalInner() {
return new Environment(this, false, inLoop, innerCount++);
}
private List<Integer> getNestedIndexes() {
List<Integer> idxs = new ArrayList<>();
for(Environment env = this; env.outer != null; env = env.outer) {
idxs.add(0, env.index);
}
return idxs;
}
public Environment outer() {
if(outer == null) throw new RuntimeException("Attempted to retrieve outer scope of global scope");
return outer;
}
/**
* Returns symbol table entry for a variable identifier, includes enclosing scopes in lookup.
* <br>
* Does not factor context of lookup, checks for order of declaration should be done while
* symbol tables are being populated.
*
* @param id identifier used in variable declaration
*
* @return variable symbol table entry
*
* @throws NonexistentSymbolException if symbol is not declared in symbol table
*/
public Symbol getVariable(String id) throws NonexistentSymbolException {
Symbol symbol = symbolTable.get(id);
if(symbol != null) return symbol;
if(outer == null) throw new NonexistentSymbolException();
if(canAccessOuterVariables) return outer.getVariable(id);
// Only functions can be accessed from restricted scopes
// TODO - Only make applicable to functions that cannot be reassigned
Symbol potentialFunction = outer.getVariableUnrestricted(id);
if(!(potentialFunction.type instanceof Type.Function)) throw new NonexistentSymbolException();
return potentialFunction;
}
private Symbol getVariableUnrestricted(String id) throws NonexistentSymbolException {
Symbol symbol = symbolTable.get(id);
if(symbol != null) return symbol;
if(outer == null) throw new NonexistentSymbolException();
return outer.getVariableUnrestricted(id);
}
public void put(String id, Symbol symbol) throws SymbolAlreadyExistsException {
if(symbolTable.containsKey(id)) throw new SymbolAlreadyExistsException();
symbolTable.put(id, symbol);
}
public static abstract class Symbol {
public final Type type;
public Symbol(Type type) {
this.type = type;
}
public static class Variable extends Symbol {
public Variable(Type type) {
super(type);
}
}
}
public static class ScopeException extends Exception {
public static class SymbolAlreadyExistsException extends ScopeException {
}
public static class NonexistentSymbolException extends ScopeException {
}
}
}
@@ -0,0 +1,22 @@
package com.dfsek.terra.addons.terrascript.v2;
import java.util.ArrayList;
import java.util.List;
import com.dfsek.terra.addons.terrascript.v2.exception.CompilationException;
public class ErrorHandler {
private final List<CompilationException> exceptions = new ArrayList<>();
public void add(CompilationException e) {
exceptions.add(e);
}
public void throwAny() throws CompilationException {
for(CompilationException e : exceptions) {
throw e;
}
}
}
@@ -0,0 +1,26 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.terrascript.v2;
import com.dfsek.terra.addons.manifest.api.AddonInitializer;
import com.dfsek.terra.api.Platform;
import com.dfsek.terra.api.addon.BaseAddon;
import com.dfsek.terra.api.inject.annotations.Inject;
public class TerraScript2Addon implements AddonInitializer {
@Inject
private Platform platform;
@Inject
private BaseAddon addon;
@Override
public void initialize() {
}
}
@@ -0,0 +1,186 @@
package com.dfsek.terra.addons.terrascript.v2;
import com.google.common.collect.Streams;
import javax.annotation.Nullable;
import java.util.List;
import com.dfsek.terra.addons.terrascript.v2.codegen.CodegenType;
import com.dfsek.terra.addons.terrascript.v2.codegen.NativeFunction;
import com.dfsek.terra.api.util.generic.pair.Pair;
public interface Type {
Type NUMBER = new Type() {
@Override
public java.lang.reflect.Type javaType() {
return double.class;
}
@Override
public CodegenType getCodegenType() {
return CodegenType.DOUBLE;
}
@Override
public String toString() {
return "num";
}
};
Type INTEGER = new Type() {
@Override
public java.lang.reflect.Type javaType() {
return int.class;
}
@Override
public CodegenType getCodegenType() {
return CodegenType.INTEGER;
}
@Override
public String toString() {
return "int";
}
};
Type STRING = new Type() {
@Override
public java.lang.reflect.Type javaType() {
return String.class;
}
@Override
public CodegenType getCodegenType() {
return CodegenType.STRING;
}
@Override
public String toString() {
return "str";
}
};
Type BOOLEAN = new Type() {
@Override
public java.lang.reflect.Type javaType() {
return boolean.class;
}
@Override
public CodegenType getCodegenType() {
return CodegenType.BOOLEAN;
}
@Override
public String toString() {
return "bool";
}
};
Type VOID = new Type() {
@Override
public java.lang.reflect.Type javaType() {
return void.class;
}
@Override
public CodegenType getCodegenType() {
return CodegenType.VOID;
}
@Override
public String toString() {
return "()";
}
};
static Type fromString(String lexeme) throws TypeException {
return switch(lexeme) {
case "num" -> NUMBER;
case "int" -> INTEGER;
case "str" -> STRING;
case "bool" -> BOOLEAN;
case "()" -> VOID;
default -> throw new TypeException();
};
}
java.lang.reflect.Type javaType();
default boolean typeOf(Type type) {
return this.equals(type);
}
CodegenType getCodegenType();
class Function implements Type {
private final Type returnType;
private final List<Type> parameters;
private final String id;
public Function(Type returnType, List<Type> parameters, @Nullable String identifier, Environment declarationScope) {
this.returnType = returnType;
this.parameters = parameters;
this.id = identifier == null ? "ANONYMOUS" : identifier + declarationScope.name;
}
private static boolean paramsAreSubtypes(List<Type> subtypes, List<Type> superTypes) {
if(subtypes.size() != superTypes.size()) return false;
return Streams.zip(subtypes.stream(), superTypes.stream(), Pair::of).allMatch(p -> p.getLeft().typeOf(p.getRight()));
}
public Type getReturnType() {
return returnType;
}
public List<Type> getParameters() {
return parameters;
}
public String getId() {
return id;
}
@Override
public boolean typeOf(Type type) {
if(!(type instanceof Function function)) return false;
return returnType.typeOf(function.returnType) && paramsAreSubtypes(parameters, function.parameters);
}
@Override
public CodegenType getCodegenType() {
return CodegenType.OBJECT;
}
@Override
public java.lang.reflect.Type javaType() {
return Function.class;
}
@Override
public String toString() {
return "(" + String.join(",", parameters.stream().map(Object::toString).toList()) + ") -> " + returnType;
}
public static class Native extends Function {
private final NativeFunction nativeFunction;
public Native(Type returnType, List<Type> parameters, @org.jetbrains.annotations.Nullable String identifier,
Environment declarationScope, NativeFunction nativeFunction) {
super(returnType, parameters, identifier, declarationScope);
this.nativeFunction = nativeFunction;
}
public NativeFunction getNativeFunction() {
return nativeFunction;
}
}
}
class TypeException extends Exception {
}
}
@@ -0,0 +1,124 @@
package com.dfsek.terra.addons.terrascript.v2.codegen;
import org.objectweb.asm.Opcodes;
public class CodegenType {
public static final CodegenType BOOLEAN = new CodegenType(InstructionType.INTEGER, "Z");
public static final CodegenType STRING = new CodegenType(InstructionType.OBJECT, "Ljava/lang/String;");
public static final CodegenType DOUBLE = new CodegenType(InstructionType.DOUBLE, "D");
public static final CodegenType INTEGER = new CodegenType(InstructionType.INTEGER, "I");
public static final CodegenType VOID = new CodegenType(InstructionType.VOID, "V");
public static final CodegenType OBJECT = new CodegenType(InstructionType.OBJECT, "Ljava/lang/Object;");
private final InstructionType instructionType;
private final String descriptor;
public CodegenType(InstructionType instructionType, String descriptor) {
this.instructionType = instructionType;
this.descriptor = descriptor;
}
public InstructionType bytecodeType() {
return instructionType;
}
public String getDescriptor() {
return descriptor;
}
public enum InstructionType {
DOUBLE {
@Override
public int slotSize() {
return 2;
}
@Override
public int returnInsn() {
return Opcodes.DRETURN;
}
@Override
public int loadInsn() {
return Opcodes.DLOAD;
}
@Override
public int storeInsn() {
return Opcodes.DSTORE;
}
},
OBJECT {
@Override
public int slotSize() {
return 1;
}
@Override
public int returnInsn() {
return Opcodes.ARETURN;
}
@Override
public int loadInsn() {
return Opcodes.ALOAD;
}
@Override
public int storeInsn() {
return Opcodes.ASTORE;
}
},
INTEGER {
@Override
public int slotSize() {
return 1;
}
@Override
public int returnInsn() {
return Opcodes.IRETURN;
}
@Override
public int loadInsn() {
return Opcodes.ILOAD;
}
@Override
public int storeInsn() {
return Opcodes.ISTORE;
}
},
VOID {
@Override
public int slotSize() {
throw new UnsupportedOperationException();
}
@Override
public int returnInsn() {
return Opcodes.RETURN;
}
@Override
public int loadInsn() {
throw new UnsupportedOperationException();
}
@Override
public int storeInsn() {
throw new UnsupportedOperationException();
}
};
public abstract int slotSize();
public abstract int returnInsn();
public abstract int loadInsn();
public abstract int storeInsn();
}
}
@@ -0,0 +1,86 @@
package com.dfsek.terra.addons.terrascript.v2.codegen;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.dfsek.terra.addons.terrascript.v2.Type;
public interface NativeFunction {
Map<String, NativeFunction> BUILTIN_FUNCTIONS = new HashMap<>() {{
put("print", new StaticMethodOfStaticField(
"java/lang/System",
"out",
"java/io/PrintStream",
"println",
"(Ljava/lang/String;)V",
Type.VOID,
List.of(Type.STRING))
);
put("printNum", new StaticMethodOfStaticField(
"java/lang/System",
"out",
"java/io/PrintStream",
"println",
"(D)V",
Type.VOID,
List.of(Type.NUMBER))
);
}};
void pushInstance(MethodVisitor method);
void callMethod(MethodVisitor method);
Type getReturnType();
List<Type> getParameterTypes();
class StaticMethodOfStaticField implements NativeFunction {
private final String fieldOwner;
private final String fieldName;
private final String className;
private final String methodName;
private final String methodDescriptor;
private final Type returnType;
private final List<Type> parameters;
// TODO - Use reflection to obtain these automatically
public StaticMethodOfStaticField(String fieldOwner, String fieldName, String className, String methodName, String methodDescriptor,
Type returnType, List<Type> parameters) {
this.fieldOwner = fieldOwner;
this.fieldName = fieldName;
this.className = className;
this.methodName = methodName;
this.methodDescriptor = methodDescriptor;
this.returnType = returnType;
this.parameters = parameters;
}
@Override
public void pushInstance(MethodVisitor method) {
method.visitFieldInsn(Opcodes.GETSTATIC, fieldOwner, fieldName, "L" + className + ";");
}
@Override
public void callMethod(MethodVisitor method) {
method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, className, methodName, methodDescriptor, false);
}
@Override
public Type getReturnType() {
return returnType;
}
@Override
public List<Type> getParameterTypes() {
return parameters;
}
}
}
@@ -0,0 +1,8 @@
package com.dfsek.terra.addons.terrascript.v2.codegen;
public interface TerraScript {
void execute();
}
@@ -0,0 +1,16 @@
package com.dfsek.terra.addons.terrascript.v2.codegen.asm;
public class DynamicClassLoader extends ClassLoader {
public DynamicClassLoader(Class<?> clazz) {
super(clazz.getClassLoader());
}
public Class<?> defineClass(String name, byte[] data) {
return defineClass(name, data, 0, data.length);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
return Class.forName(name);
}
}
@@ -0,0 +1,17 @@
package com.dfsek.terra.addons.terrascript.v2.codegen.asm;
import org.objectweb.asm.Opcodes;
public class OpcodeAlias {
public static int CMP_GREATER_THAN = Opcodes.IFGT;
public static int CMP_GREATER_EQUALS = Opcodes.IFGE;
public static int CMP_LESS_THAN = Opcodes.IFLT;
public static int CMP_LESS_EQUALS = Opcodes.IFLE;
public static int CMP_EQUALS = Opcodes.IFEQ;
public static int CMP_NOT_EQUALS = Opcodes.IFNE;
public static int BOOL_FALSE = Opcodes.IFEQ;
public static int BOOL_TRUE = Opcodes.IFNE;
public static int INTEGERS_EQUAL = Opcodes.IF_ICMPEQ;
public static int INTEGERS_NOT_EQUAL = Opcodes.IF_ICMPNE;
}
@@ -0,0 +1,565 @@
package com.dfsek.terra.addons.terrascript.v2.codegen.asm;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.commons.LocalVariablesSorter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import com.dfsek.terra.addons.terrascript.v2.Type;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedExpr;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedExpr.Assignment;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedExpr.Binary;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedExpr.Call;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedExpr.Grouping;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedExpr.Literal;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedExpr.Unary;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedExpr.Variable;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedExpr.Void;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt.Block;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt.Break;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt.Continue;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt.Expression;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt.For;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt.FunctionDeclaration;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt.If;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt.NoOp;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt.Return;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt.VariableDeclaration;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt.While;
import com.dfsek.terra.addons.terrascript.v2.codegen.CodegenType;
import com.dfsek.terra.addons.terrascript.v2.codegen.CodegenType.InstructionType;
import com.dfsek.terra.addons.terrascript.v2.codegen.NativeFunction;
import com.dfsek.terra.addons.terrascript.v2.codegen.TerraScript;
import com.dfsek.terra.addons.terrascript.v2.exception.CompilerBugException;
import com.dfsek.terra.addons.terrascript.v2.util.ASMUtil;
import com.dfsek.terra.api.util.generic.pair.Pair;
import static com.dfsek.terra.addons.terrascript.v2.util.ASMUtil.dynamicName;
public class TerraScriptClassGenerator {
private static final Class<?> TARGET_CLASS = TerraScript.class;
private static final boolean DUMP = true;
private final String debugPath;
private int generationCount = 0;
public TerraScriptClassGenerator(String debugPath) {
this.debugPath = debugPath;
}
/**
* @param root Assumed to be semantically correct
*
* @return Generated TerraScript instance
*
* @throws IOException
*/
public TerraScript generate(Block root) throws IOException {
String targetClassName = dynamicName(TARGET_CLASS);
String generatedClassName = targetClassName + "_GENERATED_" + generationCount;
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
// Create class
classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, generatedClassName, null, "java/lang/Object", new String[]{ targetClassName });
// Generate constructor method
MethodVisitor constructor = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
constructor.visitCode();
constructor.visitVarInsn(Opcodes.ALOAD, 0); // Put this reference on stack
constructor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
constructor.visitInsn(Opcodes.RETURN); // Void return
constructor.visitMaxs(0, 0);
constructor.visitEnd();
// Generate execute method
String methodName = "execute";
// Extract method description
MethodExtractor extractor = new MethodExtractor(methodName);
new ClassReader(targetClassName).accept(extractor, 0);
String description = extractor.methodDescription;
int exeAcc = Opcodes.ACC_PUBLIC;
MethodVisitor executeMethod = classWriter.visitMethod(exeAcc, methodName, description, null, null);
executeMethod.visitCode(); // Start method body
new MethodBytecodeGenerator(classWriter, generatedClassName, executeMethod, exeAcc, description).generate(
root); // Generate bytecode
// Finish up method
executeMethod.visitInsn(Opcodes.RETURN);
executeMethod.visitMaxs(0, 0);
executeMethod.visitEnd();
// Finished generating class
classWriter.visitEnd();
DynamicClassLoader loader = new DynamicClassLoader(
TARGET_CLASS); // Instantiate a new loader every time so classes can be GC'ed when they are no longer used. (Classes
// cannot be GC'ed until their loaders are).
generationCount++;
byte[] bytecode = classWriter.toByteArray();
Class<?> generatedClass = loader.defineClass(generatedClassName.replace('/', '.'), bytecode);
if(DUMP) {
File dump = new File(debugPath + "/" + generatedClass.getSimpleName() + ".class");
dump.getParentFile().mkdirs();
try(FileOutputStream out = new FileOutputStream(dump)) {
out.write(bytecode);
} catch(IOException e) {
e.printStackTrace();
}
}
try {
Object instance = generatedClass.getDeclaredConstructor().newInstance();
return (TerraScript) instance;
} catch(ReflectiveOperationException e) {
throw new Error(e); // Should literally never happen
}
}
private static class MethodBytecodeGenerator implements TypedStmt.Visitor<Void>, TypedExpr.Visitor<Void> {
private final ClassWriter classWriter;
private final String className;
private final MethodVisitor method;
private final String descriptor;
private final LocalVariablesSorter lvs;
private final Map<String, Integer> lvTable = new HashMap<>();
private final Deque<Pair<Label, Label>> loopStack = new ArrayDeque<>();
public MethodBytecodeGenerator(ClassWriter classWriter, String className, MethodVisitor method, int access, String descriptor) {
this.classWriter = classWriter;
this.className = className;
this.method = method;
this.descriptor = descriptor;
this.lvs = new LocalVariablesSorter(access, descriptor, method);
}
private static boolean exprTypesEqual(Type type, TypedExpr... exprs) {
for(TypedExpr expr : exprs) {
if(expr.type != type) return false;
}
return true;
}
public void generate(Block root) {
this.visitBlockTypedStmt(root);
}
@Override
public Void visitBinaryTypedExpr(Binary expr) {
switch(expr.operator) {
case EQUALS, NOT_EQUALS, BOOLEAN_AND, BOOLEAN_OR, GREATER, GREATER_EQUALS, LESS, LESS_EQUALS -> pushComparisonBool(expr);
case ADD -> {
pushBinaryOperands(expr);
CodegenType codegenType = expr.type.getCodegenType();
if(codegenType.bytecodeType() == InstructionType.DOUBLE)
method.visitInsn(Opcodes.DADD);
else if(Objects.equals(codegenType.getDescriptor(), "Ljava/lang/String;"))
// TODO - Optimize string concatenation
method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "concat",
"(Ljava/lang/String;)Ljava/lang/String;", false);
else throw new RuntimeException("Could not generate bytecode for ADD binary operator returning type " + expr.type);
}
case SUBTRACT -> binaryInsn(expr, Opcodes.DSUB);
case MULTIPLY -> binaryInsn(expr, Opcodes.DMUL);
case DIVIDE -> binaryInsn(expr, Opcodes.DDIV);
default -> throw new RuntimeException("Unhandled binary operator " + expr.operator);
}
return null;
}
@Override
public Void visitGroupingTypedExpr(Grouping expr) {
expr.expression.accept(this);
return null;
}
@Override
public Void visitLiteralTypedExpr(Literal expr) {
if(expr.type.getCodegenType() == CodegenType.BOOLEAN)
if((boolean) expr.value) pushTrue();
else pushFalse();
else method.visitLdcInsn(expr.value);
return null;
}
@Override
public Void visitUnaryTypedExpr(Unary expr) {
expr.operand.accept(this);
switch(expr.operator) {
case NOT -> invertBool();
case NEGATE -> method.visitInsn(Opcodes.DNEG);
}
return null;
}
@Override
public Void visitCallTypedExpr(Call expr) {
// TODO - Remove specific handling of native functions
if(expr.callee.type instanceof Type.Function.Native nativeFunction) {
NativeFunction function = nativeFunction.getNativeFunction();
function.pushInstance(method);
expr.arguments.forEach(a -> a.accept(this));
function.callMethod(method);
return null;
}
// TODO - Add support for invokevirtual
expr.arguments.forEach(a -> a.accept(this));
List<Type> parameters = expr.arguments.stream().map(e -> e.type).toList();
method.visitMethodInsn(Opcodes.INVOKESTATIC, className, ((Type.Function) expr.callee.type).getId(),
getFunctionDescriptor(parameters, expr.type), false);
return null;
}
@Override
public Void visitVariableTypedExpr(Variable expr) {
Type varType = expr.type;
method.visitVarInsn(varType.getCodegenType().bytecodeType().loadInsn(), lvTable.get(expr.identifier));
return null;
}
@Override
public Void visitAssignmentTypedExpr(Assignment expr) {
expr.rValue.accept(this);
Type type = expr.lValue.type;
method.visitVarInsn(type.getCodegenType().bytecodeType().storeInsn(), lvTable.get(expr.lValue.identifier));
return null;
}
@Override
public Void visitVoidTypedExpr(Void expr) {
return null;
}
@Override
public Void visitExpressionTypedStmt(Expression stmt) {
stmt.expression.accept(this);
return null;
}
@Override
public Void visitBlockTypedStmt(Block stmt) {
stmt.statements.forEach(s -> s.accept(this));
return null;
}
/**
* Writes function as a private static method of the current class
*
* @param stmt
*
* @return
*/
@Override
public Void visitFunctionDeclarationTypedStmt(FunctionDeclaration stmt) {
List<Type> parameterTypes = stmt.parameters.stream().map(Pair::getRight).toList();
int access = Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC;
MethodVisitor method = classWriter.visitMethod(access, stmt.scopedIdentifier,
getFunctionDescriptor(parameterTypes, stmt.returnType), null, null);
method.visitCode(); // Start method body
MethodBytecodeGenerator funcGenerator = new MethodBytecodeGenerator(classWriter, className, method, access, descriptor);
// Add local variable indexes for each parameter
int lvidx = 0;
for(Pair<String, Type> parameter : stmt.parameters) {
funcGenerator.lvTable.put(parameter.getLeft(), lvidx);
lvidx += parameter.getRight().getCodegenType().bytecodeType().slotSize(); // Increment by how many slots data type takes
}
// Generate method bytecode
funcGenerator.generate(stmt.body);
// Finish up
method.visitInsn(Opcodes.RETURN);
method.visitMaxs(0, 0);
method.visitEnd();
return null;
}
@Override
public Void visitVariableDeclarationTypedStmt(VariableDeclaration stmt) {
stmt.value.accept(this);
lvTable.put(stmt.identifier, lvs.newLocal(ASMUtil.tsTypeToAsmType(stmt.type)));
method.visitVarInsn(stmt.type.getCodegenType().bytecodeType().storeInsn(), lvTable.get(stmt.identifier));
return null;
}
@Override
public Void visitReturnTypedStmt(Return stmt) {
stmt.value.accept(this);
method.visitInsn(stmt.value.type.getCodegenType().bytecodeType().returnInsn());
return null;
}
@Override
public Void visitIfTypedStmt(If stmt) {
Label endIf = new Label();
conditionalStmt(stmt.condition, stmt.trueBody, endIf);
for(Pair<TypedExpr, Block> elseIfClause : stmt.elseIfClauses) {
conditionalStmt(elseIfClause.getLeft(), elseIfClause.getRight(), endIf);
}
stmt.elseBody.ifPresent(b -> b.accept(this));
label(endIf);
return null;
}
@Override
public Void visitForTypedStmt(For stmt) {
Label loopStart = new Label();
Label loopBody = new Label();
Label loopEnd = new Label();
stmt.initializer.accept(this);
jump(loopBody); // Skip over incrementer on first loop
label(loopStart);
stmt.incrementer.accept(this);
label(loopBody);
loopStack.push(Pair.of(loopStart, loopEnd));
conditionalStmt(stmt.condition, stmt.body, loopStart);
loopStack.pop();
label(loopEnd);
return null;
}
@Override
public Void visitWhileTypedStmt(While stmt) {
Label loopStart = new Label();
Label loopEnd = new Label();
label(loopStart);
loopStack.push(Pair.of(loopStart, loopEnd));
conditionalStmt(stmt.condition, stmt.body, loopStart);
loopStack.pop();
label(loopEnd);
return null;
}
@Override
public Void visitNoOpTypedStmt(NoOp stmt) {
return null;
}
@Override
public Void visitBreakTypedStmt(Break stmt) {
jump(loopStack.getFirst().getRight());
return null;
}
@Override
public Void visitContinueTypedStmt(Continue stmt) {
jump(loopStack.getFirst().getLeft());
return null;
}
private boolean binaryOperandsSameType(Type type, Binary expr) {
return exprTypesEqual(type, expr.left, expr.right);
}
/**
* Inverts a boolean on the stack
*/
private void invertBool() {
Label invertToFalse = new Label();
Label finished = new Label();
jumpIf(OpcodeAlias.BOOL_TRUE, invertToFalse);
pushFalse();
jump(finished);
label(invertToFalse);
pushFalse();
label(finished);
}
private void pushBinaryOperands(Binary expr) {
expr.left.accept(this);
expr.right.accept(this);
}
private void binaryInsn(Binary expr, int insn) {
pushBinaryOperands(expr);
method.visitInsn(insn);
}
/**
* Pushes boolean on to the stack based on comparison result
*
* @param condition
*/
private void pushComparisonBool(TypedExpr condition) {
Label trueFinished = new Label();
conditionalRunnable(condition, this::pushTrue, trueFinished);
pushFalse();
label(trueFinished);
}
/**
* Executes a statement then jumps to the exit label if the condition is true, jumps over the statement if false
*
* @param condition
* @param stmt
* @param exit
*/
private void conditionalStmt(TypedExpr condition, TypedStmt stmt, Label exit) {
conditionalRunnable(condition, () -> stmt.accept(this), exit);
}
private void pushTrue() {
method.visitInsn(Opcodes.ICONST_1);
}
private void pushFalse() {
method.visitInsn(Opcodes.ICONST_0);
}
private void jumpIf(int opcode, Label label) {
method.visitJumpInsn(opcode, label);
}
private void jump(Label label) {
method.visitJumpInsn(Opcodes.GOTO, label);
}
private void label(Label label) {
method.visitLabel(label);
}
private void conditionalRunnable(TypedExpr condition, Runnable trueBlock, Label trueFinished) {
Label exit = new Label(); // If the first conditional is false, jump over statement and don't execute it
if(condition instanceof Binary binaryCondition) {
switch(binaryCondition.operator) {
case BOOLEAN_AND -> {
// Operands assumed booleans
binaryCondition.left.accept(this);
jumpIf(OpcodeAlias.BOOL_FALSE, exit); // If left is false, short circuit, don't evaluate right
binaryCondition.right.accept(this);
jumpIf(OpcodeAlias.BOOL_FALSE, exit);
}
case BOOLEAN_OR -> {
Label skipRight = new Label();
// Operands assumed booleans
binaryCondition.left.accept(this);
jumpIf(OpcodeAlias.BOOL_TRUE, skipRight);
binaryCondition.right.accept(this);
jumpIf(OpcodeAlias.BOOL_FALSE, exit);
label(skipRight);
}
case EQUALS -> {
if(binaryOperandsSameType(Type.BOOLEAN, binaryCondition)) { // Operands assumed integers
pushBinaryOperands(binaryCondition);
jumpIf(OpcodeAlias.INTEGERS_NOT_EQUAL, exit);
} else if(binaryOperandsSameType(Type.NUMBER, binaryCondition)) { // Operands assumed doubles
binaryInsn(binaryCondition, Opcodes.DCMPG);
jumpIf(OpcodeAlias.CMP_NOT_EQUALS, exit);
} else if(binaryOperandsSameType(Type.STRING, binaryCondition)) {
pushBinaryOperands(binaryCondition);
method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z", false);
jumpIf(OpcodeAlias.BOOL_FALSE, exit);
} else throw new CompilerBugException();
}
case NOT_EQUALS -> {
if(binaryOperandsSameType(Type.BOOLEAN, binaryCondition)) { // Operands assumed integers
pushBinaryOperands(binaryCondition);
jumpIf(OpcodeAlias.INTEGERS_EQUAL, exit);
} else if(binaryOperandsSameType(Type.NUMBER, binaryCondition)) { // Operands assumed doubles
binaryInsn(binaryCondition, Opcodes.DCMPG);
jumpIf(OpcodeAlias.CMP_EQUALS, exit);
} else if(binaryOperandsSameType(Type.STRING, binaryCondition)) { // Operands assumed references
pushBinaryOperands(binaryCondition);
method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z", false);
invertBool();
jumpIf(OpcodeAlias.CMP_EQUALS, exit);
} else throw new CompilerBugException();
}
case GREATER, GREATER_EQUALS, LESS, LESS_EQUALS -> {
// Left and right assumed double
binaryInsn(binaryCondition, switch(binaryCondition.operator) {
case GREATER, GREATER_EQUALS -> Opcodes.DCMPL;
case LESS, LESS_EQUALS -> Opcodes.DCMPG;
default -> throw new IllegalStateException();
});
jumpIf(switch(binaryCondition.operator) {
case GREATER -> OpcodeAlias.CMP_LESS_EQUALS;
case GREATER_EQUALS -> OpcodeAlias.CMP_LESS_THAN;
case LESS -> OpcodeAlias.CMP_GREATER_EQUALS;
case LESS_EQUALS -> OpcodeAlias.CMP_GREATER_THAN;
default -> throw new IllegalStateException();
}, exit);
}
default -> throw new CompilerBugException();
}
} else {
// Assume condition returns bool
condition.accept(this);
jumpIf(OpcodeAlias.BOOL_FALSE, exit);
}
trueBlock.run();
jump(trueFinished); // Jump to end of statement after execution
label(exit);
}
private String getFunctionDescriptor(List<Type> parameters, Type returnType) {
StringBuilder sb = new StringBuilder().append("(");
parameters.stream().map(parameter -> parameter.getCodegenType().getDescriptor()).forEach(sb::append);
sb.append(")");
sb.append(returnType.getCodegenType().getDescriptor());
return sb.toString();
}
}
private static class MethodExtractor extends ClassVisitor {
private final String methodName;
private String methodDescription;
protected MethodExtractor(String methodName) {
super(Opcodes.ASM9);
this.methodName = methodName;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
if(name.equals(methodName))
methodDescription = descriptor;
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
}
}
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.terrascript.v2.exception;
import java.io.Serial;
import com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition;
public class CompilationException extends Exception {
@Serial
private static final long serialVersionUID = 6744390543046766386L;
private final SourcePosition position;
public CompilationException(String message, SourcePosition position) {
super(message);
this.position = position;
}
@Override
public String getMessage() {
return "Error at " + position + ": " + super.getMessage();
}
public SourcePosition getPosition() {
return position;
}
}
@@ -0,0 +1,5 @@
package com.dfsek.terra.addons.terrascript.v2.exception;
public class CompilerBugException extends RuntimeException {
// TODO - Add message constructor
}
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.terrascript.v2.exception.lexer;
import java.io.Serial;
import com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition;
public class EOFException extends TokenizerException {
@Serial
private static final long serialVersionUID = 3980047409902809440L;
public EOFException(String message, SourcePosition position) {
super(message, position);
}
public EOFException(String message, SourcePosition position, Throwable cause) {
super(message, position, cause);
}
}
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.terrascript.v2.exception.lexer;
import java.io.Serial;
import com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition;
public class FormatException extends TokenizerException {
@Serial
private static final long serialVersionUID = -791308012940744455L;
public FormatException(String message, SourcePosition position) {
super(message, position);
}
public FormatException(String message, SourcePosition position, Throwable cause) {
super(message, position, cause);
}
}
@@ -0,0 +1,28 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.terrascript.v2.exception.lexer;
import java.io.Serial;
import com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition;
import com.dfsek.terra.addons.terrascript.v2.parser.ParseException;
public abstract class TokenizerException extends ParseException {
@Serial
private static final long serialVersionUID = 2792384010083575420L;
public TokenizerException(String message, SourcePosition position) {
super(message, position);
}
public TokenizerException(String message, SourcePosition position, Throwable cause) {
super(message, position, cause);
}
}
@@ -0,0 +1,11 @@
package com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis;
import com.dfsek.terra.addons.terrascript.v2.exception.CompilationException;
import com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition;
public class IdentifierAlreadyDeclaredException extends CompilationException {
public IdentifierAlreadyDeclaredException(String message, SourcePosition position) {
super(message, position);
}
}
@@ -0,0 +1,11 @@
package com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis;
import com.dfsek.terra.addons.terrascript.v2.exception.CompilationException;
import com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition;
public class InvalidArgumentsException extends CompilationException {
public InvalidArgumentsException(String message, SourcePosition position) {
super(message, position);
}
}
@@ -0,0 +1,11 @@
package com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis;
import com.dfsek.terra.addons.terrascript.v2.exception.CompilationException;
import com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition;
public class InvalidCalleeException extends CompilationException {
public InvalidCalleeException(String message, SourcePosition position) {
super(message, position);
}
}
@@ -0,0 +1,11 @@
package com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis;
import com.dfsek.terra.addons.terrascript.v2.exception.CompilationException;
import com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition;
public class InvalidFunctionDeclarationException extends CompilationException {
public InvalidFunctionDeclarationException(String message, SourcePosition position) {
super(message, position);
}
}
@@ -0,0 +1,11 @@
package com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis;
import com.dfsek.terra.addons.terrascript.v2.exception.CompilationException;
import com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition;
public class InvalidTypeException extends CompilationException {
public InvalidTypeException(String message, SourcePosition position) {
super(message, position);
}
}
@@ -0,0 +1,11 @@
package com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis;
import com.dfsek.terra.addons.terrascript.v2.exception.CompilationException;
import com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition;
public class UndefinedReferenceException extends CompilationException {
public UndefinedReferenceException(String message, SourcePosition position) {
super(message, position);
}
}
@@ -0,0 +1,69 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.terrascript.v2.lexer;
import java.util.Objects;
public class Char {
private final char character;
private final SourcePosition position;
public Char(char character, SourcePosition position) {
this.character = character;
this.position = position;
}
public boolean is(char... tests) {
for(char test : tests) {
if(test == character && test != '\0') {
return true;
}
}
return false;
}
@Override
public String toString() {
return Character.toString(character);
}
@Override
public boolean equals(Object o) {
if(this == o) return true;
if(o == null || getClass() != o.getClass()) return false;
Char other = (Char) o;
return character == other.character && Objects.equals(position, other.position);
}
@Override
public int hashCode() {
return Objects.hash(character, position);
}
public char getCharacter() {
return character;
}
public boolean isWhitespace() {
return Character.isWhitespace(character);
}
public boolean isNewLine() {
return character == '\n';
}
public boolean isDigit() {
return Character.isDigit(character);
}
public boolean isEOF() {
return character == '\0';
}
}
@@ -0,0 +1,255 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.terrascript.v2.lexer;
import com.google.common.collect.Sets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import com.dfsek.terra.addons.terrascript.v2.exception.lexer.EOFException;
import com.dfsek.terra.addons.terrascript.v2.exception.lexer.FormatException;
import com.dfsek.terra.addons.terrascript.v2.exception.lexer.TokenizerException;
import com.dfsek.terra.addons.terrascript.v2.lexer.Token.TokenType;
import com.dfsek.terra.addons.terrascript.v2.parser.ParseException;
public class Lexer {
public static final Set<Character> syntaxSignificant = Sets.newHashSet(':', ';', '(', ')', '"', ',', '\\', '=', '{', '}', '+', '-', '*',
'/',
'>', '<', '!'); // Reserved chars
private final LookaheadStream reader;
private Token current;
public Lexer(String data) {
reader = new LookaheadStream(data + '\0');
current = tokenize();
}
public List<Token> analyze() {
List<Token> tokens = new ArrayList<>();
while(hasNext()) {
tokens.add(consumeUnchecked());
}
tokens.add(current()); // Add EOF token
return tokens;
}
/**
* Get the first token.
*
* @return First token
*
* @throws ParseException If token does not exist
*/
public Token current() {
return current;
}
/**
* Consume (get and remove) the first token.
*
* @return First token
*
* @throws ParseException If token does not exist
*/
public Token consume(String wrongTypeMessage, TokenType expected, TokenType... more) {
if(!current.isType(expected) && Arrays.stream(more).noneMatch(t -> t == current.type())) throw new ParseException(wrongTypeMessage,
current.position());
return consumeUnchecked();
}
public Token consumeUnchecked() {
if(current.type() == TokenType.END_OF_FILE) return current;
Token temp = current;
current = tokenize();
return temp;
}
/**
* Whether this {@code Tokenizer} contains additional tokens.
*
* @return {@code true} if more tokens are present, otherwise {@code false}
*/
public boolean hasNext() {
return current.type() != TokenType.END_OF_FILE;
}
private Token tokenize() throws TokenizerException {
consumeWhitespace();
SourcePosition position = reader.getPosition();
// Skip line if comment
while(reader.matchesString("//", true)) skipLine();
// Skip multi line comment
if(reader.matchesString("/*", true)) skipTo("*/");
// Reached end of file
if(reader.current().isEOF()) return new Token(reader.consume().toString(), TokenType.END_OF_FILE, position);
// Check if operator token
if(reader.matchesString("==", true))
return new Token("==", TokenType.EQUALS_EQUALS, position);
if(reader.matchesString("!=", true))
return new Token("!=", TokenType.BANG_EQUALS, position);
if(reader.matchesString(">=", true))
return new Token(">=", TokenType.GREATER_EQUAL, position);
if(reader.matchesString("<=", true))
return new Token("<=", TokenType.LESS_EQUALS, position);
if(reader.matchesString(">", true))
return new Token(">", TokenType.GREATER, position);
if(reader.matchesString("<", true))
return new Token("<", TokenType.LESS, position);
// Check if logical operator
if(reader.matchesString("||", true))
return new Token("||", TokenType.BOOLEAN_OR, position);
if(reader.matchesString("&&", true))
return new Token("&&", TokenType.BOOLEAN_AND, position);
// Check if number
if(isNumberStart()) {
StringBuilder num = new StringBuilder();
while(!reader.current().isEOF() && isNumberLike()) {
num.append(reader.consume().getCharacter());
}
return new Token(num.toString(), TokenType.NUMBER, position);
}
// Check if string literal
if(reader.current().is('"')) {
reader.consume(); // Consume first quote
StringBuilder string = new StringBuilder();
boolean ignoreNext = false;
while((!reader.current().is('"')) || ignoreNext) {
if(reader.current().is('\\') && !ignoreNext) {
ignoreNext = true;
reader.consume();
continue;
} else ignoreNext = false;
if(reader.current().isEOF())
throw new FormatException("No end of string literal found. ", position);
string.append(reader.consume());
}
reader.consume(); // Consume last quote
return new Token(string.toString(), TokenType.STRING, position);
}
if(reader.current().is('('))
return new Token(reader.consume().toString(), TokenType.OPEN_PAREN, position);
if(reader.current().is(')'))
return new Token(reader.consume().toString(), TokenType.CLOSE_PAREN, position);
if(reader.current().is(':'))
return new Token(reader.consume().toString(), TokenType.COLON, position);
if(reader.current().is(';'))
return new Token(reader.consume().toString(), TokenType.STATEMENT_END, position);
if(reader.current().is(','))
return new Token(reader.consume().toString(), TokenType.SEPARATOR, position);
if(reader.current().is('{')) return new Token(reader.consume().toString(), TokenType.BLOCK_BEGIN, position);
if(reader.current().is('}')) return new Token(reader.consume().toString(), TokenType.BLOCK_END, position);
if(reader.current().is('='))
return new Token(reader.consume().toString(), TokenType.ASSIGNMENT, position);
if(reader.current().is('+'))
return new Token(reader.consume().toString(), TokenType.PLUS, position);
if(reader.current().is('-'))
return new Token(reader.consume().toString(), TokenType.MINUS,
position);
if(reader.current().is('*'))
return new Token(reader.consume().toString(), TokenType.STAR,
position);
if(reader.current().is('/'))
return new Token(reader.consume().toString(), TokenType.FORWARD_SLASH, position);
if(reader.current().is('%'))
return new Token(reader.consume().toString(), TokenType.MODULO_OPERATOR, position);
if(reader.current().is('!'))
return new Token(reader.consume().toString(), TokenType.BANG, position);
// Read word
StringBuilder token = new StringBuilder();
while(!reader.current().isEOF() && !isSyntaxSignificant(reader.current().getCharacter())) {
Char c = reader.consume();
if(c.isWhitespace()) break;
token.append(c.getCharacter());
}
String tokenString = token.toString();
// Check if word is a keyword
if(tokenString.equals("true"))
return new Token(tokenString, TokenType.BOOLEAN, position);
if(tokenString.equals("false"))
return new Token(tokenString, TokenType.BOOLEAN, position);
if(tokenString.equals("var"))
return new Token(tokenString, TokenType.VARIABLE, position);
if(tokenString.equals("fun"))
return new Token(tokenString, TokenType.FUNCTION, position);
if(tokenString.equals("if"))
return new Token(tokenString, TokenType.IF_STATEMENT, position);
if(tokenString.equals("else"))
return new Token(tokenString, TokenType.ELSE, position);
if(tokenString.equals("while"))
return new Token(tokenString, TokenType.WHILE_LOOP, position);
if(tokenString.equals("for"))
return new Token(tokenString, TokenType.FOR_LOOP, position);
if(tokenString.equals("return"))
return new Token(tokenString, TokenType.RETURN, position);
if(tokenString.equals("continue"))
return new Token(tokenString, TokenType.CONTINUE, position);
if(tokenString.equals("break"))
return new Token(tokenString, TokenType.BREAK, position);
if(tokenString.equals("fail"))
return new Token(tokenString, TokenType.FAIL, position);
// If not keyword, assume it is an identifier
return new Token(tokenString, TokenType.IDENTIFIER, position);
}
private void skipLine() {
while(!reader.current().isEOF() && !reader.current().isNewLine()) reader.consume();
consumeWhitespace();
}
private void consumeWhitespace() {
while(!reader.current().isEOF() && reader.current().isWhitespace()) reader.consume(); // Consume whitespace.
}
private void skipTo(String s) throws EOFException {
SourcePosition begin = reader.getPosition();
while(!reader.current().isEOF()) {
if(reader.matchesString(s, true)) {
consumeWhitespace();
return;
}
reader.consume();
}
throw new EOFException("Reached end of file without matching '" + s + "'", begin);
}
private boolean isNumberLike() {
return reader.current().isDigit()
|| reader.current().is('_', '.', 'E');
}
private boolean isNumberStart() {
return reader.current().isDigit()
|| reader.current().is('.') && reader.peek().isDigit();
}
public boolean isSyntaxSignificant(char c) {
return syntaxSignificant.contains(c);
}
}
@@ -0,0 +1,84 @@
package com.dfsek.terra.addons.terrascript.v2.lexer;
public class LookaheadStream {
private final String source;
private int index;
private SourcePosition position = new SourcePosition(1, 1);
public LookaheadStream(String source) {
this.source = source;
}
/**
* Get the current character without consuming it.
*
* @return current character
*/
public Char current() {
return new Char(source.charAt(index), position);
}
/**
* Consume and return one character.
*
* @return Character that was consumed.
*/
public Char consume() {
Char consumed = current();
incrementIndex(1);
return consumed;
}
/**
* @return The next character in sequence.
*/
public Char peek() {
int index = this.index + 1;
if(index + 1 >= source.length()) return null;
return new Char(source.charAt(index), getPositionAfter(1));
}
/**
* Determines if the contained sequence of characters matches the string
*
* @param check Input string to check against
* @param consumeIfMatched Whether to consume the string if there is a match
*
* @return If the string matches
*/
public boolean matchesString(String check, boolean consumeIfMatched) {
boolean matches = check.equals(source.substring(index, Math.min(index + check.length(), source.length())));
if(matches && consumeIfMatched) incrementIndex(check.length());
return matches;
}
/**
* @return Current position within the source file
*/
public SourcePosition getPosition() {
return position;
}
private void incrementIndex(int amount) {
position = getPositionAfter(amount);
index = Math.min(index + amount, source.length() - 1);
}
private SourcePosition getPositionAfter(int chars) {
if(chars < 0) throw new IllegalArgumentException("Negative values are not allowed");
int line = position.line();
int column = position.column();
for(int i = index; i < Math.min(index + chars, source.length() - 1); i++) {
if(source.charAt(i) == '\n') {
line++;
column = 0;
}
column++;
}
return new SourcePosition(line, column);
}
}
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.terrascript.v2.lexer;
import java.util.Objects;
public record SourcePosition(int line, int column) {
@Override
public String toString() {
return "line " + line + ", column " + column;
}
@Override
public boolean equals(Object o) {
if(this == o) return true;
if(o == null || getClass() != o.getClass()) return false;
SourcePosition that = (SourcePosition) o;
return line == that.line && column == that.column;
}
@Override
public int hashCode() {
return Objects.hash(line, column);
}
}
@@ -0,0 +1,207 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.terrascript.v2.lexer;
import java.util.Objects;
public class Token {
private final String lexeme;
private final TokenType type;
private final SourcePosition start;
public Token(String lexeme, TokenType type, SourcePosition start) {
this.lexeme = type == TokenType.END_OF_FILE ? "END OF FILE" : lexeme;
this.type = type;
this.start = start;
}
@Override
public boolean equals(Object o) {
if(this == o) return true;
if(o == null || getClass() != o.getClass()) return false;
Token token = (Token) o;
return Objects.equals(lexeme, token.lexeme) && type == token.type && Objects.equals(start, token.start);
}
@Override
public int hashCode() {
return Objects.hash(lexeme, type, start);
}
@Override
public String toString() {
return type + ": '" + lexeme + "'";
}
public TokenType type() {
return type;
}
public String lexeme() {
return lexeme;
}
public SourcePosition position() {
return start;
}
public boolean isType(TokenType... types) {
for(TokenType t : types) if(t == type) return true;
return false;
}
public enum TokenType {
/**
* Function identifier or language keyword
*/
IDENTIFIER,
/**
* Numeric literal
*/
NUMBER,
/**
* String literal
*/
STRING,
/**
* Boolean literal
*/
BOOLEAN,
/**
* Beginning of group
*/
OPEN_PAREN,
/**
* Ending of group
*/
CLOSE_PAREN,
/**
* End of statement
*/
STATEMENT_END,
/**
* Argument separator
*/
SEPARATOR,
/**
* Beginning of code block
*/
BLOCK_BEGIN,
/**
* End of code block
*/
BLOCK_END,
/**
* assignment operator
*/
ASSIGNMENT,
/**
* Boolean equals operator
*/
EQUALS_EQUALS,
/**
* Boolean not equals operator
*/
BANG_EQUALS,
/**
* Boolean greater than operator
*/
GREATER,
/**
* Boolean less than operator
*/
LESS,
/**
* Boolean greater than or equal to operator
*/
GREATER_EQUAL,
/**
* Boolean less than or equal to operator
*/
LESS_EQUALS,
/**
* Addition/concatenation operator
*/
PLUS,
/**
* Subtraction operator
*/
MINUS,
/**
* Multiplication operator
*/
STAR,
/**
* Division operator
*/
FORWARD_SLASH,
/**
* Modulo operator.
*/
MODULO_OPERATOR,
/**
* Boolean not operator
*/
BANG,
/**
* Boolean or
*/
BOOLEAN_OR,
/**
* Boolean and
*/
BOOLEAN_AND,
/**
* Variable declaration
*/
VARIABLE,
/**
* Function declaration
*/
FUNCTION,
COLON,
/**
* If statement declaration
*/
IF_STATEMENT,
/**
* While loop declaration
*/
WHILE_LOOP,
/**
* Return statement
*/
RETURN,
/**
* Continue statement
*/
CONTINUE,
/**
* Break statement
*/
BREAK,
/**
* Fail statement. Like return keyword, but specifies that generation has failed.
*/
FAIL,
/**
* For loop initializer token
*/
FOR_LOOP,
/**
* Else keyword
*/
ELSE,
/**
* End of file
*/
END_OF_FILE
}
}
@@ -0,0 +1,25 @@
package com.dfsek.terra.addons.terrascript.v2.parser;
import com.dfsek.terra.addons.terrascript.v2.lexer.Token;
public enum BinaryOperator {
BOOLEAN_OR(Token.TokenType.BOOLEAN_OR),
BOOLEAN_AND(Token.TokenType.BOOLEAN_AND),
EQUALS(Token.TokenType.EQUALS_EQUALS),
NOT_EQUALS(Token.TokenType.BANG_EQUALS),
GREATER(Token.TokenType.GREATER),
GREATER_EQUALS(Token.TokenType.GREATER_EQUAL),
LESS(Token.TokenType.LESS),
LESS_EQUALS(Token.TokenType.LESS_EQUALS),
ADD(Token.TokenType.PLUS),
SUBTRACT(Token.TokenType.MINUS),
MULTIPLY(Token.TokenType.STAR),
DIVIDE(Token.TokenType.FORWARD_SLASH),
MODULO(Token.TokenType.MODULO_OPERATOR);
public final Token.TokenType tokenType;
BinaryOperator(Token.TokenType tokenType) { this.tokenType = tokenType; }
}
@@ -0,0 +1,38 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package com.dfsek.terra.addons.terrascript.v2.parser;
import java.io.Serial;
import com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition;
public class ParseException extends RuntimeException {
@Serial
private static final long serialVersionUID = 6744390543046766386L;
private final SourcePosition position;
public ParseException(String message, SourcePosition position) {
super(message);
this.position = position;
}
public ParseException(String message, SourcePosition position, Throwable cause) {
super(message, cause);
this.position = position;
}
@Override
public String getMessage() {
return "Error at " + position + ": " + super.getMessage();
}
public SourcePosition getPosition() {
return position;
}
}
@@ -0,0 +1,382 @@
package com.dfsek.terra.addons.terrascript.v2.parser;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import com.dfsek.terra.addons.terrascript.v2.Type;
import com.dfsek.terra.addons.terrascript.v2.Type.TypeException;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Variable;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt.Block;
import com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition;
import com.dfsek.terra.addons.terrascript.v2.lexer.Token;
import com.dfsek.terra.addons.terrascript.v2.lexer.Token.TokenType;
import com.dfsek.terra.api.util.generic.pair.Pair;
/**
* TerraScript recursive descent parser
*/
public class Parser {
private final List<Token> tokens;
private int index = 0;
private Parser(List<Token> tokens) {
if(tokens.stream().noneMatch(t -> t.isType(TokenType.END_OF_FILE)))
throw new IllegalArgumentException("Token list must contain at least one token of type " + TokenType.END_OF_FILE);
this.tokens = tokens;
}
public static Block parse(List<Token> tokens) {
return new Parser(tokens).parseTokens();
}
private Block parseTokens() {
List<Stmt> statements = new ArrayList<>();
while(hasNext()) {
statements.add(statement());
}
if(hasNext()) throw new ParseException("Tokens were remaining after parsing", current().position());
return new Stmt.Block(statements, new SourcePosition(0, 0));
}
private Token current() {
return tokens.get(index);
}
private boolean hasNext() {
return !current().isType(TokenType.END_OF_FILE);
}
private Token consume(String wrongTypeMessage, TokenType expected, TokenType... more) {
if(!current().isType(expected) && Arrays.stream(more).noneMatch(t -> t == current().type())) throw new ParseException(
wrongTypeMessage, current().position());
return consumeUnchecked();
}
public Token consumeUnchecked() {
if(!hasNext()) return current();
Token temp = current();
index++;
return temp;
}
private void consumeStatementEnd(String after) {
consume("Expected ';' after " + after + ", found '" + current().lexeme() + "'", TokenType.STATEMENT_END);
}
private Stmt statement() {
return switch(current().type()) {
case BLOCK_BEGIN -> block();
case FUNCTION -> functionDeclaration();
case VARIABLE -> variableDeclaration();
case RETURN -> returnStmt();
case IF_STATEMENT -> ifStmt();
case FOR_LOOP -> forLoop();
case WHILE_LOOP -> whileLoop();
case BREAK -> breakStmt();
case CONTINUE -> continueStmt();
case STATEMENT_END -> new Stmt.NoOp(consumeUnchecked().position());
default -> expressionStatement();
};
}
private Stmt functionDeclaration() {
SourcePosition position = consume("Expected 'fun' keyword at start of function declaration", TokenType.FUNCTION).position();
String id = consume("Expected identifier after 'fun' keyword for function declaration", TokenType.IDENTIFIER).lexeme();
consume("Expected '(' after function identifier '" + id + "'", TokenType.OPEN_PAREN);
// Parse parameters
List<Pair<String, Type>> params = new ArrayList<>();
while(!current().isType(TokenType.CLOSE_PAREN)) {
Token paramToken = consume("Expected parameter name or ')', found '" + current().lexeme() + "'", TokenType.IDENTIFIER);
String paramId = paramToken.lexeme();
if(params.stream().anyMatch(p -> Objects.equals(p.getLeft(), paramId)))
throw new ParseException("Parameter '" + paramId + "' has already been declared in function '" + id + "'",
paramToken.position());
consume("Expected type declaration after parameter name. Example: '" + paramId + ": <type>'", TokenType.COLON);
Type paramType = typeExpr();
params.add(Pair.of(paramId, paramType));
if(current().isType(TokenType.CLOSE_PAREN)) break;
consume("Expected ',' or ')' after parameter declaration '" + paramId + "' in function '" + id + "'", TokenType.SEPARATOR);
}
Type funcReturn = Type.VOID;
consume("Expected ')' after " + (params.size() == 0 ? "')'" : "parameters") + " in declaration of function '" + id + "'",
TokenType.CLOSE_PAREN);
if(current().isType(TokenType.COLON)) {
consumeUnchecked();
funcReturn = typeExpr();
}
Stmt.Block body = blockOrSingleStatement();
return new Stmt.FunctionDeclaration(id, params, funcReturn, body, position);
}
private Stmt.VariableDeclaration variableDeclaration() {
SourcePosition position = consume("Expected 'var' keyword at start of variable declaration", TokenType.VARIABLE).position();
String id = consume("Expected variable name after type for variable declaration", TokenType.IDENTIFIER).lexeme();
consume("Expected ':' after variable name", TokenType.COLON);
Type type = typeExpr();
consume("Expected '=' following variable type declaration", TokenType.ASSIGNMENT);
Expr expr = expression();
consumeStatementEnd("variable declaration");
return new Stmt.VariableDeclaration(type, id, expr, position);
}
private Type typeExpr() {
Token typeToken = consume("Expected " + TokenType.IDENTIFIER + " specified as variable type", TokenType.IDENTIFIER);
try {
return Type.fromString(typeToken.lexeme());
} catch(TypeException e) {
throw new ParseException("Failed to parse type expression", typeToken.position());
}
}
private Stmt.Return returnStmt() {
SourcePosition position = consume("Expected 'return' keyword, found '" + current().lexeme() + "'", TokenType.RETURN).position();
Expr value = new Expr.Void(position);
if(!current().isType(TokenType.STATEMENT_END))
value = expression();
consumeStatementEnd("return statement");
return new Stmt.Return(value, position);
}
private Stmt.If ifStmt() {
// Parse main if clause
SourcePosition position = consume("Expected 'if' keyword at beginning of if statement", TokenType.IF_STATEMENT).position();
consume("Expected '(' after 'if' keyword", TokenType.OPEN_PAREN);
Expr condition = expression();
consume("Expected ')' after if statement condition", TokenType.CLOSE_PAREN);
Stmt.Block trueBody = blockOrSingleStatement();
// Parse any else clauses
Stmt.Block elseBody = null;
List<Pair<Expr, Stmt.Block>> elseIfClauses = new ArrayList<>();
while(current().isType(TokenType.ELSE)) {
consumeUnchecked(); // Consume else
if(!current().isType(TokenType.IF_STATEMENT)) {
elseBody = blockOrSingleStatement();
break; // Else clause should be last in if statement
}
consumeUnchecked(); // Consume if
consume("Expected '(' after 'else if', e.g. 'if else (<condition>) ...'", TokenType.OPEN_PAREN);
Expr elseIfCondition = expression();
consume("Expected ')' after 'else if' clause, e.g. 'else if (<condition>) ...'", TokenType.CLOSE_PAREN);
Stmt.Block elseIfBody = blockOrSingleStatement();
elseIfClauses.add(Pair.of(elseIfCondition, elseIfBody));
}
return new Stmt.If(condition, trueBody, elseIfClauses, Optional.ofNullable(elseBody), position);
}
private Stmt.For forLoop() {
SourcePosition position = consume("Expected 'for' keyword at beginning of for loop", TokenType.FOR_LOOP).position();
consume("Expected '(' after 'for' keyword", TokenType.OPEN_PAREN);
Stmt initializer = statement();
Expr condition;
if(current().isType(TokenType.STATEMENT_END)) {
condition = new Expr.Literal(true, Type.BOOLEAN,
current().position()); // If no condition is provided, set condition = true
consumeUnchecked();
} else {
condition = expression();
consumeStatementEnd("loop condition");
}
Expr incrementer;
if(current().isType(TokenType.CLOSE_PAREN)) {
incrementer = null;
consumeUnchecked();
} else {
incrementer = expression();
consume("Expected ')' after for loop incrementer", TokenType.CLOSE_PAREN);
}
Stmt.Block body = blockOrSingleStatement();
return new Stmt.For(initializer, condition, incrementer, body, position);
}
private Stmt.While whileLoop() {
SourcePosition position = consume("Expected 'for' keyword at beginning of while loop", TokenType.WHILE_LOOP).position();
consume("Expected '(' after 'while' keyword", TokenType.OPEN_PAREN);
Expr condition = expression();
consume("Expected ')' after while loop condition", TokenType.CLOSE_PAREN);
Stmt.Block body = blockOrSingleStatement();
return new Stmt.While(condition, body, position);
}
private Stmt.Break breakStmt() {
SourcePosition position = consume("Expected 'break' keyword for break statement", TokenType.BREAK).position();
consumeStatementEnd("'break' keyword");
return new Stmt.Break(position);
}
private Stmt.Continue continueStmt() {
SourcePosition position = consume("Expected 'continue' keyword for continue statement", TokenType.CONTINUE).position();
consumeStatementEnd("'continue' keyword");
return new Stmt.Continue(position);
}
private Stmt.Block blockOrSingleStatement() {
if(!current().isType(TokenType.BLOCK_BEGIN)) return new Stmt.Block(List.of(statement()), current().position());
return block();
}
private Stmt.Block block() {
SourcePosition position = consume("Expected '{' at start of block", TokenType.BLOCK_BEGIN).position();
List<Stmt> statements = new ArrayList<>();
while(!current().isType(TokenType.BLOCK_END)) {
statements.add(statement());
}
consume("Expected '}' at end of block", TokenType.BLOCK_END);
return new Stmt.Block(statements, position);
}
private Stmt expressionStatement() {
Expr expression = expression();
consumeStatementEnd("expression statement");
return new Stmt.Expression(expression, expression.position);
}
private Expr expression() {
return assignment();
}
private Expr leftAssociativeBinaryExpression(Supplier<Expr> higherPrecedence, BinaryOperator... operators) {
Expr expr = higherPrecedence.get();
loop:
while(true) {
for(BinaryOperator operator : operators) {
if(current().isType(operator.tokenType)) {
SourcePosition position = consumeUnchecked().position(); // Consume operator token
expr = new Expr.Binary(expr, operator, higherPrecedence.get(), position);
continue loop;
}
}
break; // Break if not any operator
}
return expr;
}
private Expr rightAssociativeBinaryExpression(Supplier<Expr> higherPrecedence, BinaryOperator... operators) {
Expr expr = higherPrecedence.get();
for(BinaryOperator operator : operators) {
if(current().isType(operator.tokenType)) {
SourcePosition position = consumeUnchecked().position(); // Consume operator token
return new Expr.Binary(expr, operator, rightAssociativeBinaryExpression(higherPrecedence, operators), position);
}
}
return expr;
}
private Expr assignment() {
Expr expr = logicOr();
if(current().isType(TokenType.ASSIGNMENT)) {
SourcePosition position = consumeUnchecked().position(); // Consume operator token
if(!(expr instanceof Variable variable)) throw new ParseException("Invalid assignment target", position);
return new Expr.Assignment(variable, assignment(), position);
}
return expr;
}
private Expr logicOr() {
return leftAssociativeBinaryExpression(this::logicAnd, BinaryOperator.BOOLEAN_OR);
}
private Expr logicAnd() {
return leftAssociativeBinaryExpression(this::equality, BinaryOperator.BOOLEAN_AND);
}
private Expr equality() {
return leftAssociativeBinaryExpression(this::comparison, BinaryOperator.EQUALS, BinaryOperator.NOT_EQUALS);
}
private Expr comparison() {
return leftAssociativeBinaryExpression(this::term, BinaryOperator.GREATER, BinaryOperator.GREATER_EQUALS, BinaryOperator.LESS,
BinaryOperator.LESS_EQUALS);
}
private Expr term() {
return leftAssociativeBinaryExpression(this::factor, BinaryOperator.ADD, BinaryOperator.SUBTRACT);
}
private Expr factor() {
return leftAssociativeBinaryExpression(this::unary, BinaryOperator.MULTIPLY, BinaryOperator.DIVIDE, BinaryOperator.MODULO);
}
private Expr unary() {
UnaryOperator[] operators = { UnaryOperator.NOT, UnaryOperator.NEGATE };
for(UnaryOperator operator : operators) {
if(current().isType(operator.tokenType)) {
SourcePosition position = consumeUnchecked().position();
return new Expr.Unary(operator, unary(), position);
}
}
return postfix();
}
private Expr postfix() {
Expr expr = primary();
while(current().isType(TokenType.OPEN_PAREN)) {
expr = call(expr);
}
return expr;
}
private Expr primary() {
Token token = consumeUnchecked();
SourcePosition position = token.position();
return switch(token.type()) {
case NUMBER -> new Expr.Literal(Double.parseDouble(token.lexeme()), Type.NUMBER, position);
case STRING -> new Expr.Literal(token.lexeme(), Type.STRING, position);
case BOOLEAN -> new Expr.Literal(Boolean.parseBoolean(token.lexeme()), Type.BOOLEAN, position);
case IDENTIFIER -> variable(token);
case OPEN_PAREN -> {
if(current().isType(TokenType.CLOSE_PAREN)) {
consumeUnchecked(); // Consume ')'
yield new Expr.Void(position); // () evaluates to void
}
Expr expr = expression();
consume("Expected ')' to close '(' located at " + position, TokenType.CLOSE_PAREN);
yield new Expr.Grouping(expr, position);
}
default -> throw new ParseException("Unexpected token '" + token.lexeme() + "'", position);
};
}
private Expr call(Expr function) {
SourcePosition position = consume("Expected '(' to initiate function call on function", TokenType.OPEN_PAREN).position();
List<Expr> args = new ArrayList<>();
while(!current().isType(TokenType.CLOSE_PAREN)) {
args.add(expression());
if(current().isType(TokenType.CLOSE_PAREN)) break;
consume("Expected ',' or ')' after passed argument in function call", TokenType.SEPARATOR);
}
consume("Expected ')' after " + (args.size() == 0 ? "')'" : "arguments") + " in function call",
TokenType.CLOSE_PAREN);
return new Expr.Call(function, args, position);
}
private Expr variable(Token identifier) {
return new Expr.Variable(identifier.lexeme(), identifier.position());
}
}
@@ -0,0 +1,13 @@
package com.dfsek.terra.addons.terrascript.v2.parser;
import com.dfsek.terra.addons.terrascript.v2.lexer.Token;
public enum UnaryOperator {
NOT(Token.TokenType.BANG),
NEGATE(Token.TokenType.MINUS);
public final Token.TokenType tokenType;
UnaryOperator(Token.TokenType tokenType) { this.tokenType = tokenType; }
}
@@ -0,0 +1,189 @@
package com.dfsek.terra.addons.terrascript.v2.semanticanalysis;
import java.util.List;
import com.dfsek.terra.addons.terrascript.v2.Environment;
import com.dfsek.terra.addons.terrascript.v2.Environment.Symbol;
import com.dfsek.terra.addons.terrascript.v2.ErrorHandler;
import com.dfsek.terra.addons.terrascript.v2.Type;
import com.dfsek.terra.addons.terrascript.v2.Type.Function;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Assignment;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Binary;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Call;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Grouping;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Literal;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Unary;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Variable;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Visitor;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Void;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt;
import com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis.IdentifierAlreadyDeclaredException;
import com.dfsek.terra.api.util.generic.pair.Pair;
import static com.dfsek.terra.addons.terrascript.v2.Environment.ScopeException.SymbolAlreadyExistsException;
public class ScopeAnalyzer implements Visitor<Void>, Stmt.Visitor<Void> {
private final ErrorHandler errorHandler;
private Environment currentScope;
public ScopeAnalyzer(Environment globalScope, ErrorHandler errorHandler) {
this.currentScope = globalScope;
this.errorHandler = errorHandler;
}
@Override
public Void visitBinaryExpr(Binary expr) {
expr.right.accept(this);
expr.left.accept(this);
return null;
}
@Override
public Void visitGroupingExpr(Grouping expr) {
expr.expression.accept(this);
return null;
}
@Override
public Void visitLiteralExpr(Literal expr) {
return null;
}
@Override
public Void visitUnaryExpr(Unary expr) {
expr.operand.accept(this);
return null;
}
@Override
public Void visitCallExpr(Call expr) {
expr.callee.accept(this);
expr.arguments.forEach(e -> e.accept(this));
return null;
}
@Override
public Void visitVariableExpr(Variable expr) {
expr.setScope(currentScope);
return null;
}
@Override
public Void visitAssignmentExpr(Assignment expr) {
expr.lValue.accept(this);
expr.rValue.accept(this);
return null;
}
@Override
public Void visitVoidExpr(Void expr) {
return null;
}
@Override
public Void visitExpressionStmt(Stmt.Expression stmt) {
stmt.expression.accept(this);
return null;
}
@Override
public Void visitBlockStmt(Stmt.Block stmt) {
currentScope = currentScope.lexicalInner();
stmt.statements.forEach(s -> s.accept(this));
currentScope = currentScope.outer();
return null;
}
@Override
public Void visitFunctionDeclarationStmt(Stmt.FunctionDeclaration stmt) {
currentScope = currentScope.functionalInner();
for(Pair<String, Type> param : stmt.parameters) {
try {
currentScope.put(param.getLeft(), new Symbol.Variable(param.getRight()));
} catch(SymbolAlreadyExistsException e) {
throw new IllegalStateException("Formal parameter '" + param.getLeft() + "' defined in '" + stmt.identifier +
"' already exists in the function scope");
}
}
stmt.body.accept(this);
currentScope = currentScope.outer();
try {
List<Type> parameters = stmt.parameters.stream().map(Pair::getRight).toList();
Function function = new Function(stmt.returnType, parameters, stmt.identifier, currentScope);
Symbol.Variable symbol = new Symbol.Variable(function);
stmt.setSymbol(symbol);
currentScope.put(stmt.identifier, symbol);
} catch(SymbolAlreadyExistsException e) {
errorHandler.add(new IdentifierAlreadyDeclaredException("Name '" + stmt.identifier + "' is already defined in this scope",
stmt.position));
}
return null;
}
@Override
public Void visitVariableDeclarationStmt(Stmt.VariableDeclaration stmt) {
stmt.setScope(currentScope);
stmt.value.accept(this);
return null;
}
@Override
public Void visitReturnStmt(Stmt.Return stmt) {
stmt.value.accept(this);
return null;
}
@Override
public Void visitIfStmt(Stmt.If stmt) {
stmt.condition.accept(this);
stmt.trueBody.accept(this);
for(Pair<Expr, Stmt.Block> clause : stmt.elseIfClauses) {
clause.getLeft().accept(this);
clause.getRight().accept(this);
}
stmt.elseBody.ifPresent(b -> b.accept(this));
return null;
}
@Override
public Expr.Void visitForStmt(Stmt.For stmt) {
currentScope = currentScope.loopInner(); // Loop initializer, condition, and incrementer belong to inner scope
stmt.initializer.accept(this);
stmt.condition.accept(this);
stmt.incrementer.accept(this);
stmt.body.accept(this);
currentScope = currentScope.outer();
return null;
}
@Override
public Expr.Void visitWhileStmt(Stmt.While stmt) {
stmt.condition.accept(this);
currentScope = currentScope.loopInner();
stmt.body.accept(this);
currentScope = currentScope.outer();
return null;
}
@Override
public Expr.Void visitNoOpStmt(Stmt.NoOp stmt) {
return null;
}
@Override
public Expr.Void visitBreakStmt(Stmt.Break stmt) {
return null;
}
@Override
public Expr.Void visitContinueStmt(Stmt.Continue stmt) {
return null;
}
}
@@ -0,0 +1,24 @@
package com.dfsek.terra.addons.terrascript.v2.semanticanalysis;
import com.dfsek.terra.addons.terrascript.v2.Environment;
import com.dfsek.terra.addons.terrascript.v2.ErrorHandler;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt;
public class SemanticAnalyzer {
public static TypedStmt.Block analyze(Stmt.Block root, ErrorHandler errorHandler) throws Exception {
new ScopeAnalyzer(Environment.global(), errorHandler).visitBlockStmt(root);
errorHandler.throwAny();
new VariableAnalyzer(errorHandler).visitBlockStmt(root);
errorHandler.throwAny();
TypedStmt.Block checkedRoot = (TypedStmt.Block) new TypeChecker(errorHandler).visitBlockStmt(root);
errorHandler.throwAny();
return checkedRoot;
}
}
@@ -0,0 +1,279 @@
package com.dfsek.terra.addons.terrascript.v2.semanticanalysis;
import java.util.List;
import java.util.Optional;
import com.dfsek.terra.addons.terrascript.v2.ErrorHandler;
import com.dfsek.terra.addons.terrascript.v2.Type;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Assignment;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Binary;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Call;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Grouping;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Literal;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Unary;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Variable;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Visitor;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Void;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedExpr;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt;
import com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis.InvalidArgumentsException;
import com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis.InvalidCalleeException;
import com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis.InvalidFunctionDeclarationException;
import com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis.InvalidTypeException;
import com.dfsek.terra.api.util.generic.pair.Pair;
import static com.dfsek.terra.addons.terrascript.v2.util.OrdinalUtil.ordinalOf;
public class TypeChecker implements Visitor<TypedExpr>, Stmt.Visitor<TypedStmt> {
private final ErrorHandler errorHandler;
TypeChecker(ErrorHandler errorHandler) { this.errorHandler = errorHandler; }
@Override
public TypedExpr visitBinaryExpr(Binary expr) {
TypedExpr left = expr.left.accept(this);
TypedExpr right = expr.right.accept(this);
Type leftType = left.type;
Type rightType = right.type;
Type type = switch(expr.operator) {
case BOOLEAN_OR, BOOLEAN_AND -> {
if(!leftType.typeOf(Type.BOOLEAN) || !rightType.typeOf(Type.BOOLEAN))
errorHandler.add(new InvalidTypeException(
"Both operands of '" + expr.operator + "' operator must be of type '" + Type.BOOLEAN + "', found types '" +
leftType + "' and '" + rightType + "'", expr.position));
yield Type.BOOLEAN;
}
case EQUALS, NOT_EQUALS -> {
if(!leftType.typeOf(rightType)) errorHandler.add(new InvalidTypeException(
"Both operands of equality operator (==) must be of the same type, found mismatched types '" + leftType +
"' and '" + rightType + "'", expr.position));
yield Type.BOOLEAN;
}
case GREATER, GREATER_EQUALS, LESS, LESS_EQUALS -> {
if(!leftType.typeOf(Type.NUMBER) || !rightType.typeOf(Type.NUMBER))
errorHandler.add(new InvalidTypeException(
"Both operands of '" + expr.operator + "' operator must be of type '" + Type.NUMBER + "', found types '" +
leftType + "' and '" + rightType + "'", expr.position));
yield Type.BOOLEAN;
}
case ADD -> {
if(leftType.typeOf(Type.NUMBER) && rightType.typeOf(Type.NUMBER)) {
yield Type.NUMBER;
}
if(leftType.typeOf(Type.STRING) || rightType.typeOf(Type.STRING)) {
yield Type.STRING;
}
errorHandler.add(new InvalidTypeException(
"Addition operands must be either both of type '" + Type.NUMBER + "', or one of type '" + Type.STRING + "'",
expr.position));
yield Type.VOID;
}
case SUBTRACT, MULTIPLY, DIVIDE, MODULO -> {
if(!leftType.typeOf(Type.NUMBER) || !rightType.typeOf(Type.NUMBER))
errorHandler.add(new InvalidTypeException(
"Both operands of '" + expr.operator + "' operator must be of type '" + Type.NUMBER + "', found types '" +
leftType + "' and '" + rightType + "'", expr.position));
yield Type.NUMBER;
}
};
return new TypedExpr.Binary(left, expr.operator, right, type);
}
@Override
public TypedExpr visitGroupingExpr(Grouping expr) {
return expr.expression.accept(this);
}
@Override
public TypedExpr visitLiteralExpr(Literal expr) {
return new TypedExpr.Literal(expr.value, expr.type);
}
@Override
public TypedExpr visitUnaryExpr(Unary expr) {
TypedExpr right = expr.operand.accept(this);
Type type = switch(expr.operator) {
case NOT -> {
if(!right.type.typeOf(Type.BOOLEAN)) throw new RuntimeException();
yield Type.BOOLEAN;
}
case NEGATE -> {
if(!right.type.typeOf(Type.NUMBER)) throw new RuntimeException();
yield Type.NUMBER;
}
};
return new TypedExpr.Unary(expr.operator, right, type);
}
@Override
public TypedExpr visitCallExpr(Call expr) {
TypedExpr function = expr.callee.accept(this);
if(!(function.type instanceof Type.Function functionType)) {
errorHandler.add(
new InvalidCalleeException("Cannot call type '" + function.type + "', only functions can be called", expr.position));
return new TypedExpr.Void(Type.VOID);
}
List<TypedExpr> arguments = expr.arguments.stream().map(a -> a.accept(this)).toList();
List<Type> parameters = functionType.getParameters();
if(arguments.size() != parameters.size())
errorHandler.add(new InvalidArgumentsException(
"Provided " + arguments.size() + " arguments to function call, expected " + parameters.size() + " arguments",
expr.position));
for(int i = 0; i < parameters.size(); i++) {
Type expectedType = parameters.get(i);
Type providedType = arguments.get(i).type;
if(!expectedType.typeOf(providedType))
errorHandler.add(new InvalidTypeException(
ordinalOf(i + 1) + " argument provided for function. Function expects type " + expectedType + ", found " +
providedType + " instead", expr.position));
}
return new TypedExpr.Call(function, arguments, functionType.getReturnType());
}
@Override
public TypedExpr visitVariableExpr(Variable expr) {
return new TypedExpr.Variable(expr.identifier, expr.getSymbol().type);
}
@Override
public TypedExpr visitAssignmentExpr(Assignment expr) {
TypedExpr.Variable left = (TypedExpr.Variable) expr.lValue.accept(this);
TypedExpr right = expr.rValue.accept(this);
Type expected = left.type;
String id = expr.lValue.identifier;
if(!right.type.typeOf(expected))
errorHandler.add(new InvalidTypeException(
"Cannot assign variable '" + id + "' to value of type '" + right.type + "', '" + id + "' is declared with type '" +
expected + "'",
expr.position));
return new TypedExpr.Assignment(left, right, right.type);
}
@Override
public TypedExpr visitVoidExpr(Void expr) {
return new TypedExpr.Void(Type.VOID);
}
@Override
public TypedStmt visitExpressionStmt(Stmt.Expression stmt) {
return new TypedStmt.Expression(stmt.expression.accept(this));
}
@Override
public TypedStmt visitBlockStmt(Stmt.Block stmt) {
return new TypedStmt.Block(stmt.statements.stream().map(s -> s.accept(this)).toList());
}
@Override
public TypedStmt visitFunctionDeclarationStmt(Stmt.FunctionDeclaration stmt) {
TypedStmt.Block body = new TypedStmt.Block(stmt.body.statements.stream().map(s -> s.accept(this)).toList());
boolean hasReturn = alwaysReturns(body, stmt);
if(!stmt.returnType.typeOf(Type.VOID) && !hasReturn) {
errorHandler.add(
new InvalidFunctionDeclarationException("Function body for '" + stmt.identifier + "' does not contain return statement",
stmt.position));
}
return new TypedStmt.FunctionDeclaration(stmt.identifier, stmt.parameters, stmt.returnType, body,
((Type.Function) stmt.getSymbol().type).getId());
}
private boolean alwaysReturns(TypedStmt stmt, Stmt.FunctionDeclaration function) {
if(stmt instanceof TypedStmt.Return ret) {
if(!ret.value.type.typeOf(function.returnType))
errorHandler.add(new InvalidTypeException(
"Return statement must match function's return type. Function '" + function.identifier + "' expects " +
function.returnType + ", found " + ret.value.type + " instead", function.position));
return true;
} else if(stmt instanceof TypedStmt.If ifStmt) {
return alwaysReturns(ifStmt.trueBody, function) &&
ifStmt.elseIfClauses.stream().map(Pair::getRight).allMatch(s -> alwaysReturns(s, function)) &&
ifStmt.elseBody.map(body -> alwaysReturns(body, function)).orElse(
false); // If else body is not defined then statement does not always return
} else if(stmt instanceof TypedStmt.Block block) {
return block.statements.stream().anyMatch(s -> alwaysReturns(s, function));
}
return false;
}
@Override
public TypedStmt visitVariableDeclarationStmt(Stmt.VariableDeclaration stmt) {
TypedExpr value = stmt.value.accept(this);
if(!stmt.type.typeOf(value.type))
errorHandler.add(new InvalidTypeException(
"Type of value assigned to variable '" + stmt.identifier +
"' does not match variable's declared type. Expected type '" +
stmt.type + "', found '" + value.type + "' instead", stmt.position));
return new TypedStmt.VariableDeclaration(stmt.type, stmt.identifier, value);
}
@Override
public TypedStmt visitReturnStmt(Stmt.Return stmt) {
return new TypedStmt.Return(stmt.value.accept(this));
}
@Override
public TypedStmt visitIfStmt(Stmt.If stmt) {
TypedExpr condition = stmt.condition.accept(this);
if(!condition.type.typeOf(Type.BOOLEAN)) errorHandler.add(new InvalidTypeException(
"If statement conditional must be of type '" + Type.BOOLEAN + "', found '" + condition.type + "' instead.", stmt.position));
TypedStmt.Block trueBody = (TypedStmt.Block) stmt.trueBody.accept(this);
List<Pair<TypedExpr, TypedStmt.Block>> elseIfClauses = stmt.elseIfClauses.stream().map(c -> {
TypedExpr clauseCondition = c.getLeft().accept(this);
if(!clauseCondition.type.typeOf(Type.BOOLEAN)) errorHandler.add(new InvalidTypeException(
"Else if clause conditional must be of type '" + Type.BOOLEAN + "', found '" + condition.type + "' instead.",
stmt.position));
return Pair.of(clauseCondition, (TypedStmt.Block) c.getRight().accept(this));
}).toList();
Optional<TypedStmt.Block> elseBody = stmt.elseBody.map(b -> (TypedStmt.Block) b.accept(this));
return new TypedStmt.If(condition, trueBody, elseIfClauses, elseBody);
}
@Override
public TypedStmt visitForStmt(Stmt.For stmt) {
TypedStmt initializer = stmt.initializer.accept(this);
TypedExpr condition = stmt.condition.accept(this);
if(!condition.type.typeOf(Type.BOOLEAN)) errorHandler.add(new InvalidTypeException(
"For statement conditional must be of type '" + Type.BOOLEAN + "', found '" + condition.type + "' instead.",
stmt.position));
TypedExpr incrementer = stmt.incrementer.accept(this);
return new TypedStmt.For(initializer, condition, incrementer, (TypedStmt.Block) stmt.body.accept(this));
}
@Override
public TypedStmt visitWhileStmt(Stmt.While stmt) {
TypedExpr condition = stmt.condition.accept(this);
if(!condition.type.typeOf(Type.BOOLEAN)) errorHandler.add(new InvalidTypeException(
"While statement conditional must be of type '" + Type.BOOLEAN + "', found '" + condition.type + "' instead.",
stmt.position));
return new TypedStmt.While(condition, (TypedStmt.Block) stmt.body.accept(this));
}
@Override
public TypedStmt visitNoOpStmt(Stmt.NoOp stmt) {
return new TypedStmt.NoOp();
}
@Override
public TypedStmt visitBreakStmt(Stmt.Break stmt) {
return new TypedStmt.Break();
}
@Override
public TypedStmt visitContinueStmt(Stmt.Continue stmt) {
return new TypedStmt.Continue();
}
}
@@ -0,0 +1,172 @@
package com.dfsek.terra.addons.terrascript.v2.semanticanalysis;
import com.dfsek.terra.addons.terrascript.v2.Environment.ScopeException.NonexistentSymbolException;
import com.dfsek.terra.addons.terrascript.v2.Environment.ScopeException.SymbolAlreadyExistsException;
import com.dfsek.terra.addons.terrascript.v2.Environment.Symbol;
import com.dfsek.terra.addons.terrascript.v2.ErrorHandler;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Assignment;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Binary;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Call;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Grouping;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Literal;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Unary;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Variable;
import com.dfsek.terra.addons.terrascript.v2.ast.Expr.Void;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt.Block;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt.Break;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt.Continue;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt.Expression;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt.For;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt.FunctionDeclaration;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt.If;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt.NoOp;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt.Return;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt.VariableDeclaration;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt.While;
import com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis.IdentifierAlreadyDeclaredException;
import com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis.UndefinedReferenceException;
import com.dfsek.terra.api.util.generic.pair.Pair;
public class VariableAnalyzer implements Expr.Visitor<Void>, Stmt.Visitor<Void> {
private final ErrorHandler errorHandler;
public VariableAnalyzer(ErrorHandler errorHandler) { this.errorHandler = errorHandler; }
@Override
public Void visitBinaryExpr(Binary expr) {
expr.left.accept(this);
expr.right.accept(this);
return null;
}
@Override
public Void visitGroupingExpr(Grouping expr) {
expr.expression.accept(this);
return null;
}
@Override
public Void visitLiteralExpr(Literal expr) {
return null;
}
@Override
public Void visitUnaryExpr(Unary expr) {
expr.operand.accept(this);
return null;
}
@Override
public Void visitCallExpr(Call expr) {
expr.callee.accept(this);
expr.arguments.forEach(e -> e.accept(this));
return null;
}
@Override
public Void visitVariableExpr(Variable expr) {
String id = expr.identifier;
try {
expr.setSymbol(expr.getScope().getVariable(id));
} catch(NonexistentSymbolException e) {
errorHandler.add(
new UndefinedReferenceException("'" + id + "' not is defined in this scope", expr.position));
}
return null;
}
@Override
public Void visitAssignmentExpr(Assignment expr) {
expr.lValue.accept(this);
expr.rValue.accept(this);
return null;
}
@Override
public Void visitVoidExpr(Void expr) {
return null;
}
@Override
public Void visitExpressionStmt(Expression stmt) {
stmt.expression.accept(this);
return null;
}
@Override
public Void visitBlockStmt(Block stmt) {
stmt.statements.forEach(s -> s.accept(this));
return null;
}
@Override
public Void visitFunctionDeclarationStmt(FunctionDeclaration stmt) {
stmt.body.accept(this);
return null;
}
@Override
public Void visitVariableDeclarationStmt(VariableDeclaration stmt) {
stmt.value.accept(this);
try {
stmt.getScope().put(stmt.identifier, new Symbol.Variable(stmt.type));
} catch(SymbolAlreadyExistsException e) {
errorHandler.add(new IdentifierAlreadyDeclaredException("Name '" + stmt.identifier + "' is already defined in this scope",
stmt.position));
}
return null;
}
@Override
public Void visitReturnStmt(Return stmt) {
stmt.value.accept(this);
return null;
}
@Override
public Void visitIfStmt(If stmt) {
stmt.condition.accept(this);
stmt.trueBody.accept(this);
for(Pair<Expr, Block> clause : stmt.elseIfClauses) {
clause.getLeft().accept(this);
clause.getRight().accept(this);
}
stmt.elseBody.ifPresent(b -> b.accept(this));
return null;
}
@Override
public Void visitForStmt(For stmt) {
stmt.initializer.accept(this);
stmt.condition.accept(this);
stmt.incrementer.accept(this);
stmt.body.accept(this);
return null;
}
@Override
public Void visitWhileStmt(While stmt) {
stmt.condition.accept(this);
stmt.body.accept(this);
return null;
}
@Override
public Void visitNoOpStmt(NoOp stmt) {
return null;
}
@Override
public Void visitBreakStmt(Break stmt) {
return null;
}
@Override
public Void visitContinueStmt(Continue stmt) {
return null;
}
}
@@ -0,0 +1,22 @@
package com.dfsek.terra.addons.terrascript.v2.util;
import com.dfsek.terra.addons.terrascript.v2.Type;
public class ASMUtil {
/**
* Dynamically get name to account for possibility of shading
*
* @param clazz Class instance
*
* @return Internal class name
*/
public static String dynamicName(Class<?> clazz) {
return clazz.getCanonicalName().replace('.', '/');
}
public static org.objectweb.asm.Type tsTypeToAsmType(Type type) {
return org.objectweb.asm.Type.getType((Class<?>) type.javaType());
}
}
@@ -0,0 +1,11 @@
package com.dfsek.terra.addons.terrascript.v2.util;
public class OrdinalUtil {
public static String ordinalOf(int i) {
String[] suffixes = new String[]{ "th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th" };
return switch(i % 100) {
case 11, 12, 13 -> i + "th";
default -> i + suffixes[i % 10];
};
}
}
@@ -0,0 +1,12 @@
schema-version: 1
contributors:
- Terra contributors
id: structure-terrascript-v2
version: @VERSION@
entrypoints:
- "com.dfsek.terra.addons.terrascript.v2.TerraScript2Addon"
website:
issues: https://github.com/PolyhedralDev/Terra/issues
source: https://github.com/PolyhedralDev/Terra
docs: https://terra.polydev.org
license: MIT License
@@ -0,0 +1,118 @@
package codegen;
import org.junit.jupiter.api.Test;
import com.dfsek.terra.addons.terrascript.v2.ErrorHandler;
import com.dfsek.terra.addons.terrascript.v2.ast.Stmt.Block;
import com.dfsek.terra.addons.terrascript.v2.ast.TypedStmt;
import com.dfsek.terra.addons.terrascript.v2.codegen.TerraScript;
import com.dfsek.terra.addons.terrascript.v2.codegen.asm.TerraScriptClassGenerator;
import com.dfsek.terra.addons.terrascript.v2.lexer.Lexer;
import com.dfsek.terra.addons.terrascript.v2.parser.Parser;
import com.dfsek.terra.addons.terrascript.v2.semanticanalysis.SemanticAnalyzer;
public class CodeGenTest {
@Test
public void test() {
testValid("""
printNum(12345);
if (1 == 1) print("Dis is true");
var a: num = 1;
var b: num = 2;
var e: str = "test";
if (a <= b) {
print("a is <= b");
} else {
print("a is not <= b");
}
if (e == "foo") {
print("e is == foo");
} else if (e == "bar") {
print("e is == bar");
} else {
print("e is not foo or bar");
}
if (true && false || (false && true)) {
print("Thin is tru");
} else {
print("Thin is not tru :(");
}
fun loopTwiceThenBreak() {
var i: num = 0;
while (true) {
print("looped");
if (i == 1) break;
i = i + 1;
}
}
print("Should loop twice:");
loopTwiceThenBreak();
retNum();
var bln: bool = true;
print(takesArgs("test", 3, true));
print(retStr());
doStuff("Ayo", "world", true);
fun retNum(): num {
return 3 + 3;
}
fun retBool(): bool {
return true;
}
fun concatThree(a: str, b: str, c: str): str {
fun concatTwo(a: str, b: str): str {
return a + b;
}
return concatTwo(a, b) + c;
}
fun retStr(): str {
fun concatTwo(a: str, b: str): str {
return a + b;
}
var hello: str = "Hell";
hello = concatTwo(hello, "o");
var world: str = "world!";
return concatThree(hello, " ", world);
}
fun takesArgs(a: str, b: num, c: bool): str {
return a;
}
fun doStuff(a: str, b: str, c: bool) {
print("Doing stuff");
if (c) {
print(concatThree(a, " ", b));
} else {
print("c is false");
}
}
""");
}
private void testValid(String validSource) {
try {
Block script = Parser.parse(new Lexer(validSource).analyze());
TypedStmt.Block typedScript = SemanticAnalyzer.analyze(script, new ErrorHandler());
TerraScript ts = new TerraScriptClassGenerator("./build/codegentest").generate(typedScript);
ts.execute();
} catch(Exception e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,32 @@
package lexer;
import org.junit.jupiter.api.Test;
import com.dfsek.terra.addons.terrascript.v2.lexer.Lexer;
import com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition;
import com.dfsek.terra.addons.terrascript.v2.lexer.Token;
import com.dfsek.terra.addons.terrascript.v2.lexer.Token.TokenType;
import static org.junit.jupiter.api.Assertions.*;
public class LexerTest {
private static void tokenTypeTest(String input, TokenType type) {
Lexer lexer = new Lexer(input);
assertEquals(new Token(input, type, new SourcePosition(1, 1)), lexer.current());
}
@Test
public void typeTest() {
tokenTypeTest("identifier", TokenType.IDENTIFIER);
tokenTypeTest("(", TokenType.OPEN_PAREN);
tokenTypeTest(")", TokenType.CLOSE_PAREN);
}
@Test
public void multipleTokensTest() {
Lexer lexer = new Lexer("(3 + 2)");
lexer.analyze().forEach(System.out::println);
}
}
@@ -0,0 +1,85 @@
/*
* Copyright (c) 2020-2021 Polyhedral Development
*
* The Terra Core Addons are licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in this module's root directory.
*/
package lexer;
import org.junit.jupiter.api.Test;
import com.dfsek.terra.addons.terrascript.v2.lexer.Char;
import com.dfsek.terra.addons.terrascript.v2.lexer.LookaheadStream;
import com.dfsek.terra.addons.terrascript.v2.lexer.SourcePosition;
import static org.junit.jupiter.api.Assertions.*;
public class LookaheadStreamTest {
@Test
public void lookahead() {
String testString = "Test string...\nNew line";
LookaheadStream lookahead = new LookaheadStream(testString);
Char first = new Char('T', new SourcePosition(1, 1));
Char second = new Char('e', new SourcePosition(1, 2));
Char third = new Char('s', new SourcePosition(1, 3));
Char space = new Char(' ', new SourcePosition(1, 5));
Char newline = new Char('\n', new SourcePosition(1, 15));
Char lineTwoColOne = new Char('N', new SourcePosition(2, 1));
String lineTwo = "New line";
assertTrue(lookahead.matchesString("Test", false));
assertTrue(lookahead.matchesString(testString, false));
assertFalse(lookahead.matchesString(testString + "asdf", false));
assertFalse(lookahead.matchesString("Foo", false));
assertEquals(first, lookahead.current());
assertEquals(first, lookahead.current());
assertEquals(new SourcePosition(1, 1), lookahead.getPosition());
assertEquals(new SourcePosition(1, 1), lookahead.getPosition());
assertEquals(second, lookahead.peek());
assertEquals(second, lookahead.peek());
assertEquals(first, lookahead.consume());
assertFalse(lookahead.matchesString(testString, false));
assertEquals(second, lookahead.current());
assertEquals(second, lookahead.consume());
assertEquals(third, lookahead.current());
assertTrue(lookahead.matchesString("st", true));
assertEquals(space, lookahead.current());
assertEquals(space, lookahead.consume());
assertTrue(lookahead.matchesString("string...", false));
assertTrue(lookahead.matchesString("string...", true));
assertFalse(lookahead.matchesString("string...", false));
assertEquals(newline, lookahead.current());
assertEquals(newline, lookahead.consume());
assertEquals(lineTwoColOne, lookahead.current());
assertTrue(lookahead.matchesString(lineTwo, false));
assertFalse(lookahead.matchesString(lineTwo + "asdf", false));
assertTrue(lookahead.matchesString(lineTwo, true));
assertEquals(new SourcePosition(2, 8), lookahead.getPosition());
assertDoesNotThrow(lookahead::consume);
assertEquals(new SourcePosition(2, 8), lookahead.getPosition());
assertDoesNotThrow(lookahead::consume);
assertEquals(new SourcePosition(2, 8), lookahead.getPosition());
}
}
@@ -0,0 +1,369 @@
package semanticanalysis;
import org.junit.jupiter.api.Test;
import com.dfsek.terra.addons.terrascript.v2.ErrorHandler;
import com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis.IdentifierAlreadyDeclaredException;
import com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis.InvalidCalleeException;
import com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis.InvalidFunctionDeclarationException;
import com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis.InvalidTypeException;
import com.dfsek.terra.addons.terrascript.v2.exception.semanticanalysis.UndefinedReferenceException;
import com.dfsek.terra.addons.terrascript.v2.lexer.Lexer;
import com.dfsek.terra.addons.terrascript.v2.parser.Parser;
import com.dfsek.terra.addons.terrascript.v2.semanticanalysis.SemanticAnalyzer;
import static org.junit.jupiter.api.Assertions.*;
public class SemanticAnalyzerTest {
@Test
public void testVariableReference() {
// Use of declared variable
testValid("var a: num = 1; a + a;");
// Can't use undeclared variable
testInvalid("a + a;", UndefinedReferenceException.class);
// Can't reference variable before declaration
testInvalid("a + a; var a: num = 1;", UndefinedReferenceException.class);
// Variable declarations shouldn't be accessible from inner scopes
testInvalid("{ var a: num = 1; } a + a;", UndefinedReferenceException.class);
// Can access variables declared in outer scope
testValid("var a: num = 3; { a + a; }");
// Should not be able to use variables from outer scope if they're declared after scope
testInvalid("{ a + a; } var a: num = 2;", UndefinedReferenceException.class);
// Can't use undeclared variable as function argument
testInvalid("fun test(p: str) {} test(a);", UndefinedReferenceException.class);
// Same as above, but in inner scope
testInvalid("fun test(p: str) {} { test(a); }", UndefinedReferenceException.class);
// Cannot assign undeclared variable
testInvalid("a = 1;", UndefinedReferenceException.class);
// Cannot assign to variable declared after assignment
testInvalid("a = 2; var a: num = 1;", UndefinedReferenceException.class);
}
@Test
public void testAssignment() {
// Simple assignment
testValid("var a: num = 1; a = 2;");
// Can assign variables declared in outer scope
testValid("""
var a: num = 1;
{ a = 2; }
""");
// Cannot assign variables declared in inner scope
testInvalid("""
{ var a: num = 1; }
a = 2;
""", UndefinedReferenceException.class);
// Cannot assign variables declared in outer scope after reference
testInvalid("""
{ a = 2; }
var a: num = 1;
""", UndefinedReferenceException.class);
// Cannot assign variable to expression of different type
testInvalid("var a: num = 1; a = true;", InvalidTypeException.class);
}
@Test
public void testReturnStatement() {
// Function return must match signature
testInvalid("fun returnBool(): bool {}", InvalidFunctionDeclarationException.class);
testInvalid("fun returnNum(): num { return \"Not num\"; }", InvalidTypeException.class);
// Return statements can be empty for void return
testValid("fun returnVoid() { return; }");
// Return statement returns type matching function signature
testValid("fun returnNum(): num { return 3; }");
testValid("fun returnVoid() { return (); }");
}
@Test
public void testFunctionReturnControlFlowAnalysis() {
// Non-void returning function bodies must contain at least one statement that always returns
testInvalid("""
fun returnsNum(): num {
}
""", InvalidFunctionDeclarationException.class);
testValid("""
fun returnsNum(): num {
return 1;
}
""");
testValid("""
fun returnsNum(): num {
1 + 1;
return 1;
}
""");
// Statements after the first always-return-statement are unreachable, unreachable code is legal
testValid("""
fun returnsNum(): num {
return 1;
1 + 1; // Unreachable
}
""");
// Void returning functions can omit returns
testValid("""
fun returnsNothing() {
}
""");
// Returns can still be explicitly used for void returning functions
testValid("""
fun returnsNum(p: bool) {
if (p) {
return;
}
}
""");
// If all if-statement bodies always return, then the statement is considered as always returning
testValid("""
fun returnsNum(p: bool): num {
if (p) {
return 1;
} else {
return 0;
}
}
""");
testValid("""
fun returnsNum(p1: bool, p2: bool): num {
if (p1) {
return 1;
} else if (p2) {
return 2;
} else {
return 3;
}
}
""");
// If no else body is defined, an if-statement does not always return, therefore the function does not contain any
// always-return-statements
testInvalid("""
fun returnsNum(p: bool): num {
if (p) {
return 1;
}
}
""", InvalidFunctionDeclarationException.class);
testInvalid("""
fun returnsNum(p1: bool, p2: bool): num {
if (p1) {
return 1;
} else if (p2) {
return 2;
}
}
""", InvalidFunctionDeclarationException.class);
// Nested ifs should work
testValid("""
fun returnsNum(p1: bool, p2: bool): num {
if (p1) {
if (p2) {
return 1;
} else {
return 2;
}
} else {
return 3;
}
}
""");
testInvalid("""
fun returnsNum(p1: bool, p2: bool): num {
if (p1) {
if (p2) {
return 1;
}
// No else clause here, so will not always return
} else {
return 3;
}
}
""", InvalidFunctionDeclarationException.class);
// If-statement may not always return but a return statement after it means function will always return
testValid("""
fun returnsNum(p: bool): num {
if (p) {
return 1;
}
return 2;
}
""");
// Same applies when statements are swapped
testValid("""
fun returnsNum(p: bool): num {
return 1;
// Unreachable
if (p) {
return 2;
}
}
""");
}
@Test
public void testFunctionCall() {
// Simple function declaration then call
testValid("fun test() {}; test();");
// Can be used before declaration
testValid("test(); fun test() {};");
// Can be used from outer scope
testValid("fun test() {}; { test(); }");
// Can be used from outer scope before declaration
testValid("{ test(); } fun test() {};");
// Can be used in many outer scopes
testValid("{{{{{ test(); }}}}} fun test() {};");
// Calling function that hasn't been declared
testInvalid("test();", UndefinedReferenceException.class);
// Cannot call non functions
testInvalid("var test: num = 1; test();", InvalidCalleeException.class);
// Cannot use functions declared in inner scopes
testInvalid("{ fun test() {} } test();", UndefinedReferenceException.class);
testInvalid("test(); { fun test() {} }", UndefinedReferenceException.class);
// Mutual recursion supported
testValid("fun a() { b(); } fun b() { a(); }");
}
@Test
public void testFunctionArgumentPassing() {
// Simple argument passing
testValid("fun test(p: num); test(1);");
// Passing multiple arguments
testValid("fun test(p1: num, p2: bool); test(1, false);");
// Argument type must match parameter type
testInvalid("fun test(p: num); test(false);", InvalidTypeException.class);
// Function return
testValid("""
fun returnBool(): bool {
return true;
}
fun takeBool(p: bool) {}
takeBool(returnBool());
""");
// Should not be able to pass argument of type not matching parameter type
testInvalid("""
fun returnBool(): bool {
return true;
}
fun takeNum(p: num) {}
takeNum(returnBool());
""", InvalidTypeException.class);
}
@Test
public void testParameterUse() {
// Function bodies should be able to use parameter names
testValid("fun test(a: num, b: num) { a + b; }");
testInvalid("fun test(a: num, b: num) { a + c; }", UndefinedReferenceException.class);
// Function bodies can't use variables from outer scope
testInvalid("var a: num = 1; fun doStuff() { a + 2; }", UndefinedReferenceException.class);
testInvalid("fun doStuff() { a + 2; } var a: num = 1;", UndefinedReferenceException.class);
// Type checking parameters
testValid("fun takesNum(a: num) {} fun test(numberParam: num) { takesNum(numberParam); }");
testInvalid("fun takesNum(a: num) {} fun test(boolParam: bool) { takesNum(boolParam); }", InvalidTypeException.class);
}
@Test
public void testShadowing() {
// Can't shadow variable in immediate scope
testInvalid("var a: num = 1; var a: num = 2;", IdentifierAlreadyDeclaredException.class);
// Can shadow variable from outer scope
testValid("var a: num = 1; { var a: num = 2; }");
// Can declare variable after same identifier is used previously in an inner scope
testValid("{ var a: num = 2; } var a: num = 1;");
// Ensure shadowed variable type is used
testValid("""
fun takesNum(p: num) {}
var a: bool = false;
{
var a: num = 1;
takesNum(a);
}
""");
// Should not be able to use type of shadowed variable in use of shadowing variable
testInvalid("""
fun takesNum(p: num) {}
var a: num = false;
{
var a: bool = 1;
takesNum(a);
}
""", InvalidTypeException.class);
// Functions can be shadowed in inner scopes
testValid("""
fun test() {}
{
fun test() {}
}
{
fun test() {}
}
""");
// Functions can't be shadowed in the same immediate scope
testInvalid("""
fun test() {}
fun test() {}
""", IdentifierAlreadyDeclaredException.class);
// Can't use function name that is already declared as a variable
testInvalid("var id: num = 1; fun id() {}", IdentifierAlreadyDeclaredException.class);
}
private <T extends Exception> void testInvalid(String invalidSource, Class<T> exceptionType) {
ErrorHandler errorHandler = new ErrorHandler();
assertThrows(exceptionType, () -> SemanticAnalyzer.analyze(Parser.parse(new Lexer(invalidSource).analyze()), errorHandler));
}
private void testValid(String validSource) {
ErrorHandler errorHandler = new ErrorHandler();
assertDoesNotThrow(() -> SemanticAnalyzer.analyze(Parser.parse(new Lexer(validSource).analyze()), errorHandler));
}
}
@@ -21,6 +21,8 @@ public interface PluginConfig {
boolean isDebugScript();
boolean isDebugLog();
int getBiomeSearchResolution();
int getStructureCache();
@@ -51,6 +51,10 @@ public class PluginConfigImpl implements ConfigTemplate, PluginConfig {
@Default
private boolean debugScript = false;
@Value("debug.log")
@Default
private boolean debugLog = false;
@Value("biome-search-resolution")
@Default
private int biomeSearch = 4;
@@ -91,6 +95,8 @@ public class PluginConfigImpl implements ConfigTemplate, PluginConfig {
logger.info("Debug profiler enabled.");
if(debugScript)
logger.info("Script debug blocks enabled.");
if(debugLog)
logger.info("Debug logging enabled.");
}
@Override
@@ -113,6 +119,11 @@ public class PluginConfigImpl implements ConfigTemplate, PluginConfig {
return debugScript;
}
@Override
public boolean isDebugLog() {
return debugLog;
}
@Override
public int getBiomeSearchResolution() {
return biomeSearch;
@@ -17,10 +17,17 @@
package com.dfsek.terra.config.loaders;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.tectonic.api.config.template.object.ObjectTemplate;
import com.dfsek.tectonic.api.depth.DepthTracker;
import com.dfsek.tectonic.api.exception.LoadException;
import com.dfsek.tectonic.api.loader.ConfigLoader;
import com.dfsek.tectonic.api.loader.type.TypeLoader;
import com.dfsek.tectonic.impl.MapConfiguration;
import com.dfsek.terra.api.config.meta.Meta;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.AnnotatedType;
@@ -36,11 +43,26 @@ public class RangeLoader implements TypeLoader<Range> {
public Range load(@NotNull AnnotatedType type, @NotNull Object o, @NotNull ConfigLoader configLoader, DepthTracker depthTracker)
throws LoadException {
if(o instanceof Map) {
Map<String, Integer> map = (Map<String, Integer>) o;
return new ConstantRange(map.get("min"), map.get("max"));
return configLoader.load(new RangeMapTemplate(), new MapConfiguration((Map<String, Object>) o), depthTracker).get();
} else {
int h = configLoader.loadType(Integer.class, o, depthTracker);
return new ConstantRange(h, h + 1);
}
}
/*
* Template needed so keys can be meta annotated, otherwise the loader could just grab keys directly from the object
*/
public static class RangeMapTemplate implements ObjectTemplate<Range> {
@Value("min")
private @Meta int min;
@Value("max")
private @Meta int max;
@Override
public Range get() {
return new ConstantRange(min, max);
}
}
}
@@ -68,7 +68,7 @@ public class MetaListLikePreprocessor extends MetaPreprocessor<Meta> {
if(!(metaValue instanceof List)) {
throw new LoadException(
"MetaList/Set injection candidate must be list, is type " + metaValue.getClass().getCanonicalName(),
"Meta list / set injection (via <<) must point to a list. '" + meta + "' points to type " + metaValue.getClass().getCanonicalName(),
depthTracker);
}
-1
View File
@@ -8,5 +8,4 @@ terra.license=MIT
# Gradle options
org.gradle.jvmargs=-Xmx4096M -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC
org.gradle.warning.mode=all
#org.gradle.parallel=true
Binary file not shown.
+3 -2
View File
@@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
org.gradle.jvmargs=-Xmx4096m

Some files were not shown because too many files have changed in this diff Show More