/** class Expr, our internal representation of an expression * in the H1 language. * @see http://www.radford.edu/~cs380/2025spring-ibarland/Homeworks/Project/ * * @author Ian Barland * @version 2025.Apr.12 * @license CC-BY -- share/adapt this file freely, but include attribution, thx. * https://creativecommons.org/licenses/by/4.0/ * https://creativecommons.org/licenses/by/4.0/legalcode * Including a link to the *original* file satisifies "appropriate attribution". */ import java.util.*; import java.util.function.*; import java.util.stream.*; /** An `Expr` represents (the parse tree of) an H0 expression. * @see its implementations `BinOp` etc, for its subtypes/variants. */ sealed interface Expr { /** Evaluate a given Expr. * @return the Value this Expr evaluates to. * (In H0, all values are numbers (doubles), but * in H3 that will change, which is why we have * pre-emptively made the return type 'Value'.) */ abstract public Value eval(); /** Return a String representation of this Expr. * The result will be something which can be * passed into 'parse(String)' to get the same * Expr back. That is, toString and parse are * inverses of each other. * @return a String representation of this Expr. */ abstract public String toString(); /** Return (our internal representation of) the first expression in s. * * @param s The source code for exactly one Expr. Must by syntactically correct. * @return (our internal representation of) the expression s. */ public static Expr parse(String s) { return Expr.parse(new java.util.Scanner(s)); } public static final String PUNCTUATION = "@=^{}><;!~#:?|\\(\\)\\[\\]/\\\\"; //as a regex-class // Note: Beware cautious '-' or '.' or '+' as punctuation, // if it introduces ambiguity with numbers. /** Return (our internal representation of) the first expression in s. * Side-effect: consume *exactly* the chars of that expr from the front of `s`. * @param s A scanner starting with the source code for an Expr. * @pre The Expr source must be syntactically correct. * @return (our internal representation of) the expression at front of `s`. */ public static Expr parse(Scanner s) { return parse(s,PUNCTUATION); } /** Return (our internal representation of) the first expression in s. * Side-effect: consume *exactly* the chars of that expr from the front of `s`. * @param s A scanner starting with the source code for an Expr. * @param punctuation A string of the chars to be interpreted as puncutation (*and* not part larger tokens). * @pre The Expr source must be syntactically correct. * @return (our internal representation of) the expression at front of `s`. * @see {@link Expr#parse(Scanner)} */ public static Expr parse(java.util.Scanner s, String punctuation) { if (UtilIan.hasNextDoubleSplittingBy(s,punctuation)) { return Num.parse(s,punctuation); } else if (UtilIan.hasNextSplittingBy(s, Paren.START_TOKEN, punctuation) ) { return Paren.parse(s,punctuation); } else if (BinOp.hasNextBinOpSplittingBy( s, punctuation) ) { return BinOp.parse(s,punctuation); } else if (UtilIan.hasNextSplittingBy(s, IfZero.START_TOKEN, punctuation) ) { return IfZero.parse(s,punctuation); } else { /* Unknown syntax! */ String tokens = ""; while (s.hasNext()) { tokens += UtilIan.nextSplittingBy(s,punctuation) + " "; } throw new java.util.InputMismatchException( "Expr.parse: something has gone awry! Encountered \"" + tokens + "\""); } } } /** Our internal representation of a BinOp * in the H0 language. * @see http://www.radford.edu/~cs380/2025spring-ibarland/Homeworks/Project/ */ record BinOp( String op, Expr left, Expr right) implements Expr { //static final String START_TOKEN = "finna"; //static final String END_TOKEN = ""; public static final List>> OP_FUNCS = List.of( new Pair>( "add", Double::sum ) , new Pair>( "skim", (a,b) -> b-a ) , new Pair>( "scale", (a,b) -> a*b ) // I originally had `new Pair<>(...)`, omitting the full generic-type-args, // but some [only] students' compilers would complain about that! // (err msg "can't convert Object to BinaryOperator") ); public static final List OPS = OP_FUNCS.stream().map( Pair::first ).collect(Collectors.toList()); //OPS = List.of( "-", "+", "/" ); // <-- Alternately just use this, if `OP_FUNCS` not declared. @Override public String toString( /* BinOp this */ ) { return this.op + " " + this.left.toString() + " " + switch(this.op) { case "add" -> "to "; case "skim" -> "off "; case "scale" -> "to serve "; default -> throw new RuntimeException("Unhandled case: BinOp.toString for "+ this.op); } + this.right.toString() ; } /** “@Override” {@link Expr#parse} */ public static BinOp parse(java.util.Scanner s, String punctuation) { assert BinOp.hasNextBinOpSplittingBy( s, punctuation ); String op = UtilIan.nextSplittingBy(s,punctuation); Expr lefty = Expr.parse(s,punctuation); // NOTE: recur with `Expr.parse` -- not `parse`=`BinOp.parse` which is NOT what we want! dbg("BinOp#parse (left): got " + lefty.toString()+ ". " ); // Now read the in-fix part of our op; the exact token expected // depends on which op: switch (op) { case "add" -> UtilIan.verifyToken( UtilIan.nextSplittingBy(s, punctuation), "to" ); case "skim" -> UtilIan.verifyToken( UtilIan.nextSplittingBy(s, punctuation), "off" ); case "scale" -> { UtilIan.verifyToken( UtilIan.nextSplittingBy(s, punctuation), "to" ); UtilIan.verifyToken( UtilIan.nextSplittingBy(s, punctuation), "serve" ); } } Expr righty = Expr.parse(s,punctuation); dbg("BinOp#parse (right): got " + righty.toString() + ". "); return new BinOp(op, lefty, righty ); } /** consume one H0 off the front of `s` and return it. */ private static String parseOp( java.util.Scanner s, String punctuation) { String firstWord = UtilIan.nextSplittingBy(s,punctuation); // "glow up" being two words causes us a bit of headache. String nextWord = (firstWord.equals("glow") ? " "+UtilIan.nextSplittingBy(s,punctuation) : ""); String fullOp = firstWord + nextWord; dbg("BinOp#parse (op): got " + fullOp + "."); if (!(OPS.contains(fullOp))) throw new InputMismatchException(String.format("Unknown operator \"%s\".", fullOp)); return fullOp; } @Override public Value eval( /* BinOp this */) { String theOp = this.op; double leftVal = ((Num)(this.left .eval())).doubleValue(); double rightVal = ((Num)(this.right.eval())).doubleValue(); return evalBinOp( theOp, leftVal, rightVal ); } /** Evaluate E's `op` w/ `l` and `r` */ static Value evalBinOp( String op, double l, double r ) { Pair> opFunc = OP_FUNCS.stream() // didn't declare OP_FUNCS? See comment below. .filter(pr -> pr.first().equals(op) ) .findFirst() .orElseThrow( () -> new InputMismatchException(String.format("Unknown op `%s`; must be one of %s.", op, OPS)) ); return new Num( opFunc.second().apply(l,r) ); /* If OP_FUNCS wasn't defined, delete the above two statments, and instead use the following: return switch(op) { case "add" -> new Num(l+r); case "skim" -> new Num(r-l); case "scale" -> new Num(l*r); default -> throw new RuntimeException(String.format("Unknown op `%s`; must be one of %s.", op, OPS)); }; */ } /** Is the next token of `s` the opening word of a BinOp ? * (tokens delimited by whitespace and `delimeterChars`) */ public static boolean hasNextBinOpSplittingBy( Scanner s, String delimiterChars ) { boolean seenAnOp = false; for (int i = 0; i CONT_TOKENS1 = List.of(";", "add"); static final List CONT_TOKENS2 = List.of("to","taste","or","use"); static final String CONT_TOKEN3 = "instead"; @Override public String toString() { return START_TOKEN + " " + this.test.toString() + CONT_TOKENS1.stream().reduce("", (s1,s2) -> s1+(PUNCTUATION.contains(s2)?"":" ")+s2) + " " + this.thenAns.toString() + CONT_TOKENS2.stream().reduce("", (s1,s2) -> s1+" "+s2) + " " + this.elseAns.toString() + " " + CONT_TOKEN3 ; } /** “@Override” {@link Expr#parse} */ public static IfZero parse( java.util.Scanner s, String punctuation ) { UtilIan.verifyToken( UtilIan.nextSplittingBy(s,punctuation), START_TOKEN ); // consume&verify the "no" // NOTE: recur with `Expr.parse` -- not `parse` = `IfZero.parse` which is NOT what we want! Expr theTest = Expr.parse(s,punctuation); for (String token : CONT_TOKENS1) { // consume&verify the infix tokens UtilIan.verifyToken( UtilIan.nextSplittingBy(s,punctuation), token ); } Expr theThenAnswer = Expr.parse(s,punctuation); for (String token : CONT_TOKENS2) { // consume&verify the infix tokens UtilIan.verifyToken( UtilIan.nextSplittingBy(s,punctuation), token ); } Expr theElseAnswer = Expr.parse(s,punctuation); UtilIan.verifyToken( UtilIan.nextSplittingBy(s,punctuation), CONT_TOKEN3 ); return new IfZero(theTest,theThenAnswer,theElseAnswer); } static final double EPSILON = 1e-10; @Override public Value eval() { if ( Math.abs(((Num)(this.test.eval())).doubleValue()) < EPSILON ) { return this.thenAns.eval(); } else { return this.elseAns.eval(); } } } /** Our internal representation of a Paren * in the H0 language. * @see http://www.radford.edu/~cs380/2025spring-ibarland/Homeworks/Project/ */ record Paren(Expr e) implements Expr { static final String START_TOKEN = "shake"; static final String STOP_TOKEN = "bake"; @Override public String toString(/* Paren this */) { return START_TOKEN + " " + this.e.toString() + " " + STOP_TOKEN ; } /** “@Override” {@link Expr#parse} */ public static Paren parse( java.util.Scanner s, String punctuation ) { UtilIan.verifyToken( UtilIan.nextSplittingBy(s,punctuation), START_TOKEN); // Consume the opening quasi-'(', and continue. // NOTE: recur with `Expr.parse` -- not `parse` = `Paren.parse` which is NOT what we want! Expr theInsideExpr = Expr.parse(s,punctuation); UtilIan.verifyToken( UtilIan.nextSplittingBy(s,punctuation), STOP_TOKEN); // Consume the closing quasi-')', and continue. return new Paren(theInsideExpr); } public Value eval() { return this.e.eval(); } } /** * Abstract class Value - * The type of all Expressions which self-evaluate: * Numbers, and (later, in H3) functions. * * @see http://www.radford.edu/~cs380/2025spring-ibarland/Homeworks/Project/ * * (This architecture works as long as every Value is a type of * Expression, which is a nice feature to have in a language; * it means that any result can be pasted/substituted into * larger expressions, which promotes a substitution model * of evaluation.) * */ sealed interface Value extends Expr { default Value eval() { return this; } // We evaluate to ourselves, since we're a Value. } /** Our internal representation of a number in the H0 language. * @see http://www.radford.edu/~cs380/2025spring-ibarland/Project */ record Num(double x) implements Value { @Override public Value eval() { return this; } // Arg, `Value.java` has this (same) implemenation as default method, but javac // complains that we're implementing Expr w/o defining 'eval'. @Override public String toString() { // Let's make ints look better than, say, "43.0": // Omit trailing 0's after decimal, rounding to 9 places: return formatter.format(this.x); } /** “@Override” {@link Expr#parse} */ public static Num parse( Scanner s, String punctuation ) { return new Num( UtilIan.nextDoubleSplittingBy(s,punctuation) ); } private static java.text.DecimalFormat formatter = new java.text.DecimalFormat("#.#########"); /** Return the Java double this Value represents * (for use by other primitives in our language.) * @return the Java double this Value represents. */ double doubleValue() { return this.x; } @Override /** Compare two Numbers, but with some tolerance. */ public boolean equals( /* Num this, */ Object that) { if (this==that) { return true; } else if (that==null) { return false; } else if (this.getClass() != that.getClass()) { return false; } else { Num thatt = (Num) that; return UtilIan.equalsApprox(this.x,thatt.x); } } }