include config.make
-java_MODULES=notzed.nativez
+java_MODULES=notzed.nativez notzed.nativez.tools
include java.make
-
+include junit5.make
+include maven.make
JAVA_HOME ?= /usr/local/jdk
JAVAC ?= $(JAVA_HOME)/bin/javac
+JAVA ?= $(JAVA_HOME)/bin/java
JAR ?= $(JAVA_HOME)/bin/jar
JMOD ?= $(JAVA_HOME)/bin/jmod
GCCPLUGINDIR:=$(shell gcc -print-file-name=plugin)
-JAVACFLAGS +=
+JAVACFLAGS +=
# Linux options
linux-amd64_CPPFLAGS = \
# ######################################################################
+MAKEFLAGS += --no-builtin-rules
+
all_MODULES = $(java_MODULES) $(native_MODULES)
E:=
jar:
gen:
-.PHONY: all clean jar gen $(java_MODULES)
+.PHONY: all clean jar gen $(java_MODULES) dist
clean:
rm -rf bin
# setup run-* targets
define run_targets=
run-$1/$2: bin/status/$1.sdk $($1_JDEPMOD:%=bin/status/%.sdk)
- LD_LIBRARY_PATH=$(FFMPEG_HOME)/lib \
$(JAVA) \
$(if $(strip $(JAVAMODPATH) $($1_JAVAMODPATH)),--module-path $(subst $(S),:,$(strip $(JAVAMODPATH) $($1_JAVAMODPATH)))) \
$(JMAINFLAGS) $($1_JMAINFLAGS) \
--- /dev/null
+#
+# Copyright (C) 2025 Michael Zucchi
+#
+# This is the copyright for java.make
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+# ######################################################################
+# junit(5) makefile fragment
+
+# This runs tests names *Test.java or *IT.java in the src/*/tests
+# directories.
+
+# Couldn't work out how netbeans does it, might only do junit4.
+
+# This integrates with java.make and maven.make:
+
+# include java.make
+# include junit5.make
+# include maven.make
+
+maven_central_JARS += org.junit.platform:junit-platform-console-standalone:1.13.4
+
+junit5_module = org.junit.platform.console.standalone
+junit5_launcher = org.junit.platform.console.ConsoleLauncher
+
+dist_EXTRA+=junit5.make
+
+# ######################################################################
+
+define java_tests=
+ifndef $$($1_TEST_JAVA)
+$1_TEST_JAVA := $(shell find src/$1/tests -type f -name '*.java')
+$1_TEST_PACKAGE = $$(subst /,.,$$(patsubst src/$1/tests/%/,%,$$(sort $$(dir $$($1_TEST_JAVA)))))
+endif
+
+ifdef $1_TEST_JAVA
+test: $1-test
+test-it: $1-it
+check: $1-test $1-it
+bin/status/$1.tests: bin/status/$1.classes $$($1_TEST_JAVA)
+.PHONY: $1-test $1-it
+
+bin/status/$1.tests:
+ @install -d $$(@D)
+ $$(JAVAC) \
+ --module-path bin/modules:.lib \
+ --patch-module $1=src/$1/tests \
+ --add-modules $(junit5_module) \
+ --add-reads $1=$(junit5_module),ALL-UNNAMED \
+ --module-source-path 'src/*/tests' \
+ -d bin/tests \
+ $$($1_TEST_JAVA)
+ touch $$@
+
+$1-test: bin/status/$1.tests
+ $$(JAVA) \
+ --module-path bin/modules:.lib \
+ --patch-module $1=bin/tests/$1 \
+ --add-modules $1 \
+ --add-reads $1=$(junit5_module),ALL-UNNAMED \
+ $$(patsubst %,--add-opens $1/%=$(junit5_module),$$($1_TEST_PACKAGE)) \
+ $$(patsubst %,--add-exports $1/%=$(junit5_module),$$($1_TEST_PACKAGE)) \
+ -m $(junit5_module)/$(junit5_launcher) \
+ --scan-modules --include-classname='.*Test'
+
+$1-it: bin/status/$1.tests
+ $$(JAVA) \
+ --module-path bin/modules:.lib \
+ --patch-module $1=bin/tests/$1 \
+ --add-modules $1 \
+ --add-reads $1=$(junit5_module),ALL-UNNAMED \
+ $$(patsubst %,--add-opens $1/%=$(junit5_module),$$($1_TEST_PACKAGE)) \
+ $$(patsubst %,--add-exports $1/%=$(junit5_module),$$($1_TEST_PACKAGE)) \
+ -m $(junit5_module)/$(junit5_launcher) \
+ --scan-modules --include-classname='.*IT'
+endif
+
+endef
+
+.PHONY: test check
+
+$(foreach module,$(java_MODULES),$(if $(wildcard src/$(module)/tests),$(eval $(call java_tests,$(module)))))
+#$(foreach module,$(java_MODULES),$(if $(wildcard src/$(module)/tests),$(info $(call java_tests,$(module)))))
+
+# ######################################################################
--- /dev/null
+#
+# Copyright (C) 2021 Michael Zucchi
+#
+# This is the copyright for maven.make
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+# This lets one download maven packages using simple automake syntax.
+
+# maven_<name>_URL = baseurl
+
+# Define the base url. maven_central_URL is already defined as
+# maven_central_URL:=https://repo1.maven.org/maven2
+
+# maven_<name>_JARS = group:artifact:version group:artifact:version ...
+
+# Define the artifacts required from the given maven repository.
+
+# That's it!
+
+# It defines several make targets.
+
+# make maven-init
+# Will download the jar files.
+
+# make maven-verify
+# Will download and check the signatures using gpg. The public key
+# required for verification must be imported to gpg separately.
+
+# make distclean
+# Will delete .lib
+
+# define maven central
+maven_central_URL:=https://repo1.maven.org/maven2
+maven_repository_URL:=https://mvnrepository.com/artifact
+
+# find out what repositories the makefile defined
+maven_REPOS=$(patsubst maven_%_URL,%,$(filter maven_%_URL,$(.VARIABLES)))
+
+dist_EXTRA+=maven.make
+
+# (group artifact version baseurl)
+define maven_func=
+$2_jar=.lib/$2-$3.jar
+.lib/$2-$3.jar:
+ mkdir -p .lib
+ wget -O $$@ $(4)/$(subst .,/,$1)/$2/$3/$2-$3.jar || ( rm $$@ ; exit 1 )
+.lib/$2-$3.pom:
+ mkdir -p .lib
+ wget -O $$@ $(4)/$(subst .,/,$1)/$2/$3/$2-$3.pom || ( rm $$@ ; exit 1 )
+.lib-sources/$2-$3-sources.jar:
+ mkdir -p .lib-sources
+ wget -O $$@ $(4)/$(subst .,/,$1)/$2/$3/$2-$3-sources.jar || ( rm $$@ ; exit 1 )
+.lib-javadoc/$2-$3-javadoc.jar:
+ mkdir -p .lib-javadoc
+ wget -O $$@ $(4)/$(subst .,/,$1)/$2/$3/$2-$3-javadoc.jar || ( rm $$@ ; exit 1 )
+.lib/$2-$3.jar.asc: .lib/$2-$3.jar
+ wget -O $$@ $(4)/$(subst .,/,$1)/$2/$3/$2-$3.jar.asc
+ gpg --batch --verify $$@ $$< || ( rm $$@ ; echo "GPG verification failed, you may need to import the public key." ; exit 1 )
+maven-init: .lib/$2-$3.jar .lib-sources/$2-$3-sources.jar .lib-javadoc/$2-$3-javadoc.jar .lib/$2-$3.pom
+maven-verify: .lib/$2-$3.jar.asc
+endef
+
+maven-init:
+maven-verify:
+
+.PHONY: maven-init maven-verify
+
+$(foreach repo,$(maven_REPOS),\
+ $(foreach jar,$(maven_$(repo)_JARS), \
+ $(eval $(call maven_func,$(word 1,$(subst :, ,$(jar))),$(word 2,$(subst :, ,$(jar))),$(word 3,$(subst :, ,$(jar))),$(maven_$(repo)_URL)))))
+
+distclean:
+ rm -rf .lib .lib-javadoc .lib-sources
dist.jlink.output=${dist.jlink.dir}/notzed.nativez
endorsed.classpath=
excludes=
+file.reference.junit-platform-console-standalone-1.13.4.jar=.lib/junit-platform-console-standalone-1.13.4.jar
includes=**
jar.compress=false
javac.classpath=
javac.source=24
javac.target=24
javac.test.classpath=\
+ ${file.reference.junit-platform-console-standalone-1.13.4.jar}:\
${javac.classpath}
javac.test.modulepath=\
${javac.modulepath}:\
*/
package au.notzed.nativez.tools;
+import au.notzed.nativez.tools.Value.*;
+import java.io.EOFException;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.function.Function;
+import java.util.function.IntPredicate;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import java.util.stream.Collectors;
import java.util.stream.Stream;
+//Still working on cleanup of API and Template
+//Tokeniser might need a revisit too.
/**
* Loads and processes an .api file.
* <p>
public class API {
/* data loaded from api file */
- List<LibraryInfo> libraries;
- Deque<TemplateMap> types;
- // Expanded code-map with key "name:field"
- Map<String, Text> codes;
- // Expanded system-map with key 'type:name'? Is this useful?
- Map<String, Text> system;
-
- public API(List<LibraryInfo> system, List<LibraryInfo> libraries, Deque<TemplateMap> types, Collection<TemplateMap> codes) {
+ List<Group> libraries;
+ List<Group> structs;
+ // Pattern types
+ List<Type> types;
+ // All code block fields key "name:field"
+ Map<String, Value> codes;
+
+ private API(List<Group> libraries, List<Group> structs, List<Type> types, List<Type> codes) {
this.libraries = libraries;
+ this.structs = structs;
this.types = types;
this.codes = new HashMap<>();
- this.system = new HashMap<>();
- // Codes are ordered in reverse, only take first entry to override others
- for (var c: codes) {
- for (var e: c.fields.entrySet()) {
- this.codes.putIfAbsent(c.name + ":" + e.getKey(), e.getValue());
+ for (var code: codes) {
+ String prefix = code.name.name.value();
+ for (var e: code.entries.entrySet()) {
+ this.codes.put(prefix + ":" + e.getKey(), e.getValue());
}
}
- for (var c: system) {
- for (var e: c.system.entrySet()) {
- this.system.put(c.name + ":" + e.getKey(), e.getValue());
- }
- }
- }
-
- public Data code() {
- return Data.ofTextMap(codes);
- }
-
- public Data code(String module) {
- return Data.ofTextMap(codes, module + ":");
}
- public Data system() {
- return Data.ofTextMap(system);
+ public Value code(String prefix) {
+ return (key) -> codes.getOrDefault(prefix + ":" + key, Scalar.UNDEFINED);
}
- public Data system(String part) {
- return Data.ofTextMap(system, part + ":");
+ public Value.Hash code() {
+ return Value.Hash.of(codes);
}
- public Text getCode(String type, String name) {
- return codes.getOrDefault(type + ":" + name, Text.UNDEFINED);
+ public Scalar getCode(String type, String name) {
+ return (Scalar)codes.getOrDefault(type + ":" + name, Scalar.UNDEFINED);
}
- public Text getCode(Text key) {
+ public Scalar getCode(Scalar key) {
switch (key.type()) {
case LITERAL:
case STRING:
return key;
case IDENTIFIER:
- return codes.getOrDefault(key.value(), Text.UNDEFINED);
+ return (Scalar)codes.getOrDefault(key.value(), Scalar.UNDEFINED);
default:
throw new NoSuchElementException("No key for :" + key);
}
}
- public Text getSystem(Match.Type type, String key, Text fallback) {
- var v = system.get(type.name() + ":" + key);
- if (v != null)
- return v;
- else
- return fallback;
+ public APIView newDependencyMap(Export capi) {
+ return new APIView(capi);
}
- public DependencyMap newDependencyMap(Export capi) {
- return new DependencyMap(capi);
- }
-
- public class DependencyMap {
- Set<String> seen = new HashSet<>();
+ public class APIView {
+ // All types the api needs
+ private Set<String> seen = new HashSet<>();
Export capi;
- // Stores expadned types
- Map<String, TemplateMap> visited = new HashMap<>();
+ // Concrete types. Used for each field or parameter.
+ Map<String, Type> visited = new HashMap<>();
- public DependencyMap(Export capi) {
+ public APIView(Export capi) {
this.capi = capi;
}
- public Stream<Hash> visisted(Match.Type type) {
+ public Function<String, Value> getTypes() {
+ return key -> Hash.of(visited.get(key).entries);
+ }
+
+ public Set<String> allTypes() {
+ return seen;
+ }
+
+ public Set<String> allTypes(String klass) {
+ return seen.stream().filter(s->s.startsWith(klass+":")).collect(Collectors.toSet());
+ }
+
+ public Stream<Hash> visited(Field.Type type) {
String match = type.name() + ":";
return seen.stream().filter(s -> s.startsWith(match)).map(capi::getType);
}
// Types from the start set
for (String s: roots) {
- Hash h = capi.getType(s);
- assert h.defined();
+ var h = capi.getType(s);
+ assert h != null;
if (seen.add(s))
queue.add(h);
}
- /*
- for (Value v: start.hash.values()) {
- Hash h = (Hash)v;
- System.out.println(h.getScalar("type").getValue() + ":" + h.getScalar("name").getValue());
- seen.add(h.getScalar("type").getValue() + ":" + h.getScalar("name").getValue());
- queue.add((Hash)v);
- }*/
while (!queue.isEmpty()) {
- Hash type = queue.removeFirst();
-
- //System.out.println("\nprocess: " + type.getScalar("type").value + type.getScalar("name").value);
-
- //type.dump(System.out);
- //Export.allFields(type).forEach(h -> h.dump(System.out));
+ var type = queue.removeFirst();
Export.allFields(type)
- .map(Hash.field("deref"))
+ .map(h -> h.getString("deref"))
.forEach(this::visitType);
- // What do i add to the queue?
- // What about all the types it added, how do look them up?
- // Field 'type' will map t capi->{key}
Export.allFields(type)
- .map(Hash.field("type"))
+ .map(h -> h.getString("type"))
.filter(s -> s.indexOf(':') >= 0)
.filter(seen::add)
- .filter(s -> {
- Hash v = capi.types.getHash(s);
- if (!v.defined()) {
- throw new RuntimeException("no type: " + s);
- }
- /*
- if (!v.defined()) {
- if (s.startsWith("struct:")) {
- v = new Hash();
- v.put("type", s);
- } else {
- System.getLogger(API.class.getName()).log(System.Logger.Level.WARNING, () -> "Unsupported anonymous type: " + s);
- }
- }*/
- return v.defined();
- })
- .map(s -> capi.types.getHash(s))
+ .map(s -> capi.getType(s))
.forEach(queue::add);
}
+
+ // Copy all flags from types to instances of/pointers to types
+ // other than struct?
+ seen.stream().filter(s -> s.startsWith("struct:")).map(capi::getType).forEach(type -> {
+ String name = type.getString("name");
+ Type temp = visited.get("u64:${" + name + "}");
+ if (temp != null) {
+ type.value().entrySet().stream().filter(e -> e.getKey().endsWith("?"))
+ .forEach(e -> temp.entries.put(e.getKey(), (Scalar)e.getValue()));
+ } else {
+ System.getLogger(API.class.getName()).log(System.Logger.Level.DEBUG, () -> "No pointer for for type: " + name);
+ }
+ });
+
}
/**
*
* @param deref Expanded raw type name.
*/
- public void visitType(String deref) {
- Matcher matcher = null;
- TemplateMap matched = null;
-
+ void visitType(String deref) {
//System.out.printf("visit: %s\n", deref);
-
if (visited.containsKey(deref))
return;
// Find the last matching type, exact matches override pattern matching.
for (var type: types) {
- if (type.exact(deref)) {
- System.out.println("add exact type: " + deref);
- visited.put(deref, type);
- return;
- } else if (matched == null) {
- Matcher test = type.pattern.matcher(deref);
+ Matcher test = type.pattern().matcher(deref);
- if (test.matches()) {
- matcher = test;
- matched = type;
- }
- }
- }
+ if (test.matches()) {
+ Type copy = type.copyAs(deref);
- if (matcher != null && matched != null) {
- TemplateMap type = matched.copyAs(deref);
+ for (var e: test.namedGroups().entrySet())
+ copy.entries.put(e.getKey(), Scalar.ofLiteral(test.group(e.getValue())));
- for (var e: matcher.namedGroups().entrySet())
- type.fields.put(e.getKey(), Text.ofLiteral(matcher.group(e.getValue())));
-
- System.out.println("add matching type: " + deref);
- visited.put(deref, type);
- } else {
- throw new NoSuchElementException("No match for type: " + deref);
- }
- }
-
- public Function<String, Data> typeMap() {
- // Copy all flags from types to instances of/pointers to types
- seen.stream().filter(s -> s.startsWith("struct:")).map(capi::getType).forEach(type -> {
- String name = type.getScalar("name").value;
- TemplateMap temp = visited.get("u64:${" + name + "}");
- if (temp != null) {
- type.hash.entrySet().stream().filter(e -> e.getKey().endsWith("?"))
- .forEach(e -> {
- temp.setTemplate(e.getKey(), Text.ofValue(e.getValue()));
- });
- } else {
- System.getLogger(API.class.getName()).log(System.Logger.Level.INFO, () -> "No pointer for anonymous type: " + name);
+ System.getLogger(API.class.getName()).log(System.Logger.Level.TRACE, () -> "Adding matched: " + type.pattern() + " type: " + deref);
+ visited.put(deref, copy);
+ return;
}
- });
+ }
- return (key) -> {
- return visited.get(key);
- };
+ throw new NoSuchElementException("No match for type: " + deref);
}
}
/**
- * For storing code and type blocks.
- * <p>
- * TODO: templates can be pre-compiled, do that here on-demand?
+ * *****************************************************************************************
*/
- static class TemplateMap implements Data {
- String name;
- Pattern pattern;
- Map<String, Text> fields = new HashMap<>();
-
- public TemplateMap(String name, boolean regex) {
- this.name = name;
- this.pattern = regex ? Pattern.compile(name) : null;
- }
-
- public TemplateMap(String name) {
- this(name, false);
- }
-
- public TemplateMap copyAs(String name) {
- TemplateMap map = new TemplateMap(name);
- map.fields.putAll(fields);
- return map;
- }
-
- public boolean exact(String name) {
- return pattern == null && this.name.equals(name);
- }
-
- public void setTemplate(String name, Text template) {
- fields.put(name, template);
- }
-
- // pre-parse here?
- public Text getTemplate(String name) {
- return fields.get(name);
- }
-
- public void inherit(TemplateMap b) {
- fields.putAll(b.fields);
+ static record Flag(Scalar name, Scalar code) {
+ Flag(Scalar name) {
+ this(name, Scalar.TRUE);
}
+ }
- @Override
- public Value getValue(Text key) {
- return getText(key).toValue();
- }
+ static record Field(Type type, Scalar name, Pattern pattern, List<Flag> options) {
- @Override
- public Text getText(Text key) {
- return fields.getOrDefault(key.value(), Text.UNDEFINED);
+ Field(Type type, Scalar name, List<Flag> options) {
+ this(type, name, name.type() == Scalar.Type.PATTERN ? Pattern.compile(name.value()) : null, options);
}
- public void dump(PrintStream out) {
- out.printf("\ttype: %s\n", name);
- for (var e: fields.entrySet()) {
- out.printf("\t\t%s", e.getKey());
- switch (e.getValue().type()) {
- case IDENTIFIER ->
- out.printf(" %s\n", e.getValue().value());
- case LITERAL ->
- out.printf(" <[%s]>\n", e.getValue().value());
- case STRING ->
- out.printf(" \"%s\";\n", e.getValue().value());
- }
- }
+ public Scalar code() {
+ // save perhaps?
+ return options.stream().filter(o -> o.name.value().equals("code")).findFirst().orElseThrow().code;
}
- }
- static record Option(String name, Text code) {
- Option(Text code) {
- this("#flag", code);
- }
- }
-
- static record Match(Type type, Predicate<String> matcher, List<Option> options, Text code) {
-
- static Match ofPattern(Type type, String pattern, List<Option> options) {
- Text code;
- options.forEach(System.out::println);
-
- if (options.isEmpty())
- code = Text.ofLiteral("template:" + type.name());
- else {
- Option c = options.stream().filter(o -> o.name.equals("code")).findFirst().orElseGet(options::getLast);
- options.remove(c);
- code = c.code();
- }
- options.forEach(System.out::println);
-
- return new Match(type, Pattern.compile(pattern).asMatchPredicate(), List.copyOf(options), code);
+ public Predicate<Hash> match(String field) {
+ return h -> {
+ return (h.getString(field) instanceof String t)
+ && pattern.matcher(t).matches();
+ };
}
enum Type {
func,
struct,
union,
- // Only used for loading?
- load,
- option;
+ //
+ library,
+ option,
+ //
+ code,
+ type,
+ include,
+ ident;
+
+ static final Map<String, Type> map = Map.of(
+ "call", call,
+ "func", func,
+ "struct", struct,
+ "union", union,
+ "library", library,
+ "option", option,
+ "code", code,
+ "type", type,
+ "include", include
+ );
+
+ static Type getType(String name) {
+ return map.getOrDefault(name, ident);
+ }
}
+ }
- public Predicate<Hash> match(String field) {
- return h -> {
- Scalar name = h.getScalar(field);
- return name.defined() ? matcher().test(name.getValue()) : false;
+ static class Group {
+ final Field name;
+ final List<Field> entries;
+ final Map<String, Value> options;
+
+ public Group(Field.Type type, Scalar name, List<Flag> options, List<Field> entries) {
+ this.name = new Field(type, name, options);
+ this.entries = entries;
+
+ this.options = new HashMap<>();
+ entries.stream().filter(f -> f.type == Field.Type.option).forEach(o -> {
+ String oname = o.name.value().indexOf(':') < 0 ? type.name() + ":" + o.name().value() : o.name().value();
+
+ // ugh maybe the parser should work this shit out
+ // Options without values are mapped to Scalar boolean
+ // Options with a single value are mapped to a Scalar Text
+ // Options with multiple values are mapped to Array of Scalar Text
+ // What aobut options with name=value pairs? Could always set them as individual options?
+ // FIXME: how to turn off boolean options
+ //
+ // This is shit!
+ //
+ // Maybe have a schema for the possible option types and process it somewhere else.
+ if (o.options.isEmpty()) {
+ if (this.options.put(oname, Scalar.TRUE) instanceof Value old)
+ System.getLogger(API.class.getName()).log(System.Logger.Level.WARNING, () -> "Overwrote option: " + o.name + " with TRUE: " + old);
+ } else {
+ o.options.forEach(flag -> {
+ if (flag.code() != Scalar.TRUE)
+ System.getLogger(API.class.getName()).log(System.Logger.Level.ERROR, () -> "Options cannot be assignments (ignored): " + flag);
+ else
+ this.options.compute(oname, (k, val) -> switch (val) {
+ case Array a -> {
+ a.value().add(flag.name());
+ yield a;
+ }
+ case Scalar s ->
+ Array.of(new ArrayList<>(List.of(s, flag.name())));
+ case null, default ->
+ flag.name();
+ });
+ });
+ }
+ });
+ }
+
+ public Value options(String... prefix) {
+ return (String key) -> {
+ if (key.indexOf(':') >= 0) {
+ return options.getOrDefault(key, Scalar.UNDEFINED);
+ } else
+ return Stream.of(prefix)
+ .flatMap(p -> Stream.ofNullable(options.get(p + ":" + key)))
+ .findFirst()
+ .orElseGet(() -> options.getOrDefault(key, Scalar.UNDEFINED));
};
}
+ public void dump(PrintStream out) {
+ out.printf("Group: %s\n", name);
+ out.println(" options:");
+ for (var e: options.entrySet())
+ out.printf("\t%s: %s\n", e.getKey(), e.getValue());
+ out.println(" entries:");
+ for (var e: entries)
+ out.printf("\t%s\n", e);
+ }
}
- /**
- * For a library.
- * <ul>
- * <li>func /pattern/ template-or-literal;
- * <li>struct /pattern/ template-or-literal;
- * <li>code template-or-literal;
- * <li>option name value;
- * </ul>
- */
- static class LibraryInfo {
- String name;
- List<String> load = new ArrayList<>();
- // non-pattern fields
- Map<String, Text> system = new HashMap<>();
- // Patterns to match things to templates
- List<Match> field = new ArrayList<>();
-
- public LibraryInfo(String name) {
- this.name = name;
- }
+ static class Type {
+ final Field name;
+ final Map<String, Value> entries;
- // Use system() ?
- public Text getSystem(Match.Type type, String key, Text fallback) {
- var v = system.get(type.name() + ":" + key);
- if (v != null)
- return v;
- else
- return fallback;
+ public Type(Field.Type type, Scalar name, List<Flag> options, Map<String, Value> entries) {
+ this.name = new Field(type, name, options);
+ this.entries = entries;
}
- public Data system() {
- return Data.ofTextMap(system);
+ public Type copyAs(String target) {
+ return new Type(name.type, Scalar.ofIdentifier(target), List.copyOf(name.options), new HashMap<>(entries));
}
- public Data system(String type) {
- return Data.ofTextMap(system, type + ":");
+ Pattern pattern() {
+ return name.pattern;
}
- void dump(PrintStream out) {
- out.printf("library: %s\n", name);
- for (var l: load)
- out.printf(" load %s\n", l);
- for (var m: field) {
- out.printf(" %s\n", m);
- }
+ public void dump(PrintStream out) {
+ out.printf("Type: %s\n", name);
+ for (var e: entries.entrySet())
+ out.printf("\t%s: %s\n", e.getKey(), e.getValue());
}
-
}
- static class APIParser implements AutoCloseable {
+ static class GroupParser implements AutoCloseable {
static final char[] assoc = {'=', '>'};
private final Tokeniser in;
private final Path base;
- List<LibraryInfo> libraries = new ArrayList<>();
- List<LibraryInfo> system = new ArrayList<>();
- Deque<TemplateMap> typeClasses = new ArrayDeque<>();
- Deque<TemplateMap> types = new ArrayDeque<>();
- Deque<TemplateMap> codeClasses = new ArrayDeque<>();
- Deque<TemplateMap> codes = new ArrayDeque<>();
-
- public APIParser(Path base, Tokeniser in) {
+ public GroupParser(Path base, Tokeniser in) {
this.in = in;
this.base = base;
}
- enum Types {
- code,
- type,
- include,
- library,
- system;
- }
-
- Types getType(String key) throws IOException {
- try {
- return Types.valueOf(key);
- } catch (IllegalArgumentException x) {
- throw new IOException(in.fatalMessage("Invalid object type: " + key));
- }
- }
-
@Override
public void close() throws IOException {
in.close();
}
- String identifier(int t) throws IOException {
- if (t != Tokeniser.TOK_IDENTIFIER)
- throw new IOException(in.fatalMessage(t, "Expeccting identifier"));
- return in.getText();
- }
-
- /**
- * Read a list of strings until ';'.
- * Strings are string or identifier.
- *
- * @return
- * @throws IOException
- */
- List<String> stringList() throws IOException {
- List<String> list = new ArrayList<>();
+ List<Flag> readOptions(IntPredicate when) throws IOException {
+ List<Flag> list = new ArrayList<>();
+ Scalar value = null;
+ int state = 0;
int t;
- while ((t = in.nextToken()) != -1 && t != ';') {
- switch (t) {
- case Tokeniser.TOK_IDENTIFIER:
- case Tokeniser.TOK_STRING_SQ:
- case Tokeniser.TOK_STRING_DQ:
- list.add(in.getText());
- break;
- default:
- throw new IOException(in.fatalMessage(t, "Expecting STRING IDENTIFIER or ';'"));
- }
- }
- return list;
- }
- /**
- * Looks for ';' and nothing else.
- *
- * @throws IOException
- */
- void emptyList() throws IOException {
- int t = in.nextToken();
- if (t != ';')
- throw new IOException(in.fatalMessage(t, "Expecting ';'"));
- }
-
- /**
- * Load a 'type' specification.
- * Applies inheritance as it goes.
- *
- * @throws IOException
- */
- TemplateMap loadType(Deque<TemplateMap> classes, Deque<TemplateMap> types) throws IOException {
- TemplateMap map;
- int t;
- String text;
-
- switch ((t = in.nextToken())) {
- case Tokeniser.TOK_REGEX:
- text = in.getText();
- map = new TemplateMap(in.getText(), true);
- types.add(map);
- break;
- case Tokeniser.TOK_IDENTIFIER:
- map = new TemplateMap(in.getText());
- classes.addFirst(map);
- break;
- default:
- throw new IOException(in.fatalMessage(t, "Expected name or regex"));
- }
+ while ((t = in.nextToken()) != -1 && when.test(t)) {
+ int lstate = state;
+ Scalar lvalue = value;
+ System.getLogger(Tokeniser.class.getName()).log(System.Logger.Level.DEBUG, () -> "Options " + lstate + " value " + lvalue);
- // options before {, inherit
- // do i need options? or just inherit?
- while ((t = in.nextToken()) != -1 && t != '{') {
- String tmp;
-
- switch (t) {
- case Tokeniser.TOK_IDENTIFIER:
- tmp = in.getText();
- classes.stream().filter(c -> c.exact(tmp)).findFirst().ifPresent(map::inherit);
- break;
- case ';':
- return map;
- default:
- throw new IOException(in.fatalMessage(t, "Expected name"));
+ switch (state) {
+ case 0 -> {
+ value = in.getText(t);
+ state = 1;
}
- }
-
- // Parse fields
- while ((t = in.nextToken()) != -1 && t != '}') {
- String key = identifier(t);
-
- // TODO: support options, do i need options?
- Text value = textValue();
-
- map.setTemplate(key, value);
- }
- return map;
- }
-
- Text textValue() throws IOException {
- int c = in.nextToken();
- Text text = in.getText(c);
-
- if (c == Tokeniser.TOK_QUOTED || c == Tokeniser.TOK_LITERAL)
- return text;
-
- c = in.nextToken();
- if (c != ';')
- throw new IOException(in.fatalMessage(c, "Expected ';'"));
- return text;
- }
-
- Text textOrNoValue() throws IOException {
- int c = in.nextToken();
-
- if (c == ';')
- return Text.ofLiteral("#true");
-
- Text text = in.getText(c);
-
- if (c == Tokeniser.TOK_QUOTED || c == Tokeniser.TOK_LITERAL)
- return text;
-
- c = in.nextToken();
- if (c != ';')
- throw new IOException(in.fatalMessage(c, "Expected ';'"));
- return text;
- }
-
- List<Option> optionList(int until) throws IOException {
- List<Option> list = new ArrayList<>();
- Text assign = null;
- Text value = null;
- int t;
-
- while ((t = in.nextToken()) != -1 && t != until) {
- if (t == '=') {
- if (assign != null)
- throw new IOException(in.fatalMessage("Broken assignment"));
- assign = value;
- value = null;
- } else {
- Text text = in.getText(t);
-
- if (assign != null) {
- list.add(new Option(assign.value(), text));
- assign = null;
- } else if (value != null) {
- list.add(new Option(value));
- value = text;
- } else {
- value = text;
+ case 1 -> {
+ if (t == '=')
+ state = 2;
+ else {
+ list.add(new Flag(value));
+ value = in.getText(t);
}
}
+ case 2 -> {
+ list.add(new Flag(value, in.getText(t)));
+ state = 0;
+ }
+ }
+ }
+ switch (state) {
+ case 1 ->
+ list.add(new Flag(value));
+ case 2 ->
+ throw new IOException(in.fatalMessage(t, "Trailing '='"));
}
- if (assign != null)
- throw new IOException(in.fatalMessage("Broken assignment"));
-
- if (value != null)
- list.add(new Option(value));
+ if (t == -1)
+ throw new EOFException(in.fatalMessage("Parsing options list"));
return list;
}
- LibraryInfo loadLibrary(List<LibraryInfo> libs) throws IOException {
+ Scalar readName() throws IOException {
int t;
- String name = switch ((t = in.nextToken())) {
+ return switch (t = in.nextToken()) {
+ case Tokeniser.TOK_REGEX ->
+ Scalar.ofPattern(in.getText());
case Tokeniser.TOK_IDENTIFIER ->
- in.getText();
+ Scalar.ofIdentifier(in.getText());
default ->
- throw new IOException(in.fatalMessage(t, "Expected name"));
+ throw new IOException(in.fatalMessage(t, "Expecting IDENTIFIER or PATTERN"));
};
- LibraryInfo lib = new LibraryInfo(name);
+ }
- // Library options inherit?
- // Ignore for now
- while ((t = in.nextToken()) != -1 && t != '{') {
- if (t != Tokeniser.TOK_IDENTIFIER) {
- throw new IOException(in.fatalMessage(t, "Expected name"));
+ void loadGroup(Field.Type thing, List<Group> list, Map<String, Group> proto) throws IOException {
+ int t;
+ List<Flag> options;
+ List<Field> entries = new ArrayList<>();
+ Scalar name = readName();
+
+ options = proto == null
+ ? readOptions(x -> x != '{' && x != ';')
+ : readOptions(x -> x != '{' && x != ';').stream().filter(o -> {
+ if (o.code == Scalar.TRUE && proto.get(o.name.value()) instanceof Group p) {
+ entries.addAll(p.entries);
+ return false;
+ } else
+ return true;
+ }).toList();
+
+ if (in.getToken() == '{') {
+ // Fields
+ while ((t = in.nextToken()) == Tokeniser.TOK_IDENTIFIER) {
+ String field = in.getText();
+ Field.Type type = Field.Type.getType(field);
+ Scalar key;
+
+ if (type == Field.Type.ident)
+ throw new IOException(in.fatalMessage("Invalid field name: " + field));
+
+ key = readName();
+ //if (type == Field.Type.ident) {
+ // type = Field.Type.code;
+ // key = Text.ofIdentifier(field);
+ //} else {
+ // key = readName();
+ //}
+ List<Flag> flags = readOptions(x -> x != ';');
+ entries.add(new Field(type, key, flags));
}
+ if (t != '}')
+ throw new IOException(in.fatalMessage(t, "Expecting IDENTIFIER or '}'"));
}
- // Library fields
- while ((t = in.nextToken()) != -1 && t != '}') {
- Match.Type match = Match.Type.valueOf(identifier(t));
-
- switch (match) {
- case load ->
- lib.load.addAll(stringList());
- case call, func, struct, union, option -> {
- switch (t = in.nextToken()) {
- case Tokeniser.TOK_REGEX ->
- lib.field.add(Match.ofPattern(match, in.getText(), optionList(';')));
- case Tokeniser.TOK_IDENTIFIER ->
- lib.system.put(match.name() + ":" + in.getText(), textOrNoValue());
- default ->
- throw new IOException(in.fatalMessage(t, "Expecting regex or identifier"));
- }
- }
+ Group g = new Group(thing, name, options, entries);
+
+ if (proto != null && name.type() == Scalar.Type.IDENTIFIER)
+ proto.put(name.value(), g);
+ else
+ list.addFirst(g);
+ }
+
+ void loadType(Field.Type thing, List<Type> list, Map<String, Type> proto) throws IOException {
+ int t;
+ List<Flag> options;
+ Map<String, Value> entries = new HashMap<>();
+ Scalar name = readName();
+
+ options = proto == null
+ ? readOptions(x -> x != '{' && x != ';')
+ : readOptions(x -> x != '{' && x != ';').stream().filter(o -> {
+ if (o.code == Scalar.TRUE && proto.get(o.name.value()) instanceof Type p) {
+ entries.putAll(p.entries);
+ return false;
+ } else
+ return true;
+ }).toList();
+
+ if (in.getToken() == '{') {
+ while ((t = in.nextToken()) == Tokeniser.TOK_IDENTIFIER) {
+ String field = in.getText();
+ Scalar value = in.getText(t = in.nextToken());
+
+ entries.put(field, value);
+
+ if (t != Tokeniser.TOK_QUOTED && t != Tokeniser.TOK_LITERAL)
+ in.token(';');
}
+ if (t != '}')
+ throw new IOException(in.fatalMessage(t, "Expecting IDENTIFIER or '}'"));
}
- libs.add(lib);
+ Type g = new Type(thing, name, options, entries);
- return lib;
+ if (proto != null && name.type() == Scalar.Type.IDENTIFIER)
+ proto.put(name.value(), g);
+ else
+ list.addFirst(g);
}
API load() throws IOException {
int t;
-types: while ((t = in.nextToken()) != -1) {
- String name = identifier(t);
- Types type = getType(name);
- List<String> strings;
+ List<Group> libraries = new ArrayList<>();
+ List<Type> codes = new ArrayList<>();
+
+ List<Type> types = new ArrayList<>();
+ Map<String, Type> prototypes = new HashMap<>();
+
+ List<Group> structs = new ArrayList<>();
+ Map<String, Group> protostructs = new HashMap<>();
+
+ while ((t = in.nextToken()) == Tokeniser.TOK_IDENTIFIER) {
+ String name = in.getText();
+ Field.Type type = Field.Type.getType(name);
switch (type) {
- case type:
- loadType(typeClasses, types);
- break;
- case code:
- loadType(codeClasses, codes);
- break;
- case include:
- strings = stringList();
- for (int i = 0; i < strings.size(); i++)
- in.push(Files.newBufferedReader(base.resolveSibling(strings.get(strings.size() - 1 - i))));
- break;
- case library:
- loadLibrary(libraries);
- break;
- case system:
- loadLibrary(system);
- break;
+ case type ->
+ loadType(type, types, prototypes);
+ case code ->
+ loadType(type, codes, null);
+ case struct ->
+ loadGroup(type, structs, protostructs);
+ case include -> {
+ for (var o: readOptions(x -> x != ';').reversed())
+ in.push(Files.newBufferedReader(base.resolveSibling(o.name.value())));
+ }
+ case option -> {
+ }
+ case library ->
+ loadGroup(type, libraries, null);
}
}
- return new API(system, libraries, types, codeClasses);
+ if (t != -1)
+ throw new EOFException(in.fatalMessage(t, "Expecting IDENTIFIER"));
+
+ return new API(libraries, structs, types, codes);
}
}
static API load(Path src) throws IOException {
- try (APIParser p = new APIParser(src, new Tokeniser(Files.newBufferedReader(src)))) {
+ try (GroupParser p = new GroupParser(src, new Tokeniser(Files.newBufferedReader(src)))) {
return p.load();
}
}
-
- public static void main(String[] args) throws IOException {
- System.setProperty("java.util.logging.config.file", "logging.properties");
- load(Path.of("test.api"));
- }
-
}
+++ /dev/null
-/*
- * Copyright (C) 2025 Michael Zucchi
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-package au.notzed.nativez.tools;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class Array extends Value {
- static final Array UNDEFINED = new Array(List.of());
- List<Value> array;
-
- public Array() {
- this(new ArrayList<>());
- }
-
- public Array(List<Value> array) {
- this.array = array;
- }
-
- @Override
- boolean defined() {
- return this != UNDEFINED;
- }
-
- public Value getValue(int i) {
- return defined() ? array.get(i) : Scalar.UNDEFINED;
- }
-
- public Array getArray(int start, int end) {
- return new Array(array.subList(Math.min(array.size(), start), Math.min(array.size(), end)));
- }
-
- @Override
- void dumper(StringBuilder sb, String indent) {
- sb.append("[\n");
- for (au.notzed.nativez.tools.Value v: array) {
- sb.append(indent);
- v.dumper(sb, indent + "\t");
- sb.append(",\n");
- }
- sb.append(indent);
- sb.append("]");
- }
-
-}
import java.io.IOException;
import java.io.PrintStream;
import java.io.StringWriter;
+import java.io.Writer;
import java.util.ArrayList;
import java.util.List;
-import java.util.NoSuchElementException;
import java.util.function.Function;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import java.util.stream.Stream;
/**
- * Template processing context.
+ * Context for template processor.
+ * <p>
*/
-public class Context implements Data {
- Function<String, Data> typeMap;
- Context parent;
- Data data[];
+public class Context implements Value {
- public Context(Function<String, Data> typeMap, Data... data) {
- this(typeMap, null, data);
- }
+ private final Function<String, Value> types;
+ private final Context parent;
+ private final Value data[];
- private Context(Function<String, Data> typeMap, Context parent, Data... data) {
- this.typeMap = typeMap;
+ private Context(Function<String, Value> types, Context parent, Value list[]) {
+ this.types = types;
+ this.data = list;
this.parent = parent;
- this.data = data;
}
- private Context(Function<String, Data> typeMap, Context parent, List<Data> list) {
- this(typeMap, parent, list.toArray(Data[]::new));
+ private Context(Function<String, Value> types, Context parent, List<Value> list) {
+ this(types, parent, list.toArray(Value[]::new));
}
- /**
- * Resolves a key, expanding as necessary.
- *
- * FIXME: use same conventions as getValue()!
- *
- * e.g. use / for paths not .
- * @param key
- * @return
- */
- public String resolveText(String name) {
- Text key = Text.ofIdentifier(name);
- Text val = getText(key);
-
- while (true) {
- switch (val.type()) {
- case IDENTIFIER:
- val = getText(val);
- break;
- case STRING:
- try (StringWriter dst = new StringWriter()) {
- Template.load(val.value()).process(this, dst);
- return dst.toString();
- } catch (IOException ex) {
- System.getLogger(Context.class.getName()).log(System.Logger.Level.ERROR, (String)null, ex);
- throw new RuntimeException();
- }
- case LITERAL:
- return val.value();
- case UNDEFINED: {
- if (key.value().indexOf('.') > 0) {
- String path[] = key.value().split("\\.");
- int i = 0;
- Value base = getValue(Text.ofIdentifier(path[i++]));
- while (true) {
- if (base instanceof Scalar s) {
- // No way to know if it's a template so treat it as a string
- val = Text.ofString(s.getValue());
- break;
- } else {
- if (i == path.length)
- throw new NoSuchElementException(name);
- switch (base) {
- case Hash h ->
- base = h.getValue(path[i++]);
- case Array a -> {
- base = a.getValue(Integer.parseInt(path[i++]));
- }
- default -> {
- throw new NoSuchElementException(name);
- }
- }
- }
- }
- } else {
- throw new RuntimeException("No such key: " + key);
- }
- break;
- }
- default:
- throw new RuntimeException("inalid type: " + val);
- }
- }
+ public Context(Function<String, Value> types, Value... list) {
+ this(types, null, list);
}
- /**
- * Range format.
- * <p>
- * start:end
- * index
- * <p>
- * "0:4" number range exclusive
- * "1" specific entry from start
- * "-1" specific entry from end
- * "3:-" skip start to end
- * "-3:-" last 3 values
- *
- * @param key
- * @return
- */
- static Value range(Array src, String key) {
- int idx = key.indexOf(':');
-
- System.out.println("key: " + key);
- if (idx < 0) {
- idx = Integer.parseInt(key);
- if (idx < 0)
- return src.getValue(src.array.size() + idx);
- else
- return src.getValue(idx);
- } else if (idx == 0 || idx == key.length() - 1) {
- throw new IllegalArgumentException("Invalid array reference: " + key);
+ public Context push(Value... data) {
+ List<Value> list = new ArrayList<>();
+ for (var d: data) {
+ list.add(d);
+ if (d.getValue("deref") instanceof Scalar type && type.defined()
+ && types.apply(type.value()) instanceof Value val)
+ list.add(val);
}
- int start = Integer.parseInt(key, 0, idx, 10);
- int end = (idx == key.length() - 2 && key.charAt(idx + 1) == '-')
- ? src.array.size() - 1
- : Integer.parseInt(key, idx + 1, key.length(), 10);
- start = start < 0 ? src.array.size() + start : start;
- end = end < 0 ? src.array.size() + end : end;
- return src.getArray(start, end + 1);
- }
-
- @Override
- public Value getValue(Text key) {
- // Only lookup identifiers
- if (key.type() != Text.Type.IDENTIFIER)
- return key.toValue();
- // Check path reference
- if (key.value().indexOf('/') > 0) {
- String[] path = key.value().split("/");
- int idx = 0;
- Value val = getValue(Text.ofIdentifier(path[idx++]));
-
- while (idx < path.length) {
- if (val instanceof Hash h) {
- val = h.getValue(path[idx++]);
- } else if (val instanceof Array a) {
- val = range(a, path[idx++]);
- }
- }
+ return new Context(types, this, list);
+ }
- return val;
+ public void dump(PrintStream out, String prefix) {
+ int i = 0;
+ for (var v: data) {
+ out.printf("%sdata[%d] %s", prefix, i++, v.getClass());
+ v.dump(out, prefix);
+ out.println();
}
-
- // code template reference (not really needed but for completeness sake)
- //if (key.value().indexOf(':') > 0)
- // return api.getCode(key).toValue();
- return Stream.of(data)
- .map(d -> d.getValue(key))
- .filter(t -> t.defined())
- .findFirst()
- .orElseGet(() -> parent != null ? parent.getValue(key) : Scalar.UNDEFINED);
- /* .orElseGet(() -> {
- if (parent != null)
- return parent.getValue(key);
-
- System.out.println("fallback key lookup " + key);
- Text val = api.getCode("default", key.value());
- System.out.println(" code: " + val);
- if (!val.defined())
- val = api.getSystem(API.Match.Type.option, key.value(), Text.UNDEFINED);
- System.out.println(" option: " + val);
- return val.toValue();
-
- });*/
+ if (parent != null)
+ parent.dump(out, prefix + "\t");
}
public void dump(PrintStream out) {
- System.out.printf("dump %s, parent %s\n", this, parent);
-
- Context c = this;
- while (c != null) {
- for (Data d: c.data)
- d.dump(System.out);
- c = c.parent;
- System.out.printf(" parent %s\n", c);
- }
+ dump(out, "");
}
- @Override
- public Text getText(Text key) {
- while (key.type() == Text.Type.IDENTIFIER) {
- Text query = key;
+ public void expand(Scalar key, Writer dst) throws IOException {
+ Value val = getValue(key);
- key = Stream.of(data)
- .map(d -> d.getText(query))
- .filter(t -> t.defined())
- .findFirst()
- .orElseGet(() -> parent != null ? parent.getText(query) : Text.UNDEFINED);
+ if (val instanceof Scalar text) {
+ switch (text.type()) {
+ case IDENTIFIER, LITERAL, PATTERN -> {
+ dst.append(text.value());
+ return;
+ }
+ case STRING -> {
+ Template.load(text.value()).process(this, dst);
+ return;
+ }
+ }
}
+ //System.err.print(key);
+ //System.err.print(" ");
+ //dump(System.err, " > ");
+ throw new IllegalArgumentException(String.format("Result not valid scalar: %s: %s", key, val));
+ }
- return key;
-
- // code template reference
- //if (key.value().indexOf(':') > 0)
- // return api.getCode(key);
- /*
- if (parent != null)
- return parent.getText(key);
- Text val = api.getCode("default", key.value());
- if (!val.defined())
- val = api.getSystem(API.Match.Type.option, key.value(), Text.UNDEFINED);
- return val;
- */
+ public String expand(String ident) throws IOException {
+ return expand(Scalar.ofIdentifier(ident));
}
- public Context push(Data... data) {
- Text key = Text.ofIdentifier("deref");
+ public String expand(Scalar key) throws IOException {
+ Value val = getValue(key);
- List<Data> list = new ArrayList<>();
- for (var d: data) {
- Value v = d.getValue(key);
-
- list.add(d);
-
- if (v.defined() && v instanceof Scalar s) {
- Data t = typeMap.apply(s.getValue());
- if (t != null) {
- list.add(t);
- } else {
- System.getLogger(Context.class.getName()).log(System.Logger.Level.WARNING, () -> "No type expansion defined for: " + s);
- throw new AssertionError("API not properly initialised");
+ if (val instanceof Scalar text) {
+ switch (text.type()) {
+ case IDENTIFIER, LITERAL, PATTERN -> {
+ return text.value();
+ }
+ case STRING -> {
+ try (StringWriter dst = new StringWriter()) {
+ Template.load(text.value()).process(this, dst);
+ return dst.toString();
}
}
+ }
}
-
- return new Context(typeMap, this, list);
- }
-
- /*
- static final Context ROOT = new Context(null, null) {
- @Override
- protected Value getValue(String name) {
- // sigh, should it all be the same?
- return Scalar.UNDEFINED;
- }
-
- @Override
- protected Scalar getScalar(String name) {
- return Scalar.UNDEFINED;
- }
-
- @Override
- protected Array getArray(String name) {
- return Array.UNDEFINED;
- }
-
- @Override
- protected Hash getHash(String name) {
- return Hash.UNDEFINED;
- }
-
- };
- */
- /*
- @Deprecated
- public Context(Context parent, Hash hash, API api) {
- this.parent = parent;
- this.hash = hash;
- this.api = api;
+ //System.err.print(key);
+ //System.err.print(" ");
+ //dump(System.err, " > ");
+ throw new IllegalArgumentException(String.format("Result not valid scalar: '%s' = %s", key, val));
}
- @Deprecated
- public Context(Hash hash, API api) {
- this(ROOT, hash, api);
- }
- @Deprecated
- public Context pull() {
- return parent;
+ @Override
+ public Value getValue(String key) {
+ return getValue(Scalar.ofIdentifier(key));
}
- @Deprecated
- public Context push(Hash hash) {
- Scalar deref = hash.getScalar("deref");
- if (deref.defined()) {
- template = api.visited.get(deref.getValue());
+ @Override
+ public Value resolve(String... path) {
+ Context ctx = this;
+ int i = 0;
+
+ for (; i < path.length && path[i].equals(".."); i++) {
+ if (ctx.parent == null)
+ throw new IllegalArgumentException(String.format("Path parent doesn't exist: %s", List.of(path)));
+ ctx = ctx.parent;
}
- return new Context(this, hash, api);
- }
- */
- /*
- @Deprecated
- protected Value getValue(String name) {
- au.notzed.nativez.tools2.Value val = hash.getValue(name);
- return val.defined() ? val : parent.getValue(name);
- }
- @Deprecated
- protected Scalar getScalar(String name) {
- au.notzed.nativez.tools2.Scalar val = hash.getScalar(name);
- return val.defined() ? val : parent.getScalar(name);
- }
+ Value val = ctx;
- @Deprecated
- protected Array getArray(String name) {
- au.notzed.nativez.tools2.Array val = hash.getArray(name);
- return val.defined() ? val : parent.getArray(name);
- }
+ for (; i < path.length; i++)
+ val = val.getValue(path[i]);
- @Deprecated
- protected Hash getHash(String name) {
- au.notzed.nativez.tools2.Hash val = hash.getHash(name);
- return val.defined() ? val : parent.getHash(name);
- }
- */
- /* Ressolve the base type */
- /*
- @Deprecated
- public Value resolveValue(Text name) {
- assert (name.type() == Text.Type.IDENTIFIER);
- return getValue(name.value());
+ return val;
}
- @Deprecated
- public List<Value> resolveArray(Text name) {
- assert (name.type() == Text.Type.IDENTIFIER);
- return getArray(name.value()).array;
+ public <V extends Value> V getValue(Class<V> klass, Scalar key) {
+ return klass.cast(getValue(key));
}
- @Deprecated
- public Text resolve(Text name) {
- //System.out.println("resolving " + name);
-
- if (name.type() != Text.Type.IDENTIFIER)
- return name;
-
- // code template reference
- if (name.value().indexOf(':') > 0)
- return api.getCode(name.value());
-
- // Check local context
- if (hash != null) {
- au.notzed.nativez.tools2.Scalar val = hash.getScalar(name.value());
- if (val.defined())
- return Text.ofString(val.getValue());
- }
-
- // Then template
- if (template != null) {
- var tmp = template.getTemplate(name.value());
- if (tmp != null)
- return tmp;
- }
-
- // Try parent, TODO: loop here (void re-checking type/api test)
- if (parent != null)
- return parent.resolve(name);
- else
- return Text.ofLiteral("<undefined>");
- }
- */
- /**
- * Cast a value to a boolean.
- * <p>
- * A value must be defined, an array must be non-empty, and a scalar must not be '0' to be considered true.
- *
- * @param name
- * @return
- */
- public boolean resolveBoolean(Text name) {
- assert (name.type() == Text.Type.IDENTIFIER);
+ public Value getValue(Scalar key) {
+ while (key.type() == Scalar.Type.IDENTIFIER) {
+ String query = key.value();
- Value val = getValue(name);
-
- if (val.defined()) {
- if (val instanceof Array array) {
- return !array.array.isEmpty();
- } else if (val instanceof Scalar scalar) {
- return !scalar.value.equals("0");
+ // IDEA: allow '..' to go to parent, but in that case Context would have to implement resolve(String...)
+ if (query.indexOf('/') > 0) {
+ return resolve(query.split("/"));
}
- return true;
- }
- return false;
- }
-
- public int resolveCompare(Text name, Text value) {
- assert (name.type() == Text.Type.IDENTIFIER);
-
- // Could also resolve 'value' if it's an identifier i guess
- String test = resolveText(name.value());
- // TODO: numbers?
- return test.compareTo(value.value());
- /*
- Text test = getText(name);
+ Value value = Scalar.UNDEFINED;
+ Context q = this;
- // Check for compound names
- // Shit, this needs to be in getXX functions too.
- // double shit, names have . in them.
- if (!test.defined()) {
- String path[] = name.value().split("\\.");
- if (path.length > 1) {
- int i = 0;
- Value base = getValue(Text.ofIdentifier(path[i++]));
- while (true) {
- if (base instanceof Hash h) {
- base = h.getValue(path[i++]);
- } else if (base instanceof Array a) {
- base = a.getValue(Integer.parseInt(path[i++]));
- } else if (base instanceof Scalar s) {
- test = Text.ofValue(base);
- break;
- }
+found: do {
+ for (Value d: q.data) {
+ value = d.getValue(query);
+ if (value != Scalar.UNDEFINED)
+ break found;
}
+ q = q.parent;
+ } while (q != null);
+
+ if (value instanceof Scalar text) {
+ key = text;
+ } else {
+ return value;
}
}
- System.out.printf("compare '%s' <> '%s' - %s\n", name, value, test);
-
- // TODO: numbers?
- return test.value().compareTo(value.value());
- */
+ return key;
}
+
}
+++ /dev/null
-/*
- * Copyright (C) 2025 Michael Zucchi
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-package au.notzed.nativez.tools;
-
-import java.io.PrintStream;
-import java.util.Map;
-
-/* to replace context datas once i get it working */
-public interface Data {
-
- /*
- requirements
-
- template processing
- - foreach needs array or hash
- - boolean needs 'defined', array-length/hash-size or scalar value
- - compare needs scalar value
- - append needs scalar value
- */
- public default Value getValue(Text key) {
- return getText(key).toValue();
- }
-
- public default Text getText(Text key) {
- if (key.defined()) {
- if (key.type() != Text.Type.IDENTIFIER)
- return key;
-
- else if (getValue(key) instanceof Scalar s && s.defined())
- return Text.ofString(s.getValue());
- }
- return Text.UNDEFINED;
- }
-
- public static Data ofTextMap(Map<String, Text> map) {
- return new Data() {
- @Override
- public Text getText(Text key) {
- return map.getOrDefault(key.value(), Text.UNDEFINED);
- }
-
- @Override
- public void dump(PrintStream out) {
- for (var e: map.entrySet()) {
- out.printf(" %s = %s\n", e.getKey(), e.getValue());
- }
- }
- };
- }
-
- public static Data ofTextMap(Map<String, Text> map, String prefix) {
- return new Data() {
- @Override
- public Text getText(Text key) {
- return map.getOrDefault(prefix + key.value(), Text.UNDEFINED);
- }
-
- @Override
- public void dump(PrintStream out) {
- for (var e: map.entrySet()) {
- if (e.getKey().startsWith(prefix))
- out.printf(" %s = %s\n", e.getKey().substring(prefix.length()), e.getValue());
- }
- }
- };
- }
-
- public void dump(PrintStream out);
-
-}
*/
package au.notzed.nativez.tools;
+import au.notzed.nativez.tools.Value.*;
import java.io.IOException;
import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Function;
/**
* All the types in the perl export.
- * This is a hash of hashes, the hash contents depends on '.type'.
+ * <p>
+ * This is a hash of hashes, each hash contents depends on '.type'.
*/
Hash types;
// Do some processing to fix up some things missing from the export
Set<String> all = new TreeSet<>();
- for (Value ti: types.hash.values()) {
+ for (Value ti: types.value().values()) {
Hash type = (Hash)ti;
- String klass = type.getScalar("type").value;
- Stream<Value> members;
- switch (klass) {
- case "struct":
+ switch (type.getString("type")) {
+ case "struct" ->
prepareStruct(type);
- members = type.getArray("fields").array.stream();
- break;
- case "union":
- // FIXME: implement
- System.getLogger(Export.class.getName()).log(System.Logger.Level.WARNING, () -> "Union unimplemented: " + type.getScalar("name").value);
- members = type.getArray("fields").array.stream();
- break;
- case "func":
+ case "union" -> // FIXME: implement union prepare (iirc it's the same as struct but resets offset at each field)
+ System.getLogger(Export.class.getName()).log(System.Logger.Level.WARNING, () -> "Union unimplemented: " + type.getString("name"));
+ case "func" ->
prepareFunc(type);
- members = Stream.concat(Stream.of(type.getValue("result")), type.getArray("arguments").array.stream());
- break;
- default:
- members = Stream.empty();
- break;
}
- members.map(v -> (Hash)v).map(m -> {
- if (m.getScalar("type") == Scalar.UNDEFINED)
- throw new RuntimeException("Missing type:\n" + m.dumper());
- return m.getScalar("deref").value;
- }).forEach(all::add);
+
+ allFields(type).map(m -> m.getString("deref")).forEach(all::add);
}
for (String ref: all) {
if (m.matches()) {
String name = m.group("pointer");
- Hash s = types.getHash("struct:" + name);
+ var s = types.getValue("struct:" + name);
- if (s == Hash.UNDEFINED) {
- Hash handle = new Hash();
+ if (!s.defined()) {
+ Map<String, Value> handle = new HashMap<>();
System.getLogger(Export.class.getName()).log(System.Logger.Level.INFO, () -> "Insert anonymous type: " + name);
- handle.put("type", "struct");
- handle.put("name", name);
- handle.put("Name", renameField.apply(new Scalar(name)));
- handle.put("opaque?", "#true");
- handle.put("fields.doc", ("typedef struct " + name + " * " + name + ";"));
+ handle.put("type", Scalar.ofLiteral("struct"));
+ handle.put("name", Scalar.ofLiteral(name));
+ handle.put("Name", renameField.apply(name));
+ handle.put("opaque?", Scalar.TRUE);
+ handle.put("fields.doc", Scalar.ofLiteral("typedef struct " + name + " * " + name + ";"));
- types.put("struct:" + name, handle);
+ types.value().put("struct:" + name, Hash.of(handle));
}
}
}
}
public Hash getType(String typename) {
- return types.getHash(typename);
+ return (Hash)types.getValue(typename);
}
+ /*
public Hash getType(String type, String name) {
return getType(type + ":" + name);
- }
-
+ }*/
public Stream<Hash> types(String type) {
- return types.hash.values().stream()
+ return types.value().values().stream()
.map(v -> (Hash)v)
- .filter(v -> v.getScalar("type").getValue().equals(type));
+ .filter(v -> v.getString("type").equals(type));
}
+ /**
+ * Stream all fields on a type.
+ *
+ * @param def The type definition.
+ * @return
+ */
public static Stream<Hash> allFields(Hash def) {
- return (switch (def.getScalar("type").getValue()) {
- case "struct" ->
- def.getArray("fields").defined() ? def.getArray("fields").array.stream() : Stream.empty();
+ return (switch (def.getString("type")) {
+ case "struct", "union" ->
+ def.getValue("fields") instanceof Array list ? list.value().stream() : Stream.empty();
case "func", "call" ->
- Stream.concat(Stream.of(def.getHash("result")), def.getArray("arguments").array.stream());
+ Stream.concat(Stream.of(def.getValue("result")), def.getValue("arguments") instanceof Array list ? list.value().stream() : Stream.empty());
default ->
Stream.empty();
- }).map(n -> (Hash)n);
+ }).map(Hash.class::cast);
}
/**
- * Scan all entriees of all fields.
+ * Stream all fields on all types.
*
+ * @see allFields(Hash)
* @return
*/
public Stream<Hash> allFields() {
- return types.hash.values().stream()
+ return types.value().values().stream()
.map(v -> (Hash)v)
- .flatMap(def -> {
- return switch (def.getScalar("type").getValue()) {
- case "struct" ->
- def.getHash("fields").defined() ? def.getHash("fields").hash.values().stream() : Stream.empty();
- case "func", "call" ->
- Stream.concat(Stream.of(def.getHash("result")), def.getArray("arguments").array.stream());
- default ->
- Stream.empty();
- };
- })
- .map(n -> (Hash)n);
+ .flatMap(Export::allFields);
}
- Function<Scalar, Scalar> renameStruct = s -> new Scalar(Util.toStudlyCaps(s.getValue()));
- Function<Scalar, Scalar> renameField = s -> new Scalar(Util.toCamelCase(s.getValue()));
+ Function<String, Value.Scalar> renameStruct = s -> Scalar.ofLiteral(Util.toStudlyCaps(s));
+ Function<String, Value.Scalar> renameField = s -> Scalar.ofLiteral(Util.toCamelCase(s));
static final Pattern isPointer = Pattern.compile("^u64:\\$\\{(?<pointer>.*)\\}$");
static final Pattern isStruct = Pattern.compile("^\\$\\{(?<struct>.*)\\}$");
final void prepareFunc(Hash f) {
- Matcher m = isStruct.matcher(f.getHash("result").getScalar("deref").getValue());
+ Matcher m = isStruct.matcher(f.resolveString("result", "deref"));
if (m.matches()) {
- f.hash.put("struct-return?", new Scalar("#true"));
+ f.value().put("struct-return?", Scalar.TRUE);
}
}
* Adds padding information and java-names.
*/
final void prepareStruct(Hash s) {
- s.hash.computeIfAbsent("Name", k -> renameStruct.apply(s.getScalar("name")));
- if (!s.getArray("fields").defined())
- return;
+ Array fields = s.getValue(Array.class, "fields");
StringBuilder doc = new StringBuilder();
- Array list = new Array();
+ List<Value> list = new ArrayList<>();
long offset = 0;
doc.append("struct ");
- doc.append(s.getScalar("name"));
+ doc.append(s.getString("name"));
doc.append(" {\n");
- for (Value fv: s.getArray("fields").array) {
+ s.value().computeIfAbsent("Name", k -> renameStruct.apply(s.getString("name")));
+
+ for (Value fv: ((Array)fields).value()) {
Hash f = (Hash)fv;
- long fsize = f.getScalar("size").getInteger();
- long foffset = f.getScalar("offset").getInteger();
+ long fsize = Long.parseLong(f.getString("size"));
+ long foffset = Long.parseLong(f.getString("offset"));
- f.hash.computeIfAbsent("Name", k -> renameStruct.apply(f.getScalar("name")));
+ f.value().computeIfAbsent("Name", k -> renameStruct.apply(f.getString("name")));
if ((fsize & 7) != 0 || (foffset & 7) != 0) {
- System.getLogger("Generator").log(System.Logger.Level.WARNING, () -> "Bitfields not supported: " + s.getScalar("name"));
+ System.getLogger("Generator").log(System.Logger.Level.WARNING, () -> "Bitfields not supported: " + s.getString("name"));
continue;
}
if (foffset > offset) {
- Hash pad = new Hash();
-
- pad.hash.put("size", new Scalar(String.valueOf(foffset - offset)));
- pad.hash.put("offset", new Scalar(String.valueOf(foffset)));
- pad.hash.put("deref", new Scalar("padding"));
- list.array.add(pad);
+ Map<String, Value> pad = new HashMap<>();
+ String size = String.valueOf(foffset - offset);
+ pad.put("size", Scalar.ofLiteral(size));
+ pad.put("offset", Scalar.ofLiteral(String.valueOf(foffset)));
+ pad.put("deref", Scalar.ofLiteral("p" + size));
+ list.add(Hash.of(pad));
}
- list.array.add(f);
+ list.add(f);
offset = foffset + fsize;
doc.append("\t\t");
- doc.append(f.getScalar("ctype"));
+ doc.append(f.getString("ctype"));
doc.append(" ");
- doc.append(f.getScalar("name"));
+ doc.append(f.getString("name"));
doc.append(";\n");
}
- s.hash.put("fields.layout", list);
+ s.value().put("fields.layout", Array.of(list));
doc.append("\t}");
- s.hash.put("fields.doc", new Scalar(doc.toString()));
+ s.value().put("fields.doc", Scalar.ofLiteral(doc.toString()));
}
static Export load(Path src) throws IOException {
return new Export((Hash)Value.load(src));
}
-
- public static void main(String[] args) throws IOException {
- System.setProperty("java.util.logging.config.file", "logging.properties");
- load(Path.of("../notzed.dev/api/api.pm"));
- }
}
*/
package au.notzed.nativez.tools;
-import static au.notzed.nativez.tools.API.Match.Type.struct;
+import au.notzed.nativez.tools.Value.*;
import java.io.IOException;
-import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
import java.util.Set;
+import java.util.regex.Matcher;
public class Generator {
- Path dir;
+ final Path dstdir;
+ private final Path load;
- public Generator(Path dir) {
- this.dir = dir;
+ public Generator(Path dir, Path load) {
+ this.dstdir = dir;
+ this.load = load;
}
void generate(Context ctx, Template code) throws IOException {
- String name = ctx.resolveText("Name");
- String module = ctx.resolveText("module");
- String[] pkg = ctx.resolveText("package").split("/");
- Path path = dir.resolve(module).resolve("classes", pkg);
+ String name = ctx.expand("Name");
+ String module = ctx.expand("module");
+ String[] pkg = ctx.expand("package").split("/");
+ Path path = dstdir.resolve(module).resolve("classes", pkg);
Path file = path.resolve(name + ".java");
Files.createDirectories(path);
}
}
- void proto() throws IOException {
- Path base = Path.of("api");
- API api = API.load(base.resolve("test.api"));
- Export capi = Export.load(base.resolve("test.pm"));
+ void generate() throws IOException {
+ API api = API.load(load);
+ Export capi = Export.load(load.resolveSibling(load.getFileName().toString().replaceFirst("\\.api$", ".pm")));
// Find all the types we're going to use
for (var lib: api.libraries) {
Set<String> roots = new HashSet<>();
- lib.dump(System.out);
-
+ //lib.dump(System.out);
// move to Library?
// Get the base set
- for (var m: lib.field) {
+ // Note: modifies the capi types
+ for (var m: lib.entries.reversed()) {
switch (m.type()) {
case func:
case struct:
case call:
capi.types(m.type().name())
- .filter(m.match("name"))
.forEach(f -> {
- // Set the call template as a new template (put this outside the loop?)
- Text code = m.code();
- if (code.type() == Text.Type.IDENTIFIER)
- f.hash.put(m.type().name() + ".template", new Scalar("{/" + code.value() + "}"));
- else
- f.hash.put(m.type().name() + ".template", code.toValue());
- for (API.Option o: m.options()) {
- if (o.name().equals("#flag")) {
- f.put(o.code().value(), "#true");
- } else {
- f.put(o.name(), o.code().toValue());
+ Matcher test = m.pattern().matcher(f.getString("name"));
+ if (test.matches()) {
+ var map = f.value();
+
+ // Copy any regex named groups to the type
+ for (var e: test.namedGroups().entrySet())
+ map.put(e.getKey(), Scalar.ofLiteral(test.group(e.getValue())));
+
+ // Copy any options to the type
+ for (var o: m.options()) {
+ String n = o.name().value();
+ map.put(n.equals("code") ? m.type().name() + ":code" : n, o.code());
}
+ roots.add(m.type().name() + ":" + f.getString("name"));
}
- roots.add(m.type().name() + ":" + f.getScalar("name").getValue());
});
break;
-
+ case option:
+ break;
default:
System.getLogger("Generator").log(System.Logger.Level.WARNING, () -> "Unhandled library entries: " + m);
}
}
// Move to API, return a new structure?
- var view = api.newDependencyMap(capi);
+ var deps = api.newDependencyMap(capi);
- view.visitDependencies(roots);
- var typeMap = view.typeMap();
+ deps.visitDependencies(roots);
// structures
if (true) {
- // To allow overrriding per-struct would need to be pre-struct
- Context ctx = new Context(typeMap, lib.system(), lib.system("option"), api.code(), api.system("option"), api.system("default"), api.code("default"));
-
- for (String a: view.seen) {
- // if (anon.contains(a))
- // continue;
+ Context ctx = new Context(deps.getTypes(), api.code(), api.code("default"), lib.options("struct"));
- if (a.startsWith("struct:")) {
- Hash type = capi.getType(a);
+ for (String a: deps.allTypes("struct")) {
+ Value.Hash type = capi.getType(a);
+ Context sub = ctx.push(type);
+ Template code = Template.load(sub.getString("struct:code"));
- var sub = ctx.push(type);
-
- //System.out.printf("struct context\n");
- //sub.dump(System.out);
-
- Template code = Template.load(sub.getText(Text.ofIdentifier("struct.template")).value());
-
- generate(sub, code);
- }
+ generate(sub, code);
}
}
// The library class itself
if (true) {
- Hash info = new Hash();
+ Map<String, Value> info = new HashMap<>();
- info.hash.put("name", new Scalar(lib.name));
- info.hash.put("Name", new Scalar(lib.name));
+ info.put("name", Scalar.ofLiteral(lib.name.name().value()));
+ info.put("Name", Scalar.ofLiteral(lib.name.name().value()));
// Translate from api library to table format
- Array funcList = new Array();
+ List<Value> funcList = new ArrayList<>();
- view.visisted(API.Match.Type.func).forEach(funcList.array::add);
+ deps.visited(API.Field.Type.func).forEach(funcList::add);
- //funcList.array.addAll(funcs.hash.values());
- //funcList.array.add(funcs.hash.get("manifold_simple_polygon_get_point"));
- //funcList.array.add(funcs.hash.get("manifold_destruct_simple_polygon"));
- info.hash.put("func", funcList);
+ info.put("func", Array.of(funcList));
- Array loadList = new Array();
- for (String l: lib.load) {
- Hash entry = new Hash();
- entry.hash.put("name", new Scalar(l));
- loadList.array.add(entry);
+ List<Value> loadList = new ArrayList<>();
+ switch (lib.options.get("library:load")) {
+ case Scalar s ->
+ loadList.add(Hash.of(Map.of("name", s)));
+ case Array a ->
+ a.value().forEach(s -> loadList.add(Hash.of(Map.of("name", s))));
+ case null, default -> {
+ }
}
- info.hash.put("load", loadList);
+ info.put("load", Array.of(loadList));
- Context ctx = new Context(typeMap, info, lib.system(), lib.system("option"), api.code(), api.system("option"), api.system("default"), api.code("default"));
- Template code = Template.load(ctx.getText(Text.ofIdentifier("library.template")).value());
+ Context ctx = new Context(deps.getTypes(), Hash.of(info), api.code(), api.code("default"), lib.options("struct"));
+ Template code = Template.load(ctx.getString("library:code"));
generate(ctx, code);
}
- // module-info
- if (false) {
- Context ctx = new Context(typeMap, lib.system(), api.system(), api.code(), api.code("default"));
- Template code = Template.load(ctx.getText(Text.ofIdentifier("module:info")).value());
-
- Hash info = new Hash();
-
- info.put("Name", "module-info");
- info.put("package", "");
-
- System.out.println(ctx.getText(Text.ofIdentifier("module:info")));
-
- StringWriter dst = new StringWriter();
- code.process(ctx.push(info), dst);
- System.out.println(dst);
-
- generate(ctx.push(info), code);
- }
}
}
public static void main(String[] args) throws IOException {
- new Generator(Path.of("bin/gen")).proto();
+ //System.setProperty("java.util.logging.config.file", "logging.properties");
+ new Generator(Path.of("bin/gen"), Path.of("api/test.api")).generate();
}
-
}
+++ /dev/null
-/*
- * Copyright (C) 2025 Michael Zucchi
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-package au.notzed.nativez.tools;
-
-import java.io.PrintStream;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.function.Function;
-import java.util.function.Predicate;
-
-public class Hash extends Value implements Data {
- static final Hash UNDEFINED = new Hash(null);
- Map<String, Value> hash;
-
- public Hash() {
- this(new HashMap<>());
- }
-
- private Hash(Map<String, Value> hash) {
- this.hash = hash;
- }
-
- @Override
- boolean defined() {
- return this != UNDEFINED;
- }
-
- @Override
- void dumper(StringBuilder sb, String indent) {
- sb.append("{\n");
- for (java.util.Map.Entry<java.lang.String, au.notzed.nativez.tools.Value> e: hash.entrySet()) {
- sb.append(indent);
- sb.append(e.getKey());
- sb.append(" => ");
- e.getValue().dumper(sb, indent + "\t");
- sb.append(",\n");
- }
- sb.append(indent);
- sb.append("}");
- }
-
- @Override
- public void dump(PrintStream out) {
- out.print(super.dumper());
- }
-
- public static Function<Hash, String> field(String name) {
- return f -> {
- Scalar value = f.getScalar(name);
- if (!value.defined())
- throw new AssertionError("missing field: " + name + " in " + f.dumper(), null);
- return value.getValue();
- };
- }
-
- public Value getValue(String key) {
- if (defined() && hash.get(key) instanceof Value s) {
- return s;
- } else {
- return Scalar.UNDEFINED;
- }
- }
-
- public Scalar getScalar(String key) {
- if (defined() && hash.get(key) instanceof Scalar s) {
- return s;
- } else {
- return Scalar.UNDEFINED;
- }
- }
-
- public Array getArray(String key) {
- if (defined() && hash.get(key) instanceof Array s) {
- return s;
- } else {
- return Array.UNDEFINED;
- }
- }
-
- public Hash getHash(String key) {
- if (defined() && hash.get(key) instanceof Hash s) {
- return s;
- } else {
- return Hash.UNDEFINED;
- }
- }
-
- @Override
- public Value getValue(Text key) {
- if (defined() && key.defined() && hash.get(key.value()) instanceof Value s) {
- return s;
- } else {
- return Scalar.UNDEFINED;
- }
- }
-
- public void put(String key, String value) {
- hash.put(key, new Scalar(value));
- }
-
- public void put(String key, Value value) {
- hash.put(key, value);
- }
-}
+++ /dev/null
-/*
- * Copyright (C) 2025 Michael Zucchi
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-package au.notzed.nativez.tools;
-
-public class Scalar extends Value {
- static final Scalar UNDEFINED = new Scalar(null);
- String value;
-
- public Scalar(String value) {
- this.value = value;
- }
-
- @Override
- boolean defined() {
- return this != UNDEFINED;
- }
-
- public String getValue() {
- return value;
- }
-
- public long getInteger() {
- return value != null ? Long.parseLong(value) : 0;
- }
-
- @Override
- void dumper(StringBuilder sb, String indent) {
- sb.append(value);
- }
-
- @Override
- public String toString() {
- return value == null ? "<undefined>" : value;
- }
-
-}
*/
package au.notzed.nativez.tools;
+import au.notzed.nativez.tools.Value.*;
+import java.io.EOFException;
import java.io.IOException;
import java.io.PrintStream;
import java.io.Reader;
import java.util.List;
/**
- * Pre-parsed template.
+ * Template processor.
+ *
+ * Uses Context for resolving templates and keys.
*/
public class Template {
List<Command> seq;
});
}
- private void append(Context ctx, Writer dst, Text txt) throws IOException {
- var val = ctx.getText(txt);
+ private boolean resolveBoolean(Context ctx, Scalar name) {
+ System.getLogger(Template.class.getName()).log(System.Logger.Level.TRACE, () -> String.format("resolve bool '%s' raw '%s'\n", name, ctx.getValue(name)));
- // TODO: maybe this should resolve things like ctx.resolveText() does
- //ctx.resolveText(txt);
+ return switch (ctx.getValue(name)) {
+ case Array array ->
+ !array.value().isEmpty();
+ case Hash hash ->
+ !hash.value().isEmpty();
+ case Scalar text ->
+ text != Scalar.UNDEFINED && !text.value().equals("0");
+ default ->
+ false;
+ };
+ }
- switch (val.type()) {
- case IDENTIFIER:
- case LITERAL:
- dst.append(val.value());
- break;
- case STRING:
- Template tmp = load(val.value());
- tmp.process(ctx, dst);
- break;
- case UNDEFINED:
- System.getLogger(Template.class.getName()).log(System.Logger.Level.ERROR, () -> "append '/" + txt + "' doesn't exist");
+ private int resolveCompare(Context ctx, Scalar p, Scalar q) throws IOException {
+ String a = ctx.expand(p);
+ String b = ctx.expand(q);
+
+ System.getLogger(Template.class.getName()).log(System.Logger.Level.DEBUG, () -> String.format("resolve compare: '%s' (%s) <> '%s' (%s)\n", a, p, b, q));
+
+ return Util.compareCasting(a, b);
+ }
+
+ private void append(Context ctx, Writer dst, Scalar txt) throws IOException {
+ ctx.expand(txt, dst);
+ }
+
+ private void invoke(Context ctx, Writer dst, Command cmd) throws IOException {
+ switch (cmd.values[0].value()) {
+ case "studlyToSnake":
+ case "camelToSnake": {
+ if (cmd.values.length == 2)
+ dst.append(Util.toSnakeCase(ctx.expand(cmd.values[1])));
+ else
+ System.getLogger(Template.class.getName()).log(System.Logger.Level.WARNING, () -> "&command requires one argument: " + cmd.dumper());
break;
}
+ case "studlyToCamel":
+ case "snakeToStudly":
+ case "snakeToCamel":
+ default:
+ System.getLogger(Template.class.getName()).log(System.Logger.Level.WARNING, () -> "&command not implemented: " + cmd.dumper());
+ }
}
public void process(Context ctx, Writer dst) throws IOException {
for (Command cmd: seq) {
+ System.getLogger(Template.class.getName()).log(System.Logger.Level.DEBUG, () -> cmd.dumper());
switch (cmd.type) {
- case LITERAL:
+ case LITERAL -> {
for (var v: cmd.values)
dst.append(v.value());
- break;
- case ISFALSE:
- if (!ctx.resolveBoolean(cmd.values[0])) {
+ }
+ case ISFALSE -> {
+ if (!resolveBoolean(ctx, cmd.values[0]))
append(ctx, dst, cmd.values[1]);
- } else if (cmd.values.length == 3) {
+ else if (cmd.values.length == 3)
append(ctx, dst, cmd.values[2]);
- }
- break;
- case INVOKE:
- // Nothing yet
- System.getLogger(Template.class.getName()).log(System.Logger.Level.WARNING, () -> "&command not implemented: " + cmd.values[0]);
- break;
- case APPEND:
+ }
+ case INVOKE -> // Nothing yet
+ invoke(ctx, dst, cmd);
+ case APPEND ->
append(ctx, dst, cmd.values[0]);
- break;
- case CMPLT:
- if (ctx.resolveCompare(cmd.values[0], cmd.values[1]) < 0) {
+ case CMPLT -> {
+ if (resolveCompare(ctx, cmd.values[0], cmd.values[1]) < 0)
append(ctx, dst, cmd.values[2]);
- } else if (cmd.values.length == 4) {
+ else if (cmd.values.length == 4)
append(ctx, dst, cmd.values[3]);
- }
- break;
- case CMPEQ:
- if (ctx.resolveCompare(cmd.values[0], cmd.values[1]) == 0) {
+ }
+ case CMPEQ -> {
+ if (resolveCompare(ctx, cmd.values[0], cmd.values[1]) == 0)
append(ctx, dst, cmd.values[2]);
- } else if (cmd.values.length == 4) {
+ else if (cmd.values.length == 4)
append(ctx, dst, cmd.values[3]);
- }
- break;
- case CMPNE:
- if (ctx.resolveCompare(cmd.values[0], cmd.values[1]) != 0) {
+ }
+ case CMPNE -> {
+ if (resolveCompare(ctx, cmd.values[0], cmd.values[1]) != 0)
append(ctx, dst, cmd.values[2]);
- } else if (cmd.values.length == 4) {
+ else if (cmd.values.length == 4)
append(ctx, dst, cmd.values[3]);
- }
- break;
- case CMPGT:
- if (ctx.resolveCompare(cmd.values[0], cmd.values[1]) > 0) {
+ }
+ case CMPGT -> {
+ if (resolveCompare(ctx, cmd.values[0], cmd.values[1]) > 0)
append(ctx, dst, cmd.values[2]);
- } else if (cmd.values.length == 4) {
+ else if (cmd.values.length == 4)
append(ctx, dst, cmd.values[3]);
- }
- break;
- case ISTRUE:
- if (ctx.resolveBoolean(cmd.values[0])) {
+ }
+ case ISTRUE -> {
+ if (resolveBoolean(ctx, cmd.values[0]))
append(ctx, dst, cmd.values[1]);
- } else if (cmd.values.length == 3) {
+ else if (cmd.values.length == 3)
append(ctx, dst, cmd.values[2]);
- }
- break;
- case FOREACH: {
+ }
+ case FOREACH -> {
Value val = ctx.getValue(cmd.values[0]);
- //System.out.printf("foreach template: %s %s -> %s\n", cmd.values[0], cmd.values[1], val);
+ System.getLogger(Template.class.getName()).log(System.Logger.Level.DEBUG, () -> " @foreach: " + val);
+
if (val instanceof Array array) {
+ String join = cmd.values.length > 2 ? ctx.expand(cmd.values[2]) : "";
boolean first = true;
- for (au.notzed.nativez.tools.Value v: array.array) {
- Context c = ctx.push((Data)v);
- var key = c.getText(cmd.values[1]);
-
- Template tmpl = load(key.value());
-
- if (!first && cmd.values.length > 2) {
- assert (cmd.values[2].type() == Text.Type.LITERAL);
- dst.append(cmd.values[2].value());
- }
- first = false;
-
- tmpl.process(c, dst);
+ for (Value v: array.value()) {
+ System.getLogger(Template.class.getName()).log(System.Logger.Level.DEBUG, () -> " @foreach[i]: " + v);
+ if (v instanceof Hash hash) {
+ if (!first)
+ dst.append(join);
+ first = false;
+
+ ctx.push(hash).expand(cmd.values[1], dst);
+ } else
+ System.getLogger(Template.class.getName()).log(System.Logger.Level.WARNING, () -> "@foreach element not hash: " + v + " from " + cmd.values[0]);
}
} else if (val instanceof Hash hash) {
- //System.out.printf("@ over hash\n");
- //hash.dump(System.out);
- Context c = ctx.push((Data)hash);
- var key = c.getText(cmd.values[1]);
- Template tmpl = load(key.value());
-
- tmpl.process(c, dst);
+ ctx.push(hash).expand(cmd.values[1], dst);
} else {
- System.getLogger(Template.class.getName()).log(System.Logger.Level.WARNING, () -> "@foreach Cannot iterate over scalar: " + (val.defined() ? (Object)val : (Object)cmd.values[0]));
+ System.getLogger(Template.class.getName()).log(System.Logger.Level.WARNING, () -> "@foreach cannot iterate over scalar: " + val + " from " + cmd.values[0]);
}
- break;
}
}
}
}
}
- static record Command(Type type, Text[] values) {
+ static record Command(Type type, Scalar[] values) {
+
+ void dump(PrintStream out) {
+ out.printf("Command: %s [", type);
+ for (var t: values)
+ out.printf(" %s", t);
+ out.println(" ]");
+ }
+
+ String dumper() {
+ StringBuilder sb = new StringBuilder("Command: ");
+ sb.append(type);
+ sb.append("[");
+ for (var t: values) {
+ sb.append(" ");
+ sb.append(t);
+ }
+ sb.append(" ]");
+ return sb.toString();
+ }
/**
* Command type.
enum Type {
LITERAL {
@Override
- boolean valid(Text[] args) {
+ boolean valid(Scalar[] args) {
return true;
}
},
ISFALSE {
@Override
- boolean valid(Text[] args) {
+ boolean valid(Scalar[] args) {
return validBoolean(args);
}
},
},
APPEND {
@Override
- boolean valid(Text[] args) {
+ boolean valid(Scalar[] args) {
return validKey(args);
}
},
CMPLT {
@Override
- boolean valid(Text[] args) {
+ boolean valid(Scalar[] args) {
return validCompare(args);
}
},
CMPEQ {
@Override
- boolean valid(Text[] args) {
+ boolean valid(Scalar[] args) {
return validCompare(args);
}
},
CMPGT {
@Override
- boolean valid(Text[] args) {
+ boolean valid(Scalar[] args) {
return validCompare(args);
}
},
ISTRUE {
@Override
- boolean valid(Text[] args) {
+ boolean valid(Scalar[] args) {
return validBoolean(args);
}
},
FOREACH {
@Override
- boolean valid(Text[] args) {
+ boolean valid(Scalar[] args) {
// {@key temp thing}
return super.valid(args) && args.length >= 2 && args.length <= 3;
}
},
CMPNE {
@Override
- boolean valid(Text[] args) {
+ boolean valid(Scalar[] args) {
return validCompare(args);
}
};
return values()[id + 1];
}
- boolean valid(Text[] args) {
- return args.length > 0 && args[0].type() == Text.Type.IDENTIFIER;
+ boolean valid(Scalar[] args) {
+ return args.length > 0 && args[0].type() == Scalar.Type.IDENTIFIER;
}
- boolean validKey(Text[] args) {
- return args.length == 1 && args[0].type() == Text.Type.IDENTIFIER;
+ boolean validKey(Scalar[] args) {
+ return args.length == 1 && args[0].type() == Scalar.Type.IDENTIFIER;
}
- boolean validCompare(Text[] args) {
+ boolean validCompare(Scalar[] args) {
// {.key val ifeq ifne}
- return args.length >= 2 && args.length <= 4 && args[0].type() == Text.Type.IDENTIFIER;
+ return args.length >= 2 && args.length <= 4 && args[0].type() == Scalar.Type.IDENTIFIER;
}
- boolean validBoolean(Text[] args) {
+ boolean validBoolean(Scalar[] args) {
// {.key ift iff}
- return args.length >= 2 && args.length <= 3 && args[0].type() == Text.Type.IDENTIFIER;
+ return args.length >= 2 && args.length <= 3 && args[0].type() == Scalar.Type.IDENTIFIER;
}
}
return cmd;
}
+ @Override
public int nextToken() throws IOException {
- switch (state) {
- case 0 -> {
- int c, last = -1;
-
- text.setLength(0);
- textDrop = 0;
-
- while ((c = read()) != -1) {
- if (last == START && (cmd = Command.Type.valueOf((char)c)) != Command.Type.LITERAL) {
- state = 1;
- textDrop = 1;
- return Tokeniser.TOK_LITERAL;
- } else {
- text.append((char)c);
- // if by line
- if (c == '\n')
- return Tokeniser.TOK_LITERAL;
+ return token = switch (state) {
+ case 0 -> {
+ int c, last = -1;
+
+ text.setLength(0);
+ textDrop = 0;
+
+ while ((c = read()) != -1) {
+ if (last == START && (cmd = Command.Type.valueOf((char)c)) != Command.Type.LITERAL) {
+ state = 1;
+ textDrop = 1;
+ yield Tokeniser.TOK_LITERAL;
+ } else {
+ text.append((char)c);
+ // if by line
+ if (c == '\n')
+ yield Tokeniser.TOK_LITERAL;
+ }
+ last = c;
}
- last = c;
+ if (text.isEmpty())
+ yield c;
+ else
+ yield Tokeniser.TOK_LITERAL;
}
- if (text.isEmpty())
- return c;
- else
- return Tokeniser.TOK_LITERAL;
- }
- case 1 -> {
- state = 2;
- return TOK_COMMAND;
- }
- case 2 -> {
- int c = super.nextToken();
+ case 1 -> {
+ state = 2;
+ yield TOK_COMMAND;
+ }
+ case 2 -> {
+ int c = super.nextToken();
- if (c == FINISH)
- state = 0;
- return c;
- }
- default ->
- throw new AssertionError(state);
- }
+ if (c == FINISH)
+ state = 0;
+ yield c;
+ }
+ default ->
+ throw new AssertionError(state);
+ };
}
}
in.close();
}
- Command newCommand(Command.Type type, List<Text> values) throws IOException {
- Text[] array = values.toArray(Text[]::new);
+ Command newCommand(Command.Type type, List<Scalar> values) throws IOException {
+ Scalar[] array = values.toArray(Scalar[]::new);
if (!type.valid(array))
throw new IOException(in.fatalMessage(0, "Command arguments invalid for: " + type));
return new Command(type, array);
}
+ // would be nice if it could strip any common prefix
+ // from the input(e.g. from the first line)
Template load() throws IOException {
int t;
List<Command> seq = new ArrayList<>();
- List<Text> texts = new ArrayList<>();
+ List<Scalar> texts = new ArrayList<>();
- // would be nice if it could strip any common prefix
- // from the input(e.g. from the first line)
while ((t = in.nextToken()) != -1) {
switch (t) {
case Tokeniser.TOK_LITERAL -> {
- texts.add(new Text(Text.Type.LITERAL, in.getText()));
+ texts.add(new Scalar(Scalar.Type.LITERAL, in.getText()));
}
case TemplateTokeniser.TOK_COMMAND -> {
Command.Type type = in.getCommand();
texts.clear();
}
- while ((t = in.nextToken()) != -1 && t != '}') {
+ while ((t = in.nextToken()) != -1 && t != '}')
texts.add(in.getText(t));
- }
+
+ if (t == -1)
+ throw new EOFException(in.fatalMessage(t, "Missing '}'"));
+
seq.add(newCommand(type, texts));
texts.clear();
}
return new Template(seq);
}
}
-
- public static void main(String[] args) throws IOException {
- Template t = load(Path.of("test.tmp"));
-
- t.seq.forEach(c -> {
- System.out.println(c.type);
- for (var v: c.values)
- System.out.printf("\t%s\n", v);
- });
- }
-
}
+++ /dev/null
-/*
- * Copyright (C) 2025 Michael Zucchi
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-package au.notzed.nativez.tools;
-
-/**
- *
- */
-public record Text(Type type, String value) {
-
- public static final Text UNDEFINED = new Text(Type.UNDEFINED, "");
- public static final Text TRUE = new Text(Type.LITERAL, "1");
- public static final Text FALSE = new Text(Type.LITERAL, "0");
-
- public enum Type {
- IDENTIFIER,
- STRING,
- LITERAL,
- PATTERN,
- UNDEFINED;
- }
-
- public boolean defined() {
- return this != UNDEFINED;
- }
-
- public Value toValue() {
- if (defined())
- return new Scalar(value);
- else
- return Scalar.UNDEFINED;
- }
-
- public static Text ofValue(Value value) {
- if (value.defined() && value instanceof Scalar s)
- return ofLiteral(s.getValue());
- else
- return UNDEFINED;
- }
-
- public static Text ofIdentifier(String value) {
- return new Text(Type.IDENTIFIER, value);
- }
-
- public static Text ofString(String value) {
- return new Text(Type.STRING, value);
- }
-
- public static Text ofLiteral(String value) {
- return new Text(Type.LITERAL, value);
- }
-
- public static Text ofPattern(String value) {
- return new Text(Type.PATTERN, value);
- }
-}
*/
package au.notzed.nativez.tools;
+import au.notzed.nativez.tools.Value.*;
+import java.io.EOFException;
import java.io.IOException;
import java.io.Reader;
-import java.lang.foreign.MemorySegment;
-import java.nio.file.Path;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Supplier;
/**
- * Tokeniser for a few file formats.
+ * Shared tokeniser.
* <p>
* <ul>
* <li>'...' string, escapes \[rnt] or \.
* <li>"..." string, escapes \[rnt] or \.
* <li>/.../ regex string, no escapes
* <li>@...@ regex string, no escapes
- * <li><[ ... ]> literal string (nested - maybe it shouldn't be)
- * <li><{ ... }> string (nested)
+ * <li>{@code<[ ... ]>} literal string (nested)
+ * <li>{@code<{ ... }>} string (nested)
* <li>[::identifier::]+ identifier
* <li>[.] character
* </ul>
*/
public class Tokeniser implements AutoCloseable {
- // Really want paths as well?
+ // Input context
List<Reader> readers = new LinkedList<>();
- final int commentChar = '#';
Reader src;
int lastc = -1;
+ // Current token value
StringBuilder text = new StringBuilder();
int textDrop = 0;
- // For diagnostics only
+ int token = Integer.MAX_VALUE;
+ // For diagnostics - current line
StringBuilder line = new StringBuilder();
int lineNo = 1;
-
- // Not linked up yet: for diagnostics mostly
- static class Source {
- Reader src;
- Path file; // if known
- int lineNo = 1;
- StringBuilder line = new StringBuilder();
-
- public Source(String txt) {
- }
-
- public Source(Path file) {
- this.src = src;
- this.file = file;
- }
- }
+ final int commentChar = '#';
public Tokeniser(Reader src) {
this.src = src;
StringBuilder marker = new StringBuilder();
StringBuilder line = new StringBuilder(this.line);
+ // TODO: get filename
try {
int c;
while ((c = readInternal()) != -1 && c != '\n')
}
String fatalMessage(String got, String why) {
-
System.getLogger(Tokeniser.class.getName()).log(System.Logger.Level.ERROR, logMessage(why));
-
- // TODO: get filename
- //System.getLogger(Tokeniser.class.getName()).log(System.Logger.Level.ERROR, () -> String.format("template:%d:%d: %s%s", row, col, why, got));
- //System.getLogger(Tokeniser.class.getName()).log(System.Logger.Level.ERROR, () -> String.format(" %5d | %s\n", row, line));
- //System.getLogger(Tokeniser.class.getName()).log(System.Logger.Level.ERROR, () -> String.format(" %5d | %s\n", row, marker));
return (why + " got '" + got + "' (" + lineNo + "): " + line);
}
}
// identifier characters bitmap
- static final long ida = 0x87ffe85200000000L;
- static final long idb = 0x07fffffe87ffffffL;
-
- /*
- static{
- long a = 0, b = 0;
- for (int i = 0; i < 64; i++) {
- int j = i + 64;
- if ("!$&+-./:?".indexOf((char)i) >= 0
- || (i >= '0' && i <= '9'))
- a |= 1L << i;
- if ("@_".indexOf((char)(j)) >= 0
- || (j >= 'a' && j <= 'z')
- || (j >= 'A' && j <= 'Z'))
- b |= 1L << i;
- }
- //ida = a;
- //idb = b;
- System.out.printf(" static final long ida = 0x%016xL;\n", a);
- System.out.printf(" static final long idb = 0x%016xL;\n", b);
- }*/
+ static final long ID0 = 0x87ffe85200000000L;
+ static final long ID1 = 0x07fffffe87ffffffL;
+ //static final long ID2 = 0x8400805200000000L;
boolean isIdentifier(int c) {
return c < 64
- ? (ida & (1L << c)) != 0
- : (idb & (1L << c - 64)) != 0;
+ ? (ID0 & (1L << c)) != 0
+ : (ID1 & (1L << c - 64)) != 0;
}
+ // This breaks shit unless I want to support numbers too
+ //boolean isIdentifierFirst(int c) {
+ // return c < 64
+ // ? (ID2 & (1L << c)) != 0
+ // : (ID1 & (1L << c - 64)) != 0;
+ //}
+
/*
* Maybe '' should also be a literal string (not parsed) like perl.
*/
static final int TOK_LITERAL = -7;
static final int TOK_LAST = -7;
- int literal(int a, int b) throws IOException {
+ /**
+ * Is a simple string token.
+ * identifier, 'string', "string", or /regex/.
+ *
+ * @param t
+ * @return
+ */
+ static protected boolean isStringToken(int t) {
+ return t == TOK_IDENTIFIER
+ || t == TOK_STRING_SQ
+ || t == TOK_STRING_DQ
+ || t == TOK_REGEX;
+ }
+
+ protected int literal(int a, int b) throws IOException {
int c, last = -1;
int depth = 1;
last = c;
}
- if (depth != 0)
- throw new IOException(fatalMessage(c, "Truncated <" + a + " quoted " + b + ">"));
+ System.getLogger(Tokeniser.class.getName()).log(System.Logger.Level.DEBUG, () -> "Token literal quoted: " + text.toString());
- textDrop = (c == -1 ? 0 : 1);
+ if (c == -1)
+ throw new EOFException(fatalMessage(c, "Truncated <" + a + " quoted " + b + ">"));
- System.getLogger(Tokeniser.class.getName()).log(System.Logger.Level.DEBUG, () -> "Token literal quoted: " + text.toString());
+ textDrop = 1;
return b == ']' ? TOK_LITERAL : TOK_QUOTED;
}
- int regex(int q) throws IOException {
+ protected int regex(int q) throws IOException {
int c;
while ((c = read()) != -1 && c != q) {
text.append((char)c);
}
- if (c != q)
- throw new IOException(fatalMessage(c, "Truncated " + q + "regex" + q));
System.getLogger(Tokeniser.class.getName()).log(System.Logger.Level.DEBUG, () -> "Token regex: " + text.toString());
+ if (c != q)
+ throw new EOFException(fatalMessage(c, "Truncated " + q + "regex" + q));
return TOK_REGEX;
}
- String regex() throws IOException {
- int c = nextToken();
- if (c == TOK_REGEX)
- return getText();
- else
- throw new IOException(fatalMessage(c, "Expecting /regex/ or @regex@"));
- }
-
- int quoted(int q) throws IOException {
+ protected int quoted(int q) throws IOException {
int c;
while ((c = read()) != -1 && c != q) {
if (c == '\\') {
text.append((char)c);
}
System.getLogger(Tokeniser.class.getName()).log(System.Logger.Level.DEBUG, () -> "Token quoted: " + text.toString());
+ if (c != q)
+ throw new EOFException(fatalMessage(c, "Truncated " + q + "string" + q));
return q == '"' ? TOK_STRING_DQ : TOK_STRING_SQ;
}
: text.substring(0, text.length() - textDrop);
}
- public Text getText(int t) throws IOException {
- Text.Type type = switch (t) {
+ public Scalar getText(int t) throws IOException {
+ Scalar.Type type = switch (t) {
case Tokeniser.TOK_IDENTIFIER ->
- Text.Type.IDENTIFIER;
+ Scalar.Type.IDENTIFIER;
case Tokeniser.TOK_STRING_DQ, Tokeniser.TOK_QUOTED ->
- Text.Type.STRING;
+ Scalar.Type.STRING;
case Tokeniser.TOK_STRING_SQ, Tokeniser.TOK_LITERAL ->
- Text.Type.LITERAL;
+ Scalar.Type.LITERAL;
case Tokeniser.TOK_REGEX ->
- Text.Type.PATTERN;
+ Scalar.Type.PATTERN;
default ->
throw new IOException(fatalMessage(t, "Expected string or ident"));
};
- return new Text(type, getText());
+ return new Scalar(type, getText());
}
- // Parse a specific token only
- public void nextToken(char[] seq) throws IOException {
+ // Parse a specific sequence only, skipping lws
+ public void token(char[] seq) throws IOException {
int c = skipLWS();
int i = 0;
System.getLogger(Tokeniser.class.getName()).log(System.Logger.Level.DEBUG, () -> "Token seq: " + new String(seq));
}
- public void nextToken(char c) throws IOException {
+ /**
+ * Check the next token.
+ *
+ * @param n
+ * @throws IOException on i/o error or if the type doesn't match.
+ */
+ public void token(int n) throws IOException {
int t;
- if ((t = skipLWS()) != c)
- throw new IOException(fatalMessage(t, "Expected:" + c));
- System.getLogger(Tokeniser.class.getName()).log(System.Logger.Level.DEBUG, () -> "Token char: " + (char)c);
+ if ((t = nextToken()) != n)
+ throw new IOException(fatalMessage(t, "Expected:" + n));
+ System.getLogger(Tokeniser.class.getName()).log(System.Logger.Level.DEBUG, () -> "Token char: " + (char)n);
+ }
+
+ public int getToken() {
+ return token;
}
public int nextToken() throws IOException {
text.setLength(0);
textDrop = 0;
- if (c == -1) {
- return c;
- } else if (c == '\'' || c == '"') {
- return quoted(c);
- } else if (c == '/' || c == '@') {
- return regex(c);
- } else if (c == '<') {
- c = read();
- if (c == '{')
- return literal('{', '}');
- else if (c == '[')
- return literal('[', ']');
- else
- throw new IOException("Not valid literal, must be <[]> or <{}>");
- } else if (isIdentifier(c)) {
- do {
- text.append((char)c);
- } while ((c = read()) != -1 && isIdentifier(c));
- if (c != -1)
- push(c);
- System.getLogger(Tokeniser.class.getName()).log(System.Logger.Level.DEBUG, () -> "Token ident: " + text.toString());
- return TOK_IDENTIFIER;
- } else {
- int l = c;
- System.getLogger(Tokeniser.class.getName()).log(System.Logger.Level.DEBUG, () -> "Token char: " + (char)l);
- return c;
- }
+ return token = switch (c) {
+ case -1 ->
+ c;
+ case '\'', '"' ->
+ quoted(c);
+ case '/', '@' ->
+ regex(c);
+ case '<' ->
+ switch (read()) {
+ case '{' ->
+ literal('{', '}');
+ case '[' ->
+ literal('[', ']');
+ default ->
+ throw new IOException("Not valid literal, must be <[]> or <{}>");
+ };
+
+ default -> {
+ if (isIdentifier(c)) {
+ do {
+ text.append((char)c);
+ } while ((c = read()) != -1 && isIdentifier(c));
+ if (c != -1)
+ push(c);
+ System.getLogger(Tokeniser.class.getName()).log(System.Logger.Level.DEBUG, () -> "Token ident: " + text.toString());
+ yield TOK_IDENTIFIER;
+ } else {
+ int l = c;
+ System.getLogger(Tokeniser.class.getName()).log(System.Logger.Level.DEBUG, () -> "Token char: " + (char)l);
+ yield c;
+ }
+ }
+ };
}
/*
public static void main(String[] args) throws IOException {
- if (false) {
- long a = 0, b = 0;
- for (int i = 0; i < 64; i++) {
- int j = i + 64;
- if ("!$&+-./:?".indexOf((char)i) >= 0
- || (i >= '0' && i <= '9'))
- a |= 1L << i;
- if ("@_".indexOf((char)(j)) >= 0
- || (j >= 'a' && j <= 'z')
- || (j >= 'A' && j <= 'Z'))
- b |= 1L << i;
- }
- System.out.printf(" %016x %016x\n", a, b);
- return;
- }
-
- try (FileReader r = new FileReader("test.api")) {
- int t;
-
- Tokeniser tok = new Tokeniser(r);
- while ((t = tok.nextToken()) != tok.TOK_EOF) {
- System.out.printf("tok: %d %c '%s'\n", t, t >= 0 ? t : '.', tok.text());
- }
+ long a = 0, b = 0, c = 0;
+ for (int i = 0; i < 64; i++) {
+ int j = i + 64;
+ if ("!$&+-./:?".indexOf((char)i) >= 0
+ || (i >= '0' && i <= '9'))
+ a |= 1L << i;
+ if ("@_".indexOf((char)(j)) >= 0
+ || (j >= 'a' && j <= 'z')
+ || (j >= 'A' && j <= 'Z'))
+ b |= 1L << i;
+ if ("!$&/:?".indexOf((char)i) >= 0)
+ c |= 1L << i;
}
-
+ System.out.printf(" %016xL %016xL %016xL\n", a, b, c);
+ return;
}*/
}
*/
package au.notzed.nativez.tools;
-import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.regex.Pattern;
public class Util {
return capify(name, false);
}
- public static void main(String[] args) {
- String[] names = {
- "do_thing_blah",
- "a_b",
- "_foo_bar",
- "foobar"
- };
- String[] studly = {
- "DoThingBlah",
- "AB",
- "FooBar",
- "Foobar"
- };
- String[] camel = {
- "doThingBlah",
- "aB",
- "fooBar",
- "foobar"
- };
+ static String toSnakeCase(String name) {
+ StringBuilder sb = new StringBuilder();
- for (int i = 0; i < names.length; i++) {
- String s = names[i];
- String a = toStudlyCaps(s);
- String b = toCamelCase(s);
+ for (int i=0;i<name.length();i++) {
+ char c = name.charAt(i);
- System.out.printf("%12s %12s %12s\n", s, a, b);
- assert (a.equals(studly[i]));
- assert (b.equals(camel[i]));
+ if (Character.isUpperCase(c)) {
+ c = Character.toLowerCase(c);
+ if (!sb.isEmpty())
+ sb.append("_");
+ }
+ sb.append(c);
+ }
+ return sb.toString();
+ }
+
+ static final Predicate<String> isNumber = Pattern.compile("[-+]?\\d+").asMatchPredicate();
+
+ /**
+ * Compare trimmed values as strings or numbers.
+ * <p>
+ * If both a and b are numbers compare as numbers otherwise compare as strings.
+ *
+ * @param a
+ * @param b
+ * @return Result of a.compareTo(b) or Long.compare() as appropriate.
+ */
+ static int compareCasting(String a, String b) {
+ a = a.trim();
+ b = b.trim();
+ if (isNumber.test(a) && isNumber.test(b)) {
+ return Long.compare(Long.parseLong(a), Long.parseLong(b));
+ } else {
+ return a.compareTo(b);
}
}
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
-public abstract class Value {
+@FunctionalInterface
+public interface Value {
- abstract void dumper(StringBuilder sb, String indent);
+ /**
+ * Retrieve a value by reference.
+ * <dl>
+ * <dt>Scalar
+ * <dd>Always returns Scalar.UNDEFINED.
+ * <dt>Hash
+ * <dd>Retrieves Value based on key, or Scalar.UNDEFINED if not present.
+ * <dt>Array
+ * <dd>Retrieves value based on range reference.
+ * </dl>
+ *
+ * @param key Simple key value.
+ * @return A Value or Scalar.UNDEFINED.
+ */
+ public Value getValue(String key);
- abstract boolean defined();
+ /**
+ * Retrieve a value matcing a type.
+ *
+ * @param <T>
+ * @param klass
+ * @param key
+ * @return The value or null if no such value exists.
+ */
+ public default <T extends Value> T getValue(Class<T> klass, String key) {
+ Value val = getValue(key);
+ if (val != Scalar.UNDEFINED && klass.isInstance(val))
+ return klass.cast(val);
+ return null;
+ }
+
+ /**
+ * Get a value as a string.
+ *
+ * @param key
+ * @return The string value if it is a Scalar and defined, otherwise <code>null</code>
+ */
+ public default String getString(String key) {
+ if (getValue(key) instanceof Scalar s && s.defined())
+ return s.value();
+ return null;
+ }
+
+ /**
+ * Resolve a nested reference.
+ * <p>
+ * Calls getValue() on each element of path in turn.
+ *
+ * @param path Sequence of keys.
+ * @return
+ */
+ public default Value resolve(String... path) {
+ Value val = this;
+
+ for (String p: path)
+ val = val.getValue(p);
+
+ return val;
+ }
+
+ public default String resolveString(String... path) {
+ if (resolve(path) instanceof Scalar s && s != Scalar.UNDEFINED)
+ return s.value();
+ throw new NoSuchElementException(String.join("/", path));
+ }
+
+ public default boolean defined() {
+ return this != Scalar.UNDEFINED;
+ }
+
+ public default void dump(PrintStream out, String prefix) {
+ }
+
+ public default void dump(PrintStream out) {
+ dump(out, "");
+ out.println();
+ }
+
+ /**
+ * Scalar type (string).
+ */
+ public record Scalar(Type type, String value) implements Value {
+ public static final Scalar UNDEFINED = new Scalar(Type.UNDEFINED, "");
+ public static final Scalar TRUE = new Scalar(Type.LITERAL, "1");
+ public static final Scalar FALSE = new Scalar(Type.LITERAL, "0");
+
+ public enum Type {
+ IDENTIFIER,
+ STRING,
+ LITERAL,
+ PATTERN,
+ UNDEFINED;
+ }
+
+ @Override
+ public Value getValue(String key) {
+ return UNDEFINED;
+ }
+
+ public void dump(PrintStream out, String prefix) {
+ out.print(value());
+ }
+
+ @Override
+ public String toString() {
+ return switch (type) {
+ case IDENTIFIER ->
+ value;
+ case STRING ->
+ value.contains("\n")
+ ? "<{" + value + "}>"
+ : "\"" + value + "\"";
+ case LITERAL ->
+ value.contains("\n")
+ ? "<[" + value + "]>"
+ : "'" + value + "'";
+ case PATTERN ->
+ value.contains("/")
+ ? "@" + value + "@"
+ : "/" + value + "/";
+ case UNDEFINED ->
+ "#undefined";
+ };
+ }
+
+ public static Scalar ofIdentifier(String value) {
+ return new Scalar(Type.IDENTIFIER, value);
+ }
+
+ public static Scalar ofString(String value) {
+ return new Scalar(Type.STRING, value);
+ }
+
+ public static Scalar ofLiteral(String value) {
+ return new Scalar(Type.LITERAL, value);
+ }
+
+ public static Scalar ofPattern(String value) {
+ return new Scalar(Type.PATTERN, value);
+ }
+ }
+
+ public record Array(List<Value> value) implements Value {
+
+ public static Array of(List<Value> list) {
+ return new Array(list);
+ }
+
+ @Override
+ public Value getValue(String key) {
+ return range(value(), key);
+ }
+
+ @Override
+ public void dump(PrintStream out, String prefix) {
+ boolean first = true;
+ String next = prefix + "\t";
+
+ out.print("[\n");
+ for (Value v: value()) {
+ if (!first)
+ out.print(",\n");
+ first = false;
+ out.print(next);
+ v.dump(out, next);
+ }
+ out.print("\n");
+ out.print(prefix);
+ out.print("]");
+ }
+ }
+
+ public record Hash(Map<String, Value> value) implements Value {
+
+ public static Hash of(Map<String, Value> map) {
+ return new Hash(map);
+ }
+
+ @Override
+ public Value getValue(String key) {
+ return value().getOrDefault(key, Scalar.UNDEFINED);
+ }
+
+ public void dump(PrintStream out, String prefix) {
+ boolean first = true;
+ String next = prefix + "\t";
- String dumper() {
- StringBuilder sb = new StringBuilder();
- dumper(sb, "");
- return sb.toString();
+ out.print("{\n");
+ for (var e: value().entrySet()) {
+ if (!first)
+ out.print(",\n");
+ first = false;
+ out.print(next);
+ out.print(e.getKey());
+ out.print(" => ");
+ e.getValue().dump(out, next);
+ }
+ out.print("\n");
+ out.print(prefix);
+ out.print("}");
+ }
}
- void dump(PrintStream out) {
- StringBuilder sb = new StringBuilder();
- dumper(sb, "");
- out.print(sb);
+ public static Value resolve(Value base, String key) {
+ return switch (base) {
+ case Array array ->
+ range(array.value(), key);
+ case Hash hash ->
+ hash.value().get(key);
+ default ->
+ Scalar.UNDEFINED;
+ };
+ }
+
+ /**
+ * Range format.
+ * <p>
+ * start:end
+ * index
+ * <p>
+ * "0:4" number range exclusive
+ * "1" specific entry from start
+ * "-1" specific entry from end
+ * "3:-" skip start to end
+ * "-3:-" last 3 values
+ * <p>
+ * TODO: just return undefined for out of range values
+ *
+ * @param key
+ * @return
+ */
+ private static Value range(List<Value> list, String key) {
+ int idx = key.indexOf(':');
+
+ if (idx < 0) {
+ idx = Integer.parseInt(key);
+ idx = idx >= 0 ? idx : list.size() + idx;
+
+ return Integer.compareUnsigned(idx, list.size()) < 0 ? list.get(idx) : Scalar.UNDEFINED;
+ }
+
+ String a = key.substring(0, idx);
+ String b = key.substring(idx + 1);
+
+ int start = a.equals("") || a.equals("-") ? 0 : Integer.parseInt(a);
+ int end = b.equals("") || b.equals("-") ? list.size() : Integer.parseInt(b);
+
+ start = start < 0 ? list.size() + start : start;
+ end = end < 0 ? list.size() + end : end;
+
+ start = Math.clamp(start, 0, list.size());
+ end = Math.clamp(end, 0, list.size());
+
+ if (start > end)
+ throw new IllegalArgumentException("Invalid array reference: " + key + " start=" + start + " end= " + end);
+
+ return Array.of(list.subList(start, end));
}
static class ValueParser implements AutoCloseable {
switch (t) {
case Tokeniser.TOK_STRING_DQ:
case Tokeniser.TOK_STRING_SQ:
+ // Identifiers are really just simple literals in these files, not reference names.
+ // This avoids nubmers being mis-identified as well (should fix tokeniser I guess).
case Tokeniser.TOK_IDENTIFIER:
- return new Scalar(in.getText());
+ return Scalar.ofLiteral(in.getText());
+ //case Tokeniser.TOK_IDENTIFIER:
+ // return Text.ofIdentifier(in.getText());
case '{':
- Hash hash = new Hash();
+ Map<String, Value> hash = new HashMap<>();
while ((t = in.nextToken()) != -1 && t != '}') {
if (t != Tokeniser.TOK_IDENTIFIER && t != Tokeniser.TOK_STRING_SQ && t != Tokeniser.TOK_STRING_DQ)
throw new IOException(in.fatalMessage(t, "Expecting string or name"));
String key = in.getText();
- Value val;
- in.nextToken(assoc);
- t = in.nextToken();
- val = readValue(t);
- hash.hash.put(key, val);
+ in.token(assoc);
+ hash.put(key, readValue(in.nextToken()));
t = in.nextToken();
if (t == '}')
break;
else if (t != ',')
throw new IOException("Expecting '}' or ','");
}
- return hash;
+ return Value.Hash.of(hash);
case '[':
- Array array = new Array();
+ List<Value> array = new ArrayList<>();
while ((t = in.nextToken()) != -1 && t != ']') {
- array.array.add(readValue(t));
+ array.add(readValue(t));
t = in.nextToken();
if (t == ']')
break;
else if (t != ',')
throw new IOException("Expecting '}' or ','");
}
- return array;
+ return Value.Array.of(array);
default:
throw new IOException(in.fatalMessage(t, "Unexpected token"));
}
--- /dev/null
+/*
+ * Copyright (C) 2025 Michael Zucchi
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package au.notzed.nativez.tools;
+
+import au.notzed.nativez.tools.Value.*;
+import static au.notzed.nativez.tools.Data.*;
+import java.util.Map;
+import org.junit.jupiter.api.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class ContextTest {
+
+ @Test
+ public void lookup() {
+ Context ctx = new Context(types(), system());
+
+ assertSystem(ctx);
+ }
+
+ void assertLiteral(Context ctx, String key, String value) {
+ Value res = ctx.getValue(Scalar.ofIdentifier(key));
+
+ assertInstanceOf(Scalar.class, res);
+ assertInstanceOf(Scalar.class, res);
+ assertEquals(value, ((Scalar)res).value());
+ }
+
+ void assertSystem(Context ctx) {
+ Value a, b;
+
+ a = ctx.getValue("package");
+ b = ctx.getValue("module");
+
+ assertEquals(((Scalar)a).value(), ((Scalar)b).value());
+ assertEquals(a, b);
+ }
+
+ @Test
+ public void pathLookup() {
+ Context ctx = new Context(types(), system());
+
+ ctx = ctx.push(struct());
+
+ assertSystem(ctx);
+
+ assertLiteral(ctx, "name", "Object");
+ assertLiteral(ctx, "fields/0/name", "x");
+ assertLiteral(ctx, "fields/1/name", "y");
+ assertLiteral(ctx, "fields/2/name", "z");
+ }
+
+ @Test
+ public void parentLookup() {
+ Context ctx = new Context(types(), Value.Hash.of(Map.of("item", Scalar.ofLiteral("level0"))));
+
+ ctx = ctx.push(Value.Hash.of(Map.of("item", Scalar.ofLiteral("level1"))));
+ ctx = ctx.push(Value.Hash.of(Map.of("item", Scalar.ofLiteral("level2"))));
+
+ assertSystem(ctx);
+
+ assertLiteral(ctx, "item", "level2");
+ assertLiteral(ctx, "../item", "level1");
+ assertLiteral(ctx, "../../item", "level0");
+
+ Context sub = ctx;
+ assertThrows(IllegalArgumentException.class, () -> sub.getValue("../../../item"));
+ }
+
+ @Test
+ public void iteratePush() {
+ Context ctx = new Context(types(), system());
+
+ ctx = ctx.push(struct());
+
+ assertInstanceOf(Array.class, ctx.getValue("fields"));
+ Array fields = (Array)ctx.getValue("fields");
+
+ int i = 0;
+ String[] names = {"x", "y", "z"};
+ String[] jtype = {"double", "float", "int"};
+ for (Value v: fields.value()) {
+ assertInstanceOf(Hash.class, v);
+
+ Hash h = (Hash)v;
+ Context sub = ctx.push(h);
+
+ assertSystem(sub);
+ assertLiteral(sub, "name", names[i]);
+ assertLiteral(sub, "jtype", jtype[i]);
+
+ i++;
+ }
+ }
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2025 Michael Zucchi
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package au.notzed.nativez.tools;
+
+import au.notzed.nativez.tools.Value.Scalar;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+public class Data {
+ static Value.Array arrayOfStrings(String... list) {
+ return Value.Array.of(Stream.of(list).map(Scalar::ofLiteral).map(Value.class::cast).toList());
+ }
+
+ static Value.Hash hashStrings(Map<String, Value> map, String... list) {
+ for (int i = 0; i < list.length; i += 2)
+ map.put(list[i], Scalar.ofLiteral(list[i + 1]));
+ return Value.Hash.of(map);
+ }
+
+ static Value.Hash hashOfStrings(String... list) {
+ Map<String, Value> map = new HashMap<>();
+ for (int i = 0; i < list.length; i += 2)
+ map.put(list[i], Scalar.ofLiteral(list[i + 1]));
+ return Value.Hash.of(map);
+ }
+
+ static Function<String,Value> types() {
+ return Value.Hash.of(
+ Map.of(
+ "f32",
+ Value.Hash.of(Map.of("jtype", Scalar.ofLiteral("float"))),
+ "f64",
+ Value.Hash.of(Map.of("jtype", Scalar.ofLiteral("double"))),
+ "i32",
+ Value.Hash.of(Map.of("jtype", Scalar.ofLiteral("int"))))
+ )::getValue;
+ }
+
+ static Value.Hash system() {
+ Map<String, Value> system = new HashMap<>();
+
+ system.put("package", Scalar.ofIdentifier("module"));
+ system.put("module", Scalar.ofLiteral("package.name"));
+
+ return Value.Hash.of(system);
+ }
+
+ static Value.Hash struct() {
+ Map<String, Value> struct = new HashMap<>();
+
+ hashStrings(struct, "name", "Object", "size", "96");
+ struct.put("true?", Scalar.TRUE);
+ struct.put("false?", Scalar.FALSE);
+ struct.put("result", hashOfStrings("deref", "void", "ctype", "const void", "type", "void"));
+
+ List<Value> fields = new ArrayList<>();
+
+ fields.add(hashOfStrings("name", "x", "size", "64", "offset", "0", "deref", "f64"));
+ fields.add(hashOfStrings("name", "y", "size", "64", "offset", "64", "deref", "f32"));
+ fields.add(hashOfStrings("name", "z", "size", "64", "offset", "128", "deref", "i32"));
+
+ struct.put("fields", Value.Array.of(fields));
+
+ return Value.Hash.of(struct);
+ }
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2025 Michael Zucchi
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package au.notzed.nativez.tools;
+
+import static au.notzed.nativez.tools.Data.*;
+import java.io.IOException;
+import java.io.StringWriter;
+import org.junit.jupiter.api.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class TemplateTest {
+
+ String process(Context ctx, String text) {
+ try (StringWriter dst = new StringWriter()) {
+ assertDoesNotThrow(() -> Template.load(text).process(ctx, dst));
+ return dst.toString();
+ } catch (IOException ex) {
+ throw new RuntimeException();
+ }
+ }
+
+ void process(Context ctx, String... text) {
+ for (int i = 0; i < text.length; i += 2) {
+ String a = text[i];
+ String b = text[i + 1];
+ try (StringWriter dst = new StringWriter()) {
+ assertDoesNotThrow(() -> Template.load(a).process(ctx, dst));
+
+ assertEquals(b, dst.toString(), () -> String.format("Template: '%s' != '%s'", a, b));
+ } catch (IOException ex) {
+ throw new RuntimeException();
+ }
+ }
+ }
+
+ @Test
+ public void append() {
+ Context ctx = new Context(types(), system());
+ String a = process(ctx, "{/package}");
+ String b = process(ctx, "{/module}");
+
+ assertEquals("package.name", a);
+ assertEquals("package.name", b);
+ }
+
+ @Test
+ public void bool() {
+ Context ctx = new Context(types(), hashOfStrings("id-yes", "yes", "id-no", "no"), struct());
+ String[] tests = {
+ "{?true? 'yes' 'no'}", "yes",
+ "{?false? 'no' 'yes'}", "yes",
+ "{!true? 'no' 'yes'}", "yes",
+ "{!false? 'yes' 'no'}", "yes",
+ "{?true? 'yes'}", "yes",
+ "{?false? 'no'}", "",
+ "{!true? 'no'}", "",
+ "{!false? 'yes'}", "yes",
+ "{?true? id-yes id-no}", "yes",
+ "{?false? id-no id-yes}", "yes",
+ "{!true? id-no id-yes}", "yes",
+ "{!false? id-yes id-no}", "yes",
+ "{?true? id-yes}", "yes",
+ "{?false? id-no}", "",
+ "{!true? id-no}", "",
+ "{!false? id-yes}", "yes",
+ "{!bob 'bob'}", "bob",
+ "{?bob 'bob'}", "",
+ "{!bob 'bob' 'jan'}", "bob",
+ "{?bob 'bob' 'jan'}", "jan"
+ };
+
+ process(ctx, tests);
+ }
+
+ @Test
+ public void compare() {
+ Context ctx = new Context(types(), hashOfStrings("id-yes", "yes", "id-no", "no", "id-a", "a", "id-b", "b", "one", "1", "ten", "10", "zero", "0"), struct());
+
+ process(ctx,
+ "{=id-yes 'yes' 'is' 'isnot'}", "is",
+ "{=id-yes 'yes' 'is'}", "is",
+ "{=id-no 'yes' 'is' 'isnot'}", "isnot",
+ "{=id-no 'yes' 'is'}", "",
+ "{~id-yes 'yes' 'is' 'isnot'}", "isnot",
+ "{~id-yes 'yes' 'is'}", "",
+ "{~id-no 'yes' 'is' 'isnot'}", "is",
+ "{~id-no 'yes' 'is'}", "is",
+ "{<id-a 'b' 'true' 'false'}", "true",
+ "{<id-a 'b' 'true'}", "true",
+ "{<id-b 'b' 'true' 'false'}", "false",
+ "{<id-b 'b' 'true'}", "",
+ "{>id-a 'b' 'true' 'false'}", "false",
+ "{>id-a 'b' 'true'}", "",
+ "{>id-b 'b' 'true' 'false'}", "false",
+ "{>id-b 'b' 'true'}", "",
+ "{=id-yes \"{/id-yes}\" id-a id-b}", "a",
+ "{=id-yes id-no id-a id-b}", "b",
+ "{=one '1' 'is' 'isnot'}", "is",
+ "{=one '10' 'is' 'isnot'}", "isnot",
+ "{<zero '-1' 'is' 'isnot'}", "isnot",
+ "{<zero ten 'is' 'isnot'}", "is",
+ "{>zero '-1' 'is' 'isnot'}", "is",
+ "{>zero ten 'is' 'isnot'}", "isnot"
+ );
+ }
+
+ @Test
+ public void foreach1() {
+ Context ctx = new Context(types(), struct());
+
+ process(ctx,
+ "{@result deref}", "void",
+ "{@result ctype}", "const void");
+ }
+
+ @Test
+ public void foreachArray() {
+ Context ctx = new Context(types(), struct());
+
+ process(ctx,
+ "{@fields name ', '}", "x, y, z",
+ "{@fields name}", "xyz",
+ "{@fields \"({/offset}+{/size})\" ' '}", "(0+64) (64+64) (128+64)"
+ );
+ }
+
+ @Test
+ public void foreachRange() {
+ Context ctx = new Context(types(), struct());
+
+ process(ctx,
+ "{@fields/0:1 name ', '}", "x",
+ "{@fields/0:2 name ', '}", "x, y",
+ "{@fields/0:1 name}", "x",
+ "{@fields/0:2 name}", "xy",
+ "{@fields/1:- name}", "yz",
+ "{@fields/-1:- name}", "z"
+ );
+ }
+
+ @Test
+ public void function() {
+ Context ctx = new Context(types(), system()).push(struct());
+ String val = process(ctx,
+ """
+ Handle {/name}$MH = Handle.of{=result/type 'void' 'Void'}(
+ {~result/type 'void' "{@result type}{?fields ', '}"}
+ {@fields "{/name} ({/deref})" <[,
+ ]>});
+ """);
+
+ String cmp
+ = """
+ Handle Object$MH = Handle.ofVoid(
+
+ x (f64),
+ y (f32),
+ z (i32));
+ """;
+
+ assertEquals(cmp, val);
+ }
+
+}
--- /dev/null
+package au.notzed.nativez.tools;
+
+import static org.junit.jupiter.api.Assertions.*;
+import org.junit.jupiter.api.Test;
+
+public class UtilTest {
+ String[] snake = {
+ "do_thing_blah",
+ "a_b",
+ "foobar",
+ };
+ String[] studly = {
+ "DoThingBlah",
+ "AB",
+ "Foobar",
+ };
+ String[] camel = {
+ "doThingBlah",
+ "aB",
+ "foobar",
+ };
+ // These can only go one way
+ String[] snakeleading = {
+ "_foo_bar",
+ "__foo_bar",
+ "__a__"
+ };
+ String[] studlyleading = {
+ "FooBar",
+ "FooBar",
+ "A"
+ };
+ String[] camelleading = {
+ "fooBar",
+ "fooBar",
+ "a"
+ };
+
+ @Test
+ public void leadingStudly() {
+ assertEquals("FooBar", Util.toStudlyCaps("_foo_bar"));
+ assertEquals("FooBar", Util.toStudlyCaps("__foo_bar"));
+ }
+
+ @Test
+ public void leadingCamel() {
+ assertEquals("fooBar", Util.toCamelCase("_foo_bar"));
+ assertEquals("fooBar", Util.toCamelCase("__foo_bar"));
+ }
+
+ @Test
+ public void studly() {
+ assertEquals("FooBar", Util.toStudlyCaps("foo_bar"));
+ assertEquals("FooBar", Util.toStudlyCaps("foo_bar"));
+ assertEquals("FooBarBaz", Util.toStudlyCaps("foo_bar_baz"));
+ assertEquals("ABC", Util.toStudlyCaps("a_b_c"));
+ assertEquals("Abc", Util.toStudlyCaps("abc"));
+ }
+
+ @Test
+ public void camel() {
+ assertEquals("fooBar", Util.toCamelCase("foo_bar"));
+ assertEquals("fooBar", Util.toCamelCase("foo_bar"));
+ assertEquals("fooBarBaz", Util.toCamelCase("foo_bar_baz"));
+ assertEquals("aBC", Util.toCamelCase("a_b_c"));
+ assertEquals("abc", Util.toCamelCase("abc"));
+ }
+
+ @Test
+ public void toSnake() {
+ for (int i = 0; i < snake.length; i++) {
+ assertEquals(snake[i], Util.toSnakeCase(camel[i]));
+ assertEquals(snake[i], Util.toSnakeCase(studly[i]));
+ }
+ }
+
+ @Test
+ public void isnumber() {
+ String[] test = {
+ "123",
+ "-123",
+ "+123",
+ "+",
+ "-",
+ "23-",
+ "one"};
+ boolean[] res = {
+ true,
+ true,
+ true,
+ false,
+ false,
+ false,
+ false
+ };
+ for (int i = 0; i < test.length; i++) {
+ if (res[i])
+ assertTrue(Util.isNumber.test(test[i]), String.format("true? isNumber(\"%s\")", test[i]));
+ else
+ assertFalse(Util.isNumber.test(test[i]), String.format("false? isNumber(\"%s\")", test[i]));
+ }
+ }
+
+ @Test
+ public void compareStrings() {
+ assertEquals(0, Util.compareCasting("a", "a"));
+ assertEquals(-1, Util.compareCasting("a", "b"));
+ assertEquals(1, Util.compareCasting("b", "a"));
+ assertEquals(0, Util.compareCasting(" a", "a "));
+ assertEquals(0, Util.compareCasting("a", " a "));
+ }
+
+ @Test
+ public void compareNumbers() {
+ assertEquals(0, Util.compareCasting("0", "0"));
+ assertEquals(-1, Util.compareCasting("99", "100"));
+ assertEquals(1, Util.compareCasting("74", "1"));
+ assertEquals(0, Util.compareCasting("-1", "-1"));
+ assertEquals(-1, Util.compareCasting("-1", "0"));
+ }
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2025 Michael Zucchi
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package au.notzed.nativez.tools;
+
+import au.notzed.nativez.tools.Value.Array;
+import au.notzed.nativez.tools.Value.Hash;
+import au.notzed.nativez.tools.Value.Scalar;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.IntStream;
+import org.junit.jupiter.api.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class ValueTest {
+
+ static Value hash;
+ static Value array;
+
+ public static Array array(List<String> list, int depth) {
+ List<Value> array = new ArrayList<>();
+
+ for (String s: list) {
+ array.add(Scalar.ofLiteral("scalar." + s));
+ if (depth > 0) {
+ array.add(array(list, depth - 1));
+ array.add(hash(list, depth - 1));
+ }
+ }
+
+ return Array.of(array);
+ }
+
+ public static Hash hash(List<String> list, int depth) {
+ Map<String, Value> root = new HashMap<>();
+
+ for (String s: list) {
+ root.put("scalar." + s, Scalar.ofLiteral("scalar." + s));
+ if (depth > 0) {
+ root.put("list." + s, Array.of(list.stream().map(Scalar::ofLiteral).map(Value.class::cast).toList()));
+ root.put("hash." + s, hash(list, depth - 1));
+ }
+ }
+ return Hash.of(root);
+ }
+
+ @BeforeAll
+ static void setup() {
+ List<String> list = List.of("a", "b", "c");
+
+ hash = hash(list, 2);
+
+ array = Array.of(IntStream.range(0, 10).mapToObj(String::valueOf).map(Scalar::ofLiteral).map(Value.class::cast).toList());
+ }
+
+ @Test
+ public void resolve() {
+ assertInstanceOf(Scalar.class, hash.getValue("scalar.a"));
+ assertInstanceOf(Array.class, hash.getValue("list.a"));
+ assertInstanceOf(Hash.class, hash.getValue("hash.a"));
+
+ assertNotNull(hash.getValue("nothing"));
+ assertEquals(hash.getValue("nothing"), Scalar.UNDEFINED);
+ }
+
+ @Test
+ public void resolvePath() {
+ assertInstanceOf(Scalar.class, hash.resolve("hash.a", "scalar.a"));
+ assertInstanceOf(Array.class, hash.resolve("hash.a", "list.a"));
+ assertInstanceOf(Hash.class, hash.resolve("hash.a", "hash.a"));
+
+ assertInstanceOf(Scalar.class, hash.resolve("hash.a", "list.a", "0"));
+ assertEquals("a", ((Scalar)hash.resolve("hash.a", "list.a", "0")).value());
+ assertEquals("b", ((Scalar)hash.resolve("hash.a", "list.a", "1")).value());
+ assertEquals("c", ((Scalar)hash.resolve("hash.a", "list.a", "2")).value());
+ }
+
+ @Test
+ public void arrays() {
+ assertInstanceOf(Scalar.class, array.getValue("0"));
+ for (int i = 0; i < 10; i++) {
+ String k = String.valueOf(i);
+ assertEquals(array.resolveString(k), k);
+ }
+ assertEquals(array.getValue("99"), Scalar.UNDEFINED);
+
+ assertInstanceOf(Array.class, array.getValue("0:0"));
+ assertInstanceOf(Array.class, array.getValue("0:99"));
+ assertInstanceOf(Array.class, array.getValue("1:-1"));
+ assertInstanceOf(Array.class, array.getValue("99:99"));
+ }
+
+ @Test
+ public void arraySubarray() {
+ Array list;
+
+ list = (Array)array.getValue("1:-1");
+ assertEquals(8, list.value().size());
+ assertEquals("1", ((Scalar)list.value().get(0)).value());
+ assertEquals("8", ((Scalar)list.value().get(list.value().size() - 1)).value());
+
+ list = (Array)array.getValue("1:1");
+ assertEquals(0, list.value().size());
+
+ list = (Array)array.getValue("5:-");
+ assertEquals(5, list.value().size());
+ assertEquals("5", ((Scalar)list.value().get(0)).value());
+ assertEquals("9", ((Scalar)list.value().get(list.value().size() - 1)).value());
+
+ list = (Array)array.getValue("-:5");
+ assertEquals(5, list.value().size());
+ assertEquals("0", ((Scalar)list.value().get(0)).value());
+ assertEquals("4", ((Scalar)list.value().get(list.value().size() - 1)).value());
+
+ list = (Array)array.getValue("-6:-4");
+ assertEquals(2, list.value().size());
+ assertEquals("4", ((Scalar)list.value().get(0)).value());
+ assertEquals("5", ((Scalar)list.value().get(list.value().size() - 1)).value());
+ }
+
+ @Test
+ public void arrayBackwards() {
+ for (int i = 0; i < 10; i++) {
+ String k = String.valueOf(-i - 1);
+ String j = String.valueOf(10 - i - 1);
+
+ assertEquals(array.resolveString(k), j);
+ }
+ }
+}