Major reworking of the generator, added junit5 tests. foreign-abi
authorNot Zed <notzed@gmail.com>
Wed, 30 Jul 2025 11:31:07 +0000 (21:01 +0930)
committerNot Zed <notzed@gmail.com>
Wed, 30 Jul 2025 11:31:07 +0000 (21:01 +0930)
 - all values now go through Value.{Scalar,Array,Hash} with simpler
   semantics.  Value is now an interface and each instance is a record.
 - junit5 tests for some classes, they run from the makefile only as yet.
 - enhanced api format
  - flags and named options on some objects/fields
  - pattern matched 'things' add named-group's from the pattern to the map
  - namespace for certain options
 - enhanced templates
   - 'builtin function's for naming convention conversion.
   - relative paths better defined (using '/') including leading '..' parts
   - better range specification/behaviour
   - comparisons use numbers (long) if they are numbers
 - bit better logging
 - removed some dumb ideas/dead code etc.

24 files changed:
Makefile
config.make.in
java.make
junit5.make [new file with mode: 0644]
maven.make [new file with mode: 0644]
nbproject/project.properties
src/notzed.nativez.tools/classes/au/notzed/nativez/tools/API.java
src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Array.java [deleted file]
src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Context.java
src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Data.java [deleted file]
src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Export.java
src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Generator.java
src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Hash.java [deleted file]
src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Scalar.java [deleted file]
src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Template.java
src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Text.java [deleted file]
src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Tokeniser.java
src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Util.java
src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Value.java
src/notzed.nativez.tools/tests/au/notzed/nativez/tools/ContextTest.java [new file with mode: 0644]
src/notzed.nativez.tools/tests/au/notzed/nativez/tools/Data.java [new file with mode: 0644]
src/notzed.nativez.tools/tests/au/notzed/nativez/tools/TemplateTest.java [new file with mode: 0644]
src/notzed.nativez.tools/tests/au/notzed/nativez/tools/UtilTest.java [new file with mode: 0644]
src/notzed.nativez.tools/tests/au/notzed/nativez/tools/ValueTest.java [new file with mode: 0644]

index 6b5c0a5..21f7b77 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -10,7 +10,8 @@ dist_EXTRA=README                             \
 
 include config.make
 
-java_MODULES=notzed.nativez
+java_MODULES=notzed.nativez notzed.nativez.tools
 
 include java.make
-
+include junit5.make
+include maven.make
index e598104..f2e8fc3 100644 (file)
@@ -4,11 +4,12 @@ TARGET ?= linux-amd64
 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 = \
index c3b9030..41e9362 100644 (file)
--- a/java.make
+++ b/java.make
 
 # ######################################################################
 
+MAKEFLAGS += --no-builtin-rules
+
 all_MODULES = $(java_MODULES) $(native_MODULES)
 
 E:=
@@ -194,7 +196,7 @@ all:
 jar:
 gen:
 
-.PHONY: all clean jar gen $(java_MODULES)
+.PHONY: all clean jar gen $(java_MODULES) dist
 clean:
        rm -rf bin
 
@@ -341,7 +343,6 @@ $(foreach module,$(java_MODULES),$(eval $(call java_targets,$(module))))
 # 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) \
diff --git a/junit5.make b/junit5.make
new file mode 100644 (file)
index 0000000..850bcc5
--- /dev/null
@@ -0,0 +1,98 @@
+#
+# 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)))))
+
+# ######################################################################
diff --git a/maven.make b/maven.make
new file mode 100644 (file)
index 0000000..2fd31d3
--- /dev/null
@@ -0,0 +1,86 @@
+#
+# 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
index 796d4da..f408e86 100644 (file)
@@ -36,6 +36,7 @@ dist.jlink.dir=${dist.dir}/jlink
 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=
@@ -50,6 +51,7 @@ javac.processorpath=\
 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}:\
index 287266a..0ed2710 100644 (file)
  */
 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;
@@ -31,11 +32,15 @@ import java.util.Map;
 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>
@@ -43,87 +48,79 @@ import java.util.stream.Stream;
 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);
                }
@@ -134,57 +131,40 @@ public class API {
 
                        // 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);
+                               }
+                       });
+
                }
 
                /**
@@ -194,155 +174,56 @@ public class API {
                 *
                 * @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 {
@@ -350,354 +231,327 @@ public class API {
                        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"));
-       }
-
 }
diff --git a/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Array.java b/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Array.java
deleted file mode 100644 (file)
index 658d5e9..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * 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("]");
-       }
-
-}
index e566efa..3e5d26b 100644 (file)
@@ -19,427 +19,166 @@ package au.notzed.nativez.tools;
 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;
        }
+
 }
diff --git a/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Data.java b/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Data.java
deleted file mode 100644 (file)
index fc191f9..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * 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);
-
-}
index 3492e19..7245226 100644 (file)
  */
 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;
@@ -33,7 +38,8 @@ public class Export {
 
        /**
         * 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;
 
@@ -43,34 +49,19 @@ public class Export {
                // 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) {
@@ -78,82 +69,79 @@ public class Export {
 
                        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);
                }
        }
 
@@ -162,61 +150,56 @@ public class Export {
         * 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"));
-       }
 }
index e7e82cf..d471708 100644 (file)
  */
 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);
@@ -49,132 +55,102 @@ public class Generator {
                }
        }
 
-       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();
        }
-
 }
diff --git a/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Hash.java b/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Hash.java
deleted file mode 100644 (file)
index f98a198..0000000
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * 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);
-       }
-}
diff --git a/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Scalar.java b/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Scalar.java
deleted file mode 100644 (file)
index bd04116..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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;
-       }
-
-}
index a5698f3..c91afcd 100644 (file)
@@ -16,6 +16,8 @@
  */
 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;
@@ -27,7 +29,9 @@ import java.util.Arrays;
 import java.util.List;
 
 /**
- * Pre-parsed template.
+ * Template processor.
+ *
+ * Uses Context for resolving templates and keys.
  */
 public class Template {
        List<Command> seq;
@@ -45,115 +49,124 @@ public class Template {
                });
        }
 
-       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;
                        }
                        }
                }
@@ -171,7 +184,26 @@ public class Template {
                }
        }
 
-       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.
@@ -180,14 +212,14 @@ public class Template {
                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);
                                }
                        },
@@ -195,45 +227,45 @@ public class Template {
                        },
                        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);
                                }
                        };
@@ -251,22 +283,22 @@ public class Template {
                                        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;
                        }
 
                }
@@ -288,46 +320,47 @@ public class Template {
                        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);
+                       };
                }
        }
 
@@ -350,24 +383,24 @@ public class Template {
                        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();
@@ -376,9 +409,12 @@ public class Template {
                                                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();
                                }
@@ -392,15 +428,4 @@ public class Template {
                        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);
-               });
-       }
-
 }
diff --git a/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Text.java b/src/notzed.nativez.tools/classes/au/notzed/nativez/tools/Text.java
deleted file mode 100644 (file)
index 64c0685..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * 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);
-       }
-}
index d0ce00e..4f071fb 100644 (file)
  */
 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>&lt;[ ... ]&gt; literal string (nested - maybe it shouldn't be)
- * <li>&lt;{ ... }&gt; 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;
@@ -87,6 +73,7 @@ public class Tokeniser implements AutoCloseable {
                StringBuilder marker = new StringBuilder();
                StringBuilder line = new StringBuilder(this.line);
 
+               // TODO: get filename
                try {
                        int c;
                        while ((c = readInternal()) != -1 && c != '\n')
@@ -105,13 +92,7 @@ public class Tokeniser implements AutoCloseable {
        }
 
        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);
        }
 
@@ -215,34 +196,23 @@ public class Tokeniser implements AutoCloseable {
        }
 
        // 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.
         */
@@ -261,7 +231,21 @@ public class Tokeniser implements AutoCloseable {
        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;
 
@@ -277,36 +261,28 @@ public class Tokeniser implements AutoCloseable {
                        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 == '\\') {
@@ -328,6 +304,8 @@ public class Tokeniser implements AutoCloseable {
                        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;
        }
 
@@ -337,25 +315,25 @@ public class Tokeniser implements AutoCloseable {
                        : 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;
 
@@ -368,11 +346,21 @@ public class Tokeniser implements AutoCloseable {
                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 {
@@ -381,61 +369,57 @@ public class Tokeniser implements AutoCloseable {
                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;
        }*/
 }
index 05ef8a7..9fa7bf2 100644 (file)
@@ -16,7 +16,8 @@
  */
 package au.notzed.nativez.tools;
 
-import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.regex.Pattern;
 
 public class Util {
 
@@ -46,34 +47,40 @@ 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);
                }
        }
 
index 87d8ef8..67f6e5b 100644 (file)
@@ -20,23 +20,272 @@ import java.io.IOException;
 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 {
@@ -60,39 +309,40 @@ public abstract class Value {
                        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"));
                        }
diff --git a/src/notzed.nativez.tools/tests/au/notzed/nativez/tools/ContextTest.java b/src/notzed.nativez.tools/tests/au/notzed/nativez/tools/ContextTest.java
new file mode 100644 (file)
index 0000000..fb4c151
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+ * 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++;
+               }
+       }
+
+}
diff --git a/src/notzed.nativez.tools/tests/au/notzed/nativez/tools/Data.java b/src/notzed.nativez.tools/tests/au/notzed/nativez/tools/Data.java
new file mode 100644 (file)
index 0000000..e124d08
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * 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);
+       }
+
+}
diff --git a/src/notzed.nativez.tools/tests/au/notzed/nativez/tools/TemplateTest.java b/src/notzed.nativez.tools/tests/au/notzed/nativez/tools/TemplateTest.java
new file mode 100644 (file)
index 0000000..80d954b
--- /dev/null
@@ -0,0 +1,178 @@
+/*
+ * 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);
+       }
+
+}
diff --git a/src/notzed.nativez.tools/tests/au/notzed/nativez/tools/UtilTest.java b/src/notzed.nativez.tools/tests/au/notzed/nativez/tools/UtilTest.java
new file mode 100644 (file)
index 0000000..13f231f
--- /dev/null
@@ -0,0 +1,122 @@
+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"));
+       }
+
+}
diff --git a/src/notzed.nativez.tools/tests/au/notzed/nativez/tools/ValueTest.java b/src/notzed.nativez.tools/tests/au/notzed/nativez/tools/ValueTest.java
new file mode 100644 (file)
index 0000000..7b926de
--- /dev/null
@@ -0,0 +1,145 @@
+/*
+ * 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);
+               }
+       }
+}