अपनी स्वयं की प्रोग्रामिंग भाषा बनाने के इस भाग में, हम कक्षाओं को पहले से परिभाषित संरचनाओं के विस्तार के रूप में लागू करेंगे। कृपया पिछले भागों को देखें:
पूर्ण स्रोत कोड उपलब्ध है
पहले खंड में, हम शाब्दिक विश्लेषण को कवर करेंगे। संक्षेप में, यह स्रोत कोड को भाषा के शब्दों में विभाजित करने की एक प्रक्रिया है, जैसे कि कीवर्ड, चर, ऑपरेटर, आदि।
आप पिछले भागों से याद कर सकते हैं कि मैं टोकन टाइप एनम में रेगेक्स एक्सप्रेशन का उपयोग सभी लेक्सेम प्रकारों को परिभाषित करने के लिए कर रहा था।
आइए निम्न वर्ग के प्रोटोटाइप को देखें और लापता रेगेक्स भागों को हमारे TokenType
में जोड़ें:
Keyword
लेक्सेम के एक्सप्रेशन में class
शब्द डालना होगा: package org.example.toylanguage.token; ... public enum TokenType { ... Keyword("(if|elif|else|end|print|input|fun|return|loop|in|by|break|next|class)(?=\\s|$)"), ... private final String regex; }
This
लेक्सेम चाहिए: public enum TokenType { ... This("(this)(?=,|\\s|$)"); private final String regex; }
दूसरे खंड में, हम अपने भाषा नियमों का पालन करते हुए लेक्सिकल एनालाइज़र से प्राप्त शब्दांशों को अंतिम कथनों में रूपांतरित करेंगे।
जब हम एक वर्ग या एक समारोह की घोषणा करते हैं, तो यह घोषणा परिभाषित पृथक सीमाओं के भीतर उपलब्ध होनी चाहिए। उदाहरण के लिए, यदि हम निम्नलिखित सूची में turn_on []
नामक फ़ंक्शन की घोषणा करते हैं, तो यह घोषणा के बाद निष्पादन के लिए उपलब्ध होगा:
लेकिन अगर हम एक ही फंक्शन को क्लास स्कोप के अंदर घोषित करते हैं, तो यह फंक्शन सीधे मुख्य ब्लॉक से एक्सेस नहीं किया जाएगा:
DefinitionScope
वर्ग बनाएंगे और सभी घोषित परिभाषाओं को कक्षाओं और कार्यों के लिए दो सेटों के अंदर संग्रहीत करेंगे: package org.example.toylanguage.context.definition; public class DefinitionScope { private final Set<ClassDefinition> classes; private final Set<FunctionDefinition> functions; public DefinitionScope() { this.classes = new HashSet<>(); this.functions = new HashSet<>(); } }
यह क्षमता प्रदान करने के लिए, हम DefinitionScope
पैरेंट इंस्टेंस को ऊपरी परत के संदर्भ के रूप में जोड़ेंगे, जिसका उपयोग हम शीर्ष परिभाषा परत पर चढ़ने के लिए करेंगे।
public class DefinitionScope { private final Set<ClassDefinition> classes; private final Set<FunctionDefinition> functions; private final DefinitionScope parent; public DefinitionScope(DefinitionScope parent) { this.classes = new HashSet<>(); this.functions = new HashSet<>(); this.parent = parent; } }
public class DefinitionScope { … public ClassDefinition getClass(String name) { Optional<ClassDefinition> classDefinition = classes.stream() .filter(t -> t.getName().equals(name)) .findAny(); if (classDefinition.isPresent()) return classDefinition.get(); else if (parent != null) return parent.getClass(name); else throw new ExecutionException(String.format("Class is not defined: %s", name)); } public void addClass(ClassDefinition classDefinition) { classes.add(classDefinition); } public FunctionDefinition getFunction(String name) { Optional<FunctionDefinition> functionDefinition = functions.stream() .filter(t -> t.getName().equals(name)) .findAny(); if (functionDefinition.isPresent()) return functionDefinition.get(); else if (parent != null) return parent.getFunction(name); else throw new ExecutionException(String.format("Function is not defined: %s", name)); } public void addFunction(FunctionDefinition functionDefinition) { functions.add(functionDefinition); } }
java.util.Stack
संग्रह (LIFO) का उपयोग करके संदर्भ वर्ग बनाते हैं: package org.example.toylanguage.context.definition; public class DefinitionContext { private final static Stack<DefinitionScope> scopes = new Stack<>(); public static DefinitionScope getScope() { return scopes.peek(); } public static DefinitionScope newScope() { return new DefinitionScope(scopes.isEmpty() ? null : scopes.peek()); } public static void pushScope(DefinitionScope scope) { scopes.push(scope); } public static void endScope() { scopes.pop(); } }
इस सेक्शन में, हम क्लास और फंक्शन वेरिएबल्स को मैनेज करने के लिए MemoryScope
को कवर करेंगे।
लेकिन अगर हम किसी फ़ंक्शन या वर्ग के भीतर एक चर घोषित करते हैं, तो चर अब कोड के मुख्य (ऊपरी) ब्लॉक से उपलब्ध नहीं होगा:
एक विशिष्ट दायरे में परिभाषित इस तर्क और स्टोर चर को लागू करने के लिए, हम MemoryScope
वर्ग बनाते हैं जिसमें चर नाम के साथ एक कुंजी के रूप में एक मानचित्र और मान के रूप में चर Value
होगा:
public class MemoryScope { private final Map<String, Value<?>> variables; public MemoryScope() { this.variables = new HashMap<>(); } }
DefinitionScope
के समान, हम पैरेंट के स्कोप वेरिएबल्स तक पहुंच प्रदान करते हैं: public class MemoryScope { private final Map<String, Value<?>> variables; private final MemoryScope parent; public MemoryScope(MemoryScope parent) { this.variables = new HashMap<>(); this.parent = parent; } }
MemoryScope
परत में पहले से ही एक परिभाषित चर है: public class MemoryScope { ... public Value<?> get(String name) { Value<?> value = variables.get(name); if (value != null) return value; else if (parent != null) return parent.get(name); else return NullValue.NULL_INSTANCE; } public void set(String name, Value<?> value) { MemoryScope variableScope = findScope(name); if (variableScope == null) { variables.put(name, value); } else { variableScope.variables.put(name, value); } } private MemoryScope findScope(String name) { if (variables.containsKey(name)) return this; return parent == null ? null : parent.findScope(name); } }
set
और तरीकों get
अलावा, हम MemoryScope
की वर्तमान (स्थानीय) परत के साथ बातचीत करने के लिए दो और कार्यान्वयन जोड़ते हैं: public class MemoryScope { ... public Value<?> getLocal(String name) { return variables.get(name); } public void setLocal(String name, Value<?> value) { variables.put(name, value); } }
इन विधियों का उपयोग बाद में फ़ंक्शन तर्कों या कक्षा के उदाहरण तर्कों को प्रारंभ करने के लिए किया जाएगा। उदाहरण के लिए, यदि हम Lamp
वर्ग का एक उदाहरण बनाते हैं और पूर्वनिर्धारित वैश्विक type
के चर को पास करते हैं, तो इस चर को तब नहीं बदला जाना चाहिए जब हम lamp_instance :: type
की संपत्ति को अपडेट करने का प्रयास करते हैं:
java.util.Stack
संग्रह का उपयोग करके MemoryContext
कार्यान्वयन बनाते हैं: package org.example.toylanguage.context; public class MemoryContext { private static final Stack<MemoryScope> scopes = new Stack<>(); public static MemoryScope getScope() { return scopes.peek(); } public static MemoryScope newScope() { return new MemoryScope(scopes.isEmpty() ? null : scopes.peek()); } public static void pushScope(MemoryScope scope) { scopes.push(scope); } public static void endScope() { scopes.pop(); } }
इस खंड में, हम वर्ग परिभाषाओं को पढ़ेंगे और संग्रहीत करेंगे।
package org.example.toylanguage.statement; public class ClassStatement { }
public class ClassStatement extends CompositeStatement { }
ClassDefinition
की घोषणा करते हैं: package org.example.toylanguage.context.definition; import java.util.List; @RequiredArgsConstructor @Getter @EqualsAndHashCode(onlyExplicitlyIncluded = true) public class ClassDefinition implements Definition { @EqualsAndHashCode.Include private final String name; private final List<String> arguments; private final ClassStatement statement; private final DefinitionScope definitionScope; }
class
मूल्य के साथ कीवर्ड शब्दांश से मिलते हैं, तो सबसे पहले हमें वर्ग कोष्ठक के अंदर वर्ग के नाम और उसके तर्कों को पढ़ने की आवश्यकता होती है:
package org.example.toylanguage; public class StatementParser { ... private void parseClassDefinition() { Token type = tokens.next(TokenType.Variable); List<String> arguments = new ArrayList<>(); if (tokens.peek(TokenType.GroupDivider, "[")) { tokens.next(TokenType.GroupDivider, "["); //skip opening square bracket while (!tokens.peek(TokenType.GroupDivider, "]")) { Token argumentToken = tokens.next(TokenType.Variable); arguments.add(argumentToken.getValue()); if (tokens.peek(TokenType.GroupDivider, ",")) tokens.next(); } tokens.next(TokenType.GroupDivider, "]"); //skip closing square bracket } } }
इन बयानों को स्टोर करने के लिए, हम पहले परिभाषित ClassStatement
का एक उदाहरण बनाते हैं:
private void parseClassDefinition() { ... ClassStatement classStatement = new ClassStatement(); }
DefinitionScope
की एक नई परत शुरू करते हैं: private void parseClassDefinition() { ... ClassStatement classStatement = new ClassStatement(); DefinitionScope classScope = DefinitionContext.newScope(); }
ClassDefinition
के एक उदाहरण को इनिशियलाइज़ करते हैं और इसे DefinitionContext
में डालते हैं: private void parseClassDefinition() { ... ClassStatement classStatement = new ClassStatement(); DefinitionScope classScope = DefinitionContext.newScope(); ClassDefinition classDefinition = new ClassDefinition(type.getValue(), arguments, classStatement, classScope); DefinitionContext.getScope().addClass(classDefinition); }
classStatement
इंस्टेंस के अंदर स्टेटमेंट्स को तब तक कलेक्ट करेगा, जब तक कि हम फाइनलिंग end
लेक्सेम को पूरा नहीं कर लेते, जिसे अंत में छोड़ देना चाहिए: private void parseClassDefinition() { ... //parse class statements StatementParser.parse(this, classStatement, classScope); tokens.next(TokenType.Keyword, "end"); }
इस बिंदु पर, हम पहले से ही कंस्ट्रक्टर स्टेटमेंट और फ़ंक्शंस के साथ क्लास की परिभाषाएँ पढ़ सकते हैं। अब, वर्ग के उदाहरण को पार्स करते हैं:
ClassValue
को परिभाषित करते हैं, जिसमें प्रत्येक वर्ग के उदाहरण की स्थिति होगी। कक्षाओं के विपरीत, कार्यों के विपरीत, एक स्थायी MemoryScope
होना चाहिए और यह दायरा हर बार जब हम कक्षा के उदाहरण के साथ बातचीत करते हैं तो सभी वर्ग के उदाहरण तर्कों और राज्य चर के साथ उपलब्ध होना चाहिए: public class ClassValue extends IterableValue<ClassDefinition> { private final MemoryScope memoryScope; public ClassValue(ClassDefinition definition, MemoryScope memoryScope) { super(definition); this.memoryScope = memoryScope; } }
MemoryContext
का उपयोग करके क्लास के इंस्टेंस गुणों के साथ काम करने के तरीके प्रदान करते हैं: public class ClassValue extends IterableValue<ClassDefinition> { private final MemoryScope memoryScope; public ClassValue(ClassDefinition definition, MemoryScope memoryScope) { super(definition); this.memoryScope = memoryScope; } @Override public String toString() { return getValue().getArguments().stream() .map(t -> t + " = " + getValue(t)) .collect(Collectors.joining(", ", getValue().getName() + " [ ", " ]")); } public Value<?> getValue(String name) { Value<?> result = MemoryContext.getScope().getLocal(name); return result != null ? result : NULL_INSTANCE; } public void setValue(String name, Value<?> value) { MemoryContext.getScope().setLocal(name, value); } }
MemoryScope#getLocal()
और MemoryScope#setLocal()
विधियों को कॉल करके, हम MemoryScope
चर की वर्तमान परत के साथ काम करते हैं। लेकिन क्लास के इंस्टेंस स्टेट तक पहुँचने से पहले, हमें इसके MemoryScope
को मेमोरी कॉन्टेक्स्ट में डालना होगा और जब हम समाप्त कर लेंगे तो इसे रिलीज़ कर MemoryContext
: public class ClassValue extends IterableValue<ClassDefinition> { ... public Value<?> getValue(String name) { MemoryContext.pushScope(memoryScope); try { Value<?> result = MemoryContext.getScope().getLocal(name); return result != null ? result : NULL_INSTANCE; } finally { MemoryContext.endScope(); } } public void setValue(String name, Value<?> value) { MemoryContext.pushScope(memoryScope); try { MemoryContext.getScope().setLocal(name, value); } finally { MemoryContext.endScope(); } } }
ClassExpression
को लागू कर सकते हैं जिसका उपयोग सिंटैक्स विश्लेषण के दौरान परिभाषित वर्ग उदाहरणों के निर्माण के लिए किया जाएगा। वर्ग की उदाहरण परिभाषा घोषित करने के लिए, हम ClassDefinition
और Expression
तर्कों की सूची प्रदान करते हैं जो कथन निष्पादन के दौरान अंतिम Value
उदाहरणों में परिवर्तित हो जाएंगे: package org.example.toylanguage.expression; @RequiredArgsConstructor @Getter public class ClassExpression implements Expression { private final ClassDefinition definition; private final List<Expression> arguments; @Override public Value<?> evaluate() { ... } }
Expression#evaluate()
विधि को लागू करें जिसका उपयोग निष्पादन के दौरान पहले से परिभाषित ClassValue
का एक उदाहरण बनाने के लिए किया जाएगा। सबसे पहले, हम Expression
तर्कों का Value
तर्कों में मूल्यांकन करते हैं: @Override public Value<?> evaluate() { //initialize class arguments List<Value<?>> values = arguments.stream() .map(Expression::evaluate) .collect(Collectors.toList()); }
@Override public Value<?> evaluate() { //initialize class arguments List<Value<?>> values = arguments.stream().map(Expression::evaluate).collect(Collectors.toList()); //get class's definition and statement ClassStatement classStatement = definition.getStatement(); //set separate scope MemoryScope classScope = new MemoryScope(null); MemoryContext.pushScope(classScope); try { ... } finally { MemoryContext.endScope(); } }
ClassValue
का एक उदाहरण बनाते हैं और वर्ग के Value
तर्कों को अलग-अलग मेमोरी स्कोप में लिखते हैं: try { //initialize constructor arguments ClassValue classValue = new ClassValue(definition, classScope); IntStream.range(0, definition.getArguments().size()).boxed() .forEach(i -> MemoryContext.getScope() .setLocal(definition.getArguments().get(i), values.size() > i ? values.get(i) : NullValue.NULL_INSTANCE)); ... } finally { MemoryContext.endScope(); }
कृपया ध्यान दें कि खाली MemoryScope
सेट करने से पहले हमने Expression
तर्कों को Value
तर्कों में बदल दिया। अन्यथा, हम उदाहरण के लिए, वर्ग के उदाहरण तर्कों तक पहुँचने में सक्षम नहीं होंगे:
ClassStatement
को निष्पादित कर सकते हैं। लेकिन इससे पहले, हमें क्लास के DefinitionScope
को कन्स्ट्रक्टर स्टेटमेंट के भीतर क्लास के कार्यों तक पहुंचने में सक्षम होना चाहिए: try { //initialize constructor arguments ClassValue classValue = new ClassValue(definition, classScope); ClassInstanceContext.pushValue(classValue); IntStream.range(0, definition.getArguments().size()).boxed() .forEach(i -> MemoryContext.getScope() .setLocal(definition.getArguments().get(i), values.size() > i ? values.get(i) : NullValue.NULL_INSTANCE)); //execute function body DefinitionContext.pushScope(definition.getDefinitionScope()); try { classStatement.execute(); } finally { DefinitionContext.endScope(); } return classValue; } finally { MemoryContext.endScope(); ClassInstanceContext.popValue(); }
9*. एक आखिरी बात, हम कक्षाओं को अधिक लचीला बना सकते हैं और कक्षा की परिभाषा घोषित करने से पहले उपयोगकर्ता को कक्षा के उदाहरण बनाने की अनुमति दे सकते हैं:
यह ClassDefinition
इनिशियलाइज़ेशन को DefinitionContext
को सौंप कर किया जा सकता है और इसे केवल तभी एक्सेस किया जा सकता है जब हम किसी एक्सप्रेशन का मूल्यांकन करते हैं:
public class ClassExpression implements Expression { private final String name; private final List<Expression> arguments; @Override public Value<?> evaluate() { //get class's definition and statement ClassDefinition definition = DefinitionContext.getScope().getClass(name); ... } }
परिभाषा से पहले कार्यों को लागू करने के लिए आप FunctionExpression के लिए एक ही प्रतिनिधिमंडल कर सकते हैं।
ExpressionReader
के साथ कक्षा के उदाहरणों को पढ़ना समाप्त कर सकते हैं। पहले परिभाषित संरचना उदाहरणों के बीच कोई अंतर नहीं है। हमें केवल Expression
के तर्कों को पढ़ने और ClassExpression
का निर्माण करने की आवश्यकता है: public class ExpressionReader ... // read class instance: new Class[arguments] private ClassExpression readClassInstance(Token token) { List<Expression> arguments = new ArrayList<>(); if (tokens.peekSameLine(TokenType.GroupDivider, "[")) { tokens.next(TokenType.GroupDivider, "["); //skip opening square bracket while (!tokens.peekSameLine(TokenType.GroupDivider, "]")) { Expression value = ExpressionReader.readExpression(this); arguments.add(value); if (tokens.peekSameLine(TokenType.GroupDivider, ",")) tokens.next(); } tokens.next(TokenType.GroupDivider, "]"); //skip closing square bracket } return new ClassExpression(token.getValue(), arguments); } }
इस समय, हम एक क्लास बना सकते हैं और क्लास के कंस्ट्रक्टर स्टेटमेंट को निष्पादित कर सकते हैं। लेकिन हम अभी भी वर्ग के कार्यों को क्रियान्वित करने में असमर्थ हैं।
FunctionExpression#evaluate
मेथड को ओवरलोड करते हैं जो ClassValue
को उस क्लास इंस्टेंस के संदर्भ के रूप में स्वीकार करेगा जिससे हम एक फंक्शन इनवॉइस करना चाहते हैं: package org.example.toylanguage.expression; public class FunctionExpression implements Expression { ... public Value<?> evaluate(ClassValue classValue) { } }
Expression
तर्कों को वर्तमान MemoryScope
का उपयोग करके Value
तर्कों में बदलना है: public Value<?> evaluate(ClassValue classValue) { //initialize function arguments List<Value<?>> values = argumentExpressions.stream() .map(Expression::evaluate) .collect(Collectors.toList()); }
MemoryScope
और DefinitionScope
को संदर्भ में पारित करने की आवश्यकता है: ... //get definition and memory scopes from class definition ClassDefinition classDefinition = classValue.getValue(); DefinitionScope classDefinitionScope = classDefinition.getDefinitionScope(); //set class's definition and memory scopes DefinitionContext.pushScope(classDefinitionScope); MemoryContext.pushScope(memoryScope);
Value
तर्क पास करते हैं: public Value<?> evaluate(ClassValue classValue) { //initialize function arguments List<Value<?>> values = argumentExpressions.stream().map(Expression::evaluate).collect(Collectors.toList()); //get definition and memory scopes from class definition ClassDefinition classDefinition = classValue.getValue(); DefinitionScope classDefinitionScope = classDefinition.getDefinitionScope(); MemoryScope memoryScope = classValue.getMemoryScope(); //set class's definition and memory scopes DefinitionContext.pushScope(classDefinitionScope); MemoryContext.pushScope(memoryScope); try { //proceed function return evaluate(values); } finally { DefinitionContext.endScope(); MemoryContext.endScope(); } }
::
ऑपरेटर का उपयोग करेंगे। वर्तमान में, इस ऑपरेटर को ClassPropertyOperator
( स्ट्रक्चरवैल्यूऑपरेटर ) कार्यान्वयन द्वारा प्रबंधित किया जाता है, जो वर्ग की संपत्तियों तक पहुँचने के लिए ज़िम्मेदार है:
आइए इसे एक ही डबल कोलन के साथ फ़ंक्शन इनवोकेशन का समर्थन करने के लिए सुधारें ::
वर्ण:
इस ऑपरेटर द्वारा क्लास के फ़ंक्शन को केवल तभी प्रबंधित किया जा सकता है जब बायां एक्सप्रेशन ClassExpression
और दूसरा FunctionExpression
एक्सप्रेशन हो:
package org.example.toylanguage.expression.operator; public class ClassPropertyOperator extends BinaryOperatorExpression implements AssignExpression { public ClassPropertyOperator(Expression left, Expression right) { super(left, right); } @Override public Value<?> evaluate() { Value<?> left = getLeft().evaluate(); if (left instanceof ClassValue) { if (getRight() instanceof VariableExpression) { // access class's property // new ClassInstance[] :: class_argument return ((ClassValue) left).getValue(((VariableExpression) getRight()).getName()); } else if (getRight() instanceof FunctionExpression) { // execute class's function // new ClassInstance[] :: class_function [] return ((FunctionExpression) getRight()).evaluate((ClassValue) left); } } throw new ExecutionException(String.format("Unable to access class's property `%s``", getRight())); } @Override public void assign(Value<?> value) { Value<?> left = getLeft().evaluate(); if (left instanceof ClassValue && getRight() instanceof VariableExpression) { String propertyName = ((VariableExpression) getRight()).getName(); ((ClassValue) left).setValue(propertyName, value); } } }
इस भाग में, हमने कक्षाएं लागू कीं। अब हम और अधिक जटिल चीजें बना सकते हैं, जैसे कि ढेर कार्यान्वयन:
main [] fun main [] stack = new Stack [] loop num in 0..5 # push 0,1,2,3,4 stack :: push [ num ] end size = stack :: size [] loop i in 0..size # should print 4,3,2,1,0 print stack :: pop [] end end class Stack [] arr = {} n = 0 fun push [ item ] this :: arr << item n = n + 1 end fun pop [] n = n - 1 item = arr { n } arr { n } = null return item end fun size [] return this :: n end end